Skills mac-contacts
git clone https://github.com/openclaw/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/openclaw/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/bdwelle/mac-contacts" ~/.claude/skills/openclaw-skills-mac-contacts && rm -rf "$T"
T=$(mktemp -d) && git clone --depth=1 https://github.com/openclaw/skills "$T" && mkdir -p ~/.openclaw/skills && cp -r "$T/skills/bdwelle/mac-contacts" ~/.openclaw/skills/openclaw-skills-mac-contacts && rm -rf "$T"
skills/bdwelle/mac-contacts/SKILL.mdmac-contacts
macOS Contacts CLI backed by
CNContactStore. All reads use unified contact
views (iCloud + local + Exchange merged). All writes are atomic via
CNSaveRequest. Group membership removal uses osascript to work around a
silent no-op in CNSaveRequest.removeMember_fromGroup_ for iCloud-backed
groups.
Dependencies
pip install pyobjc-framework-Contacts
pip install pyyaml
Grant Contacts access to Terminal (or your agent host) when prompted on first run, or via System Settings → Privacy & Security → Contacts.
Invocation
python3 skill://mac-contacts/scripts/mac-contacts.py <subcommand> [options]
All examples below use
mac-contacts as shorthand for the full invocation.
Subcommands
search
Search contacts. With a positional query, performs a single-pass search across name, organisation, note, email, phone (digits normalised), and postal address fields. Use explicit flags to restrict to a specific field.
search [QUERY] [--list LIST] [--email EMAIL] [--phone PHONE] [--city CITY] [--country COUNTRY]
| Flag | Description |
|---|---|
| Comprehensive search across all fields. Phone digits are matched fuzzily (query digits must appear in contact's digit-stripped number; minimum 4 digits required for phone matching). |
| Return only contacts that are members of the named list/group. |
| Match by email address (uses the CNContact native email predicate — efficient). |
| Match by phone number; non-digit characters stripped before comparison. Minimum 4 digits. |
| Match by city in any postal address. |
| Match by country in any postal address. |
Examples:
# Comprehensive — finds by name, org, email, phone, address mac-contacts search "John" mac-contacts search "john@example.com" # auto-matches email mac-contacts search "415-555" # auto-matches phone (≥4 digits) mac-contacts search "San Francisco" # auto-matches city # Explicit field targeting mac-contacts search --email "john@acme.com" mac-contacts search --phone "415" # error: fewer than 4 digits mac-contacts search --phone "4155551234" mac-contacts search --city "London" mac-contacts search --country "Germany" # Filter to list members mac-contacts search --list "Work"
Output per contact: name, organisation, phone(s), email(s), address(es).
show
Show every available field for a contact, including list membership.
show NAME
Output includes: full name (with prefix/middle/suffix), nickname, organisation, job title, department, phones, emails, postal addresses, URLs, social profiles, birthday, dates, note (if readable), and Lists.
mac-contacts show "Jane Doe" mac-contacts show "Apple" # matches any contact whose name contains "Apple"
Note: If multiple contacts match
, only the first result is shown.NAME
create
Create a new contact. All flags except
--first-name are optional.
create --first-name NAME [--last-name NAME] [--organization ORG] [--email EMAIL] (repeatable) [--phone PHONE] (repeatable) [--street STREET] [--city CITY] [--state STATE] [--zip ZIP] [--country COUNTRY] [--url URL] (repeatable) [--birthday DATE]
mac-contacts create --first-name "Jane" --last-name "Doe" \ --organization "Acme" \ --email "jane@acme.com" --email "jane.personal@gmail.com" \ --phone "415-555-0100" \ --street "123 Main St" --city "San Francisco" \ --state "CA" --zip "94102" --country "United States" \ --url "https://jane.acme.com" \ --birthday "1985-03-22"
Note: The
flag is intentionally absent. Writing contact notes requires the--noteentitlement, which Terminal-based tools do not hold.com.apple.developer.contacts.notes
format:--birthdayfor a full date (e.g.YYYY-MM-DD), or1985-03-22to store month/day without a year (e.g.--MM-DD).--03-22
update
Update fields on an existing contact. Phone and email values are appended to existing values (not replaced). A new postal address block is appended if any address flag is provided.
update NAME [--organization ORG] [--email EMAIL] (repeatable, appends) [--phone PHONE] (repeatable, appends) [--street STREET] [--city CITY] [--state STATE] [--zip ZIP] [--country COUNTRY] [--url URL] (repeatable, appends) [--birthday DATE] (replaces existing)
mac-contacts update "Jane Doe" --phone "415-555-0199" mac-contacts update "Jane Doe" --organization "New Corp" --city "Oakland" mac-contacts update "Jane Doe" --birthday "1985-03-22" mac-contacts update "Jane Doe" --url "https://jane.dev"
delete
Delete a contact. Prompts for confirmation unless
--force is given.
delete NAME [--force]
mac-contacts delete "Jane Doe" # prompts y/N mac-contacts delete "Jane Doe" --force # no prompt
add_to_list
Add a contact to a named list (CNGroup). Creates the list if it does not exist.
add_to_list NAME LIST
mac-contacts add_to_list "Jane Doe" "Work"
remove_from_list
Remove a contact from a named list.
remove_from_list NAME LIST
mac-contacts remove_from_list "Jane Doe" "Work"
Implementation note: Uses
(Contacts.app) becauseosascriptsilently no-ops for iCloud-backed groups.CNSaveRequest.removeMember_fromGroup_
list_groups
List all contact groups (lists) in the store.
list_groups
mac-contacts list_groups
Output conventions
,search
, andshow
emit YAML (requireslist_groups
).pyyaml
andsearch
return a YAML list;list_groups
returns a single YAML mapping. Parse withshow
orpython3 -c "import sys,yaml; print(yaml.safe_load(sys.stdin))"
.yq- Success messages begin with
.Success: - Error messages begin with
orError:
.[FATAL]
andsearch
exit with code 1 when no results are found.list_groups- All other commands exit 1 on any failure.
Known limitations
- Notes (write): Setting a contact note requires the
entitlement.com.apple.developer.contacts.notes
andcreate
do not exposeupdate
for this reason. Existing notes on contacts are readable via--note
.show
replaces nothing: Phone, email, and address values are always appended, never replaced. To change a value, delete and recreate the contact, or edit via Contacts.app.update
NAME matching: Usesshow
which matches substrings across name fields. If ambiguous, the first result is returned.CNContact.predicateForContactsMatchingName_