# Running Instar as a LaunchDaemon (macOS) By default, Instar installs as a **LaunchAgent** — it runs when you log in. That's fine for most setups, but has a limitation: **the agent only runs when you're logged in.** If the Mac reboots while you're away (OS update, power event, etc.) and your user doesn't auto-login, the agent stays offline until someone logs in. **LaunchDaemon mode** fixes this: the agent starts at boot, before any user login, and keeps running across all user sessions. This is useful for a dedicated "always-on" Mac that you access remotely. There are three gotchas when running as a daemon. This document covers all of them. ## 1. Authentication (Claude OAuth doesn't work in daemons) Claude Code's interactive mode reads OAuth credentials from the macOS login keychain. LaunchDaemons run without a user session, so they cannot access the user's login keychain — meaning OAuth-based auth silently fails. **Solution:** Use a long-lived OAuth token via `CLAUDE_CODE_OAUTH_TOKEN` instead of keychain-backed OAuth. ### Generate a long-lived token Requires a Claude subscription (Pro or Max). ```bash claude setup-token ``` This launches an interactive browser flow. When complete, it prints a token starting with `sk-ant-oat01-...` that's valid for one year. **Alternative:** Use an API key from [console.anthropic.com](https://console.anthropic.com/). That's pay-per-use rather than subscription-billed, but works identically. ### Configure Instar Add to `.instar/config.json`: ```json { "sessions": { "anthropicApiKey": "sk-ant-oat01-..." } } ``` Instar auto-detects the format: - `sk-ant-oat01-*` → routed to `CLAUDE_CODE_OAUTH_TOKEN` (subscription-billed, works in interactive mode) - `sk-ant-api03-*` → routed to `ANTHROPIC_API_KEY` (pay-per-use API billing) ## 2. iMessage database access The `~/Library/Messages/` directory is protected by macOS TCC. Granting Full Disk Access to the node binary would work but is overly broad — node could then access anything, and FDA has to be re-granted if Homebrew changes the node path. **Instar handles this automatically.** On startup, the iMessage adapter hardlinks `chat.db` (plus WAL and SHM files) from `~/Library/Messages/` to `/imessage/`. Hardlinks share the same inode, so the server reads the current data, but the link path itself isn't inside the TCC-protected directory — no FDA required on node. ### How it works - **First startup:** the adapter needs FDA to create the hardlinks. Run `instar server start` once from a terminal that has FDA granted (System Settings → Privacy & Security → Full Disk Access → add Terminal.app or iTerm). The hardlinks are created in `.instar/imessage/`. - **All subsequent startups:** the hardlinks already exist. The adapter reads through them without needing FDA on node. Works fine from a LaunchDaemon. - **If Messages.app resets** (new install, major macOS upgrade), the inode may change. The adapter detects this on startup and recreates the hardlinks — which again requires FDA for that one startup. ### Overriding the default If you want to point at a different database location (e.g., a shared volume), set `dbPath` explicitly: ```json { "type": "imessage", "enabled": true, "config": { "dbPath": "/path/to/your/chat.db", "authorizedContacts": ["+14081234567"] } } ``` When `dbPath` is set, the adapter uses it as-is and does not create hardlinks. ### Note on sending `imsg send` uses ScriptingBridge to talk to Messages.app, which requires a running user session with Automation permission granted. A daemon can send iMessages *if* a user is logged in when the daemon spawns `imsg`. If you need sending to work even before any user logs in, you'll need a LaunchAgent (not Daemon) for the sending path. In practice: most setups have someone log in eventually, and sending works thereafter. ## 3. iMessage photo attachments (optional) If your agent needs to receive and process **photo attachments** sent via iMessage (not just text), a second piece of infrastructure is required. The `chat.db` hardlink approach only covers message text — photo files live in `~/Library/Messages/Attachments/`, which is a separate TCC-protected directory. ### Why a dedicated binary Granting FDA to a general-purpose binary like `bash`, `node`, or `fswatch` works but is broader than necessary — any process using that binary gets the same access. The right approach is a **purpose-built binary** whose name makes the FDA grant self-documenting: `instar-attachments-sync`. ### What it does `instar-attachments-sync` is a small Go binary (~3MB) that: 1. On startup, hardlinks all existing image/video attachments from `~/Library/Messages/Attachments/` to `.instar/imessage/attachments/` 2. Watches for new files via FSEvents and hardlinks them within ~500ms of arrival 3. Prunes hardlinks whose source has been deleted Hardlinks share the same inode as the originals, so the agent can read them without any FDA grant of its own. ### Build and install ```bash # Requires Go (brew install go) cd scripts/attachments-sync go build -o ~/.instar/agents/AGENT/.instar/bin/instar-attachments-sync . ``` Or copy a pre-built binary from the [releases page](https://github.com/JKHeadley/instar/releases) *(coming soon)*. ### Grant Full Disk Access In **System Settings → Privacy & Security → Full Disk Access**, click `+` and add: ``` ~/.instar/agents/AGENT/.instar/bin/instar-attachments-sync ``` > **Important:** FDA is granted per resolved binary path. If you rebuild or replace the binary, you must re-grant FDA. Use the Cellar path or a stable location that won't change. ### LaunchAgent plist The attachments watcher should run as a **LaunchAgent** (not a Daemon) because it needs a user session context to access the Messages sandbox. Save to `~/Library/LaunchAgents/ai.instar.AttachmentsWatcher.plist`: ```xml Label ai.instar.AttachmentsWatcher Program /Users/YOU/.instar/agents/AGENT/.instar/bin/instar-attachments-sync RunAtLoad KeepAlive StandardOutPath /Users/YOU/.instar/agents/AGENT/.instar/logs/attachments-watcher.log StandardErrorPath /Users/YOU/.instar/agents/AGENT/.instar/logs/attachments-watcher.err EnvironmentVariables HOME /Users/YOU ThrottleInterval 5 ``` Load it: ```bash launchctl load ~/Library/LaunchAgents/ai.instar.AttachmentsWatcher.plist ``` ### Verify ```bash tail -f ~/.instar/agents/AGENT/.instar/logs/attachments-watcher.log ``` You should see: ``` 2026-01-01T00:00:00Z instar-attachments-sync starting 2026-01-01T00:00:00Z initial sync: linked N new files 2026-01-01T00:00:00Z watching /Users/YOU/Library/Messages/Attachments ``` If you see `operation not permitted`, FDA has not been granted or was granted to a different binary path. ## 4. Node binary path Instar symlinks `.instar/bin/node` to a node binary. If multiple Homebrew prefixes exist (e.g., `/opt/homebrew` and `/Users/you/homebrew`), the symlink may point to the wrong one. This matters if you've granted FDA to a specific binary — FDA is per-path. **Solution:** Point the symlink at the binary that has FDA (if any), using the resolved Cellar path (not the `bin/` symlink): ```bash ln -sf /Users/YOU/homebrew/Cellar/node/XX.Y.Z/bin/node /Users/YOU/.instar/agents/AGENT/.instar/bin/node ``` With the hardlink approach for chat.db, FDA isn't needed at all — this step can be skipped. ## LaunchDaemon plist template ```xml Label ai.instar.AGENT UserName YOUR_USER ProgramArguments /Users/YOU/homebrew/Cellar/node/XX.Y.Z/bin/node /Users/YOU/.instar/agents/AGENT/.instar/instar-boot.js server start --foreground --dir /Users/YOU/.instar/agents/AGENT WorkingDirectory /Users/YOU/.instar/agents/AGENT RunAtLoad KeepAlive StandardOutPath /Users/YOU/.instar/agents/AGENT/.instar/logs/server-launchd.log StandardErrorPath /Users/YOU/.instar/agents/AGENT/.instar/logs/server-launchd.err EnvironmentVariables HOME /Users/YOU PATH /Users/YOU/homebrew/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin ThrottleInterval 10 ``` Install: ```bash sudo cp my.plist /Library/LaunchDaemons/ai.instar.AGENT.plist sudo chown root:wheel /Library/LaunchDaemons/ai.instar.AGENT.plist sudo launchctl bootstrap system /Library/LaunchDaemons/ai.instar.AGENT.plist ``` Reload after changes: ```bash sudo launchctl bootout system /Library/LaunchDaemons/ai.instar.AGENT.plist sudo launchctl bootstrap system /Library/LaunchDaemons/ai.instar.AGENT.plist ``` ## Verifying the setup After the daemon starts: ```bash curl -s http://localhost:4040/health curl -s -H "Authorization: Bearer $(python3 -c "import json; print(json.load(open('/Users/YOU/.instar/agents/AGENT/.instar/config.json'))['authToken'])")" \ http://localhost:4040/imessage/status ``` Both should return healthy responses. The iMessage status should show `"state":"connected"` without any `"reason":"unable to open database file"` in the degradations log. Send a test iMessage to the agent's authorized number. You should see: 1. An ack message back within ~2 seconds (if `immediateAck` is configured) 2. A real reply from a Claude session within 30–90 seconds