imsg-permissions
Grant the imsg CLI its macOS privacy permissions (Full Disk Access, Automation → Messages, Contacts) and prove each one from every parent that runs imsg; on a SIP-off Mac, also deploy its IMCore bridg
SKILL.mdimsg-permissions/SKILL.md
GitHubimsg-permissions
imsg (openclaw/imsg, Homebrew) needs three macOS privacy grants; upstream lists them in
docs/permissions.md. How TCC
grants work, and the backup-first steps for a TCC.db edit, are in
macos-tcc.md. Read it before step 3.
The work centres on the grant table: one row per parent that runs imsg, one column per gate. A cell is ✅ only when a real command from that real parent passed.
| Parent | Full Disk Access | Automation → Messages | Contacts |
|---|---|---|---|
| SSH | |||
| Terminal.app, Ghostty, iTerm2 (each one installed) | |||
paseo daemon (charged to its LaunchAgent's first program, /bin/zsh, or node on hosts in macbook-provision's paseo_node_launch_hosts (mac-mini-m6); with SIP on, node under a zsh job is denied, personal-agent Q265–Q266) |
|||
| imsg as its own process (LaunchAgent) | |||
openroutine daemon (charged to ~/.cargo/bin/openroutine, its LaunchAgent's program; cargo install leaves it ad-hoc signed, so each rebuild drops the grant until coding-agent's devops/openroutine/resign.sh re-signs it with a self-made certificate, coding-agent Q710; its tcc-probe task checks daily) |
Scripts
probe.sh <own-address> [--keep-ssh]tests all three gates from the parent that runs it, and sends nothing.--keep-sshonly in the SSH row.as-imsg.sh [--tty] <imsg args…>runs imsg once as its own process (a one-shot gui LaunchAgent) and prints its output and exit code.--ttyputs a tty on stdin.
Steps
Fill the rows. Keep the terminal apps installed on this host. Find paseo's launch line with
plutil -extract ProgramArguments json -o - ~/Library/LaunchAgents/sh.paseo.daemon.plist; macOS charges the first program, even afterexec(personal-agent Q264). Done when each row names a real parent.Test every cell with
probe.sh(<own-address>is the user's iMessage address) from its parent:- SSH: run it in this shell with
--keep-ssh. - Terminal: a
.commandfile that runs it, viaopen -a Terminal <file>. - Ghostty:
open -na Ghostty --args -e <file>. - paseo:
launchctl submit -l <label> -o <log> --plus paseo's exact launch line, withexec paseo daemon runswapped forexec <probe.sh> <own-address>; thenlaunchctl remove <label>. - imsg itself:
as-imsg.sh chats --limit 1 --jsonandas-imsg.sh nickname --local --address <own-address> --json. Its Automation cell waits for step 5.
Write output to files and read them back. Done when every cell holds pass or fail with output.
- SSH: run it in this shell with
Grant each failing cell.
- Full Disk Access: System Settings +, adding the parent app, or for imsg itself the real
binary
/opt/homebrew/Cellar/imsg/<ver>/libexec/imsg(bin/imsgis a bash wrapper). - Automation: rerun the parent's probe. When "<parent> wants access to control Messages.app" shows, click Allow with System Events, as macos-tcc.md shows.
- Contacts: imsg asks only with a TTY on stdin and no
SSH_CONNECTION/SSH_CLIENT;chatsandhistorynever ask. For imsg itself, runas-imsg.sh --tty nickname --local --address <own-address> --json. A terminal app asks only when its own code asks.
When no prompt shows, read tccd's log (macos-tcc.md). Done when each cell passes, or tccd names why it cannot.
- Full Disk Access: System Settings +, adding the parent app, or for imsg itself the real
binary
Take each blocked cell to the user, with tccd's reason and the options: leave it, change the host, or a direct
TCC.dbedit. Do the edit only on the user's yes, by macos-tcc.md's steps.Test imsg's own Automation only on the user's yes. It needs a real send: ask for the recipient, then
as-imsg.sh send --to <address> --text "imsg permission test (ignore)"once, and click Allow on the prompt. Onmay_have_completed … retry is not safe, find the message before any retry (gotchas).Re-test the whole table. Remove test files, and quit apps you started. Done when every cell is ✅, or marked with its blocker and the user's choice.
Report the table, and log each grant and each
TCC.dbedit withlog-decisionsin the repo you work from.
IMCore bridge (SIP-off hosts)
Typing indicators, read receipts, polls and the other private sends of upstream docs/advanced-imcore.md need imsg's helper dylib injected into Messages.app. These are not privacy grants: finish the grant table first. mac-mini-m2 runs the bridge (Q86).
- Check the gates.
csrutil statussays disabled./opt/homebrew/opt/imsg/libexec/imsg-bridge-helper.dylibexists (Homebrew ships it).defaults read /Library/Preferences/com.apple.loginwindow autoLoginUsernames the Messages user, because the bridge agent starts only after login. mac-mini-m2 also hasamfi_get_out_of_my_way=1innvram boot-args, against macOS 26+ library validation. A gate that fails goes to the user: SIP and boot-args need Recovery and a reboot. - Inject.
IMSG_LAUNCH_READY_TIMEOUT=60 imsg launch --jsonrestarts Messages with the helper. - Keep it across reboots.
/opt/homebrew/opt/imsg/libexec/imsg launch --install-agent --jsonwrites~/Library/LaunchAgents/org.openclaw.imsg.bridge.plist, which re-injects every 300 s when the helper is missing or stale. Install it through theoptpath: the agent saves the path of the binary that ran, andbin/imsgis a wrapper that runs the versioned Cellar binary, whichbrew cleanupdeletes.
Done when imsg status --json has advanced_features, typing_indicators and read_receipts
true and helper_version equal to version, and launchctl print gui/$(id -u)/org.openclaw.imsg.bridge
shows last exit code = 0. Before you use a bridge command, check its selectors entry in that
JSON: on macOS 27 some are false (editMessage, deleteChat, leaveChat).
Gotchas
- SSH needs no Contacts grant. Over SSH, imsg reads the AddressBook SQLite files through
sshd's Full Disk Access. Judge Contacts by
contacts_unavailable;sourcesayslocal-addressbookeither way. openfrom SSH leaksSSH_*into the app, and imsg then takes the SSH path.probe.shunsets them.- Terminal.app never asks for Contacts. Its
Info.plisthas noNSContactsUsageDescription. Its Contacts cell needs aTCC.dbrow (macos-tcc.md). - "Policy disallows prompt." On mac-mini-m2 (SIP off) tccd refused every Contacts prompt, for
every parent. All four Contacts cells there came from
TCC.dbrows (personal-agent Q254, Q255). On every host,nodegets the same refusal: its hardened runtime lacks the Contacts entitlement, so the node row's Contacts cell needs aTCC.dbrow (m6 test, 2026-10-27). That row is possible only with SIP off. With SIP on, when paseo's program has Full Disk Access, a~/.local/bin/imsgwrapper ahead of/opt/homebrew/binon paseo's PATH that setsSSH_CLIENT(when unset) makes imsg read names from the AddressBook files. On m6 it gives node's imsg calls names; mark that cell ✅ (fallback) (personal-agent Q262). - A self-send can land in the phone-number thread. A send to the user's email address was
filed under their phone number, so imsg exited 1 with
may_have_completed … retry is not safe, though the message went out. Find it withimsg chats --limit 5(newestlast_message_at) andimsg history --chat-id <id>; send again only if it is missing (personal-agent Q256). - Upgrades drop path grants. Grants on
libexec/imsgand on mise'snodematch by path, sobrew upgrade imsgor a new node version needs this skill again. The bridge agent handles its own upgrade: a helper whose version differs from the CLI's is replaced at the next check.