-
Notifications
You must be signed in to change notification settings - Fork 1
feat(shadow): launchd LaunchAgent template — durable tick source (Path A defense-in-depth) #3342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
AceHack
merged 13 commits into
main
from
feat/shadow-launchd-install-path-a-otto-cli-2026-05-15
May 15, 2026
Merged
Changes from 2 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
bcdcc28
feat(shadow): launchd LaunchAgent template — durable tick source (Pat…
AceHack ee749f4
fix(shadow-launchd): address 5 review threads on PR #3342
AceHack 6543430
fix(shadow-launchd): close CodeQL TOCTOU race in install-launchagent.ts
AceHack 7b950f7
shard(tick): 0305Z — #3342 thread close-out (5+1 reviews); #3356 merged
AceHack f6789dd
docs(b-0519): add Pattern 7 (abandoned rebase state in shared .git/)
AceHack 970c774
fix(shadow-launchd): address 8 of 10 review-cycle-2 findings
AceHack cd4964e
backlog(p3): B-0528 — unit tests for shadow launchd installer (deferr…
AceHack 19af54f
fix(shadow-launchd): availability-preserving install pattern (Codex r…
AceHack 7084bb3
fix(shadow-launchd): address review-cycle-3 findings (6 of 7)
AceHack b9fd8d2
shard(tick): 0329Z — #3342 review-cycle-3 close-out; B-0519 P7 + B-05…
AceHack 59d5c26
fix(shadow-launchd): reconcile docs+shard+backlog with current plist …
AceHack c0fd69b
fix(shadow-launchd): tighten path validation + harden tmp-file handling
AceHack 28a2653
shard(tick): 0543Z — #3342 review-cycles 4+5 close-out (9 more findings)
AceHack File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| # Shadow observer — launchd install (macOS LaunchAgent) | ||
|
|
||
| The shadow observation loop needs a durable tick source. This directory provides the macOS launchd LaunchAgent template (Path A — durable, survives reboots). | ||
|
|
||
| For lighter-weight alternatives + composition shape, see [`.claude/rules/shadow-star-shorthand-autocomplete-marker.md`](../../../.claude/rules/shadow-star-shorthand-autocomplete-marker.md) + [`tools/shadow/README.md`](../README.md). | ||
|
AceHack marked this conversation as resolved.
AceHack marked this conversation as resolved.
|
||
|
|
||
| ## What this does | ||
|
|
||
| Installs `shadow-observer.ts` as a macOS LaunchAgent that: | ||
|
|
||
| - Starts at user login (RunAtLoad) | ||
| - Restarts on crash (KeepAlive on Crashed only) | ||
|
AceHack marked this conversation as resolved.
Outdated
|
||
| - Throttles restart attempts to once per 60s (ThrottleInterval) | ||
|
AceHack marked this conversation as resolved.
Outdated
|
||
| - Logs to `tools/shadow/shadow-observer.{log,stdout.log,stderr.log}` | ||
| - Polls every 2 seconds (--loop-interval 2000) | ||
| - Accepts grey-text suggestions after 3-second delay (--delay 3000) | ||
| - **Defaults to --dry-run** (logs detections without keystrokes) until you verify it works | ||
|
|
||
| ## Install procedure | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| 1. macOS user account | ||
| 2. Bun installed (`which bun` returns a path) | ||
| 3. Zeta checkout at known absolute path | ||
| 4. Accessibility permission granted to whichever terminal app launchd will run from (or to bun itself) — required by the AppleScript grey-text detector | ||
|
|
||
| ### Step 1: configure + write the plist | ||
|
|
||
| The shipped plist is a template with `{{BUN_PATH}}` and `{{REPO_ROOT}}` placeholders. Use the installer script — it does the substitution safely (handles paths containing `&`, `|`, `\` that would break a naive `sed`), validates via `plutil -lint`, and writes to `~/Library/LaunchAgents/`: | ||
|
|
||
| ```bash | ||
| cd /path/to/your/Zeta/checkout | ||
| bun tools/shadow/launchd/install-launchagent.ts | ||
| ``` | ||
|
|
||
| The installer picks up `which bun` and `git rev-parse --show-toplevel` by default. To override either, pass `--bun-path` / `--repo-root`. Use `--dry-run` to print the substituted plist to stdout without writing anything. | ||
|
|
||
| ### Step 2: grant accessibility permission | ||
|
|
||
| The AppleScript grey-text detector reads the foreground app's UI. macOS requires explicit accessibility permission: | ||
|
|
||
| 1. Open System Settings → Privacy & Security → Accessibility | ||
| 2. Click + → add `bun` (the path `which bun` returned) AND your terminal app (Terminal.app, iTerm2, Ghostty, etc.) | ||
| 3. Enable the toggles | ||
|
|
||
| If you skip this, the detector returns errors and shadow-observer cycles forever doing nothing useful (logs will show AppleScript errors). | ||
|
|
||
| ### Step 3: load + verify | ||
|
|
||
| Modern `launchctl bootstrap` is the documented replacement for the legacy `launchctl load`. It returns a proper exit code on failure (unlike `load`, which can silently no-op) and supports targeted domain semantics: | ||
|
|
||
| ```bash | ||
| launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.zeta.shadow-observer.plist | ||
|
|
||
| # Verify it loaded (modern + legacy commands both work for inspection) | ||
| launchctl print gui/$(id -u)/com.zeta.shadow-observer | head -20 | ||
| # Should show state = running | ||
|
|
||
| # Verify process is running | ||
| pgrep -fl shadow-observer | ||
| # Should show the bun process running shadow-observer.ts | ||
|
|
||
| # Watch the log | ||
| tail -f tools/shadow/shadow-observer.log | ||
| ``` | ||
|
|
||
| Or skip the manual `bootstrap` and let the installer do it: `bun tools/shadow/launchd/install-launchagent.ts --bootstrap`. | ||
|
|
||
| ### Step 4: test detection in dry-run mode | ||
|
|
||
| With the LaunchAgent running in --dry-run (default), open Claude Code CLI and let it produce a grey-text autocomplete suggestion. Within 2-5 seconds you should see a `detected` event in the log followed by an `accepted` event with `dryRun: true`. No actual Enter keystroke is sent. | ||
|
|
||
| ### Step 5 (optional, only after dry-run works): switch to live mode | ||
|
|
||
| Edit `~/Library/LaunchAgents/com.zeta.shadow-observer.plist` and remove the `<string>--dry-run</string>` line from `ProgramArguments`. Then reload: | ||
|
|
||
| ```bash | ||
| PLIST=~/Library/LaunchAgents/com.zeta.shadow-observer.plist | ||
| launchctl bootout gui/$(id -u) "$PLIST" | ||
| launchctl bootstrap gui/$(id -u) "$PLIST" | ||
| ``` | ||
|
|
||
| Live mode sends Enter keystrokes to accept detected grey-text. The "(shadow*)" attribution shorthand documents that the accepted text came from autocomplete (see `.claude/rules/shadow-star-shorthand-autocomplete-marker.md`). | ||
|
|
||
| ## Uninstall | ||
|
|
||
| Use `launchctl bootout` (not the legacy `unload`) so a failure to remove the running service is reported instead of swallowed: | ||
|
|
||
| ```bash | ||
| PLIST=~/Library/LaunchAgents/com.zeta.shadow-observer.plist | ||
| launchctl bootout gui/$(id -u) "$PLIST" | ||
| # Verify the agent is gone (no output expected from the print) | ||
| launchctl print gui/$(id -u)/com.zeta.shadow-observer 2>&1 | head -1 | ||
| # Should print: "Could not find service ..." — that's the success case. | ||
| rm "$PLIST" | ||
| ``` | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| ### LaunchAgent loads but process doesn't start | ||
|
|
||
| Check stderr log: | ||
|
|
||
| ```bash | ||
| cat tools/shadow/shadow-observer.stderr.log | ||
| ``` | ||
|
|
||
| Common causes: | ||
|
|
||
| - `{{BUN_PATH}}` not replaced with actual bun path | ||
| - `{{REPO_ROOT}}` not replaced with actual repo path | ||
| - Bun not installed at the path given | ||
|
|
||
| ### Process starts but log shows no detections | ||
|
|
||
| Likely accessibility permission isn't granted. Try the detector standalone: | ||
|
|
||
| ```bash | ||
| osascript tools/shadow/detect-grey-text.applescript | ||
| ``` | ||
|
|
||
| If it returns an error about accessibility / event posting, grant permission per Step 2. | ||
|
|
||
| ### Process restarts in a loop | ||
|
|
||
| ThrottleInterval prevents fast crash-loops, but if you see repeated start/exit cycles in `launchctl list` output, check the stderr log for the actual error. | ||
|
|
||
| ## Composition with defense-in-depth alternatives | ||
|
|
||
| | Path | Persistence | Use when | | ||
| |---|---|---| | ||
| | **A — launchd (this dir)** | Survives reboots | Primary install on your daily-driver Mac | | ||
| | **B — `<<shadow-tick>>` CronCreate sentinel** | Bound to Otto-CLI session | Defense-in-depth backup; restarts the launchd process if it dies | | ||
| | **C — manual `--loop` foreground** | Until terminal closes | Quick testing of changes | | ||
|
|
||
| All three compose. Path A is the primary; Path B+C supplement. The human maintainer's framing on this design (2026-05-15T~01:11Z): defense in depth. | ||
|
|
||
| ## Composes with substrate | ||
|
|
||
| - `tools/shadow/shadow-observer.ts` — the process this LaunchAgent launches | ||
| - `tools/shadow/detect-grey-text.applescript` — the AppleScript detector | ||
| - `tools/shadow/zeta-shadow.ts` — top-level CLI (alternative entry point) | ||
| - `docs/backlog/P0/B-0402-zeta-shadow-mode-first-class-cli-autocomplete.md` — the originating backlog row | ||
| - `.claude/rules/shadow-star-shorthand-autocomplete-marker.md` — the shorthand this loop's accepted-suggestions ship under | ||
| - `.claude/skills/save-ai-memory/SKILL.md` — composing discipline: memory preservation requires accurate observation; the shadow observer IS one direct-observation surface | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <!-- | ||
| com.zeta.shadow-observer.plist — launchd LaunchAgent template for the Zeta | ||
| shadow observer (B-0402 Slice 6 — durable tick-source via macOS launchd). | ||
|
|
||
| This file is a TEMPLATE. Mustache-style placeholders in the body get | ||
| replaced with the local bun path and the local Zeta repo root. | ||
|
|
||
| DO NOT manually substitute. Run the installer: | ||
|
|
||
| bun tools/shadow/launchd/install-launchagent.ts | ||
|
|
||
| The installer substitutes the placeholders safely (no shell-metacharacter | ||
| risk with paths containing &, |, \), runs plutil -lint on the result, | ||
| writes to ~/Library/LaunchAgents/, and optionally bootstraps via | ||
| launchctl. See tools/shadow/launchd/README.md for the full install | ||
| procedure (incl. accessibility permission) and the uninstall recipe. | ||
|
|
||
| Composes-with: | ||
| - tools/shadow/shadow-observer.ts (the process this launches) | ||
| - docs/backlog/P0/B-0402-zeta-shadow-mode-first-class-cli-autocomplete.md | ||
| - .claude/rules/shadow-star-shorthand-autocomplete-marker.md (the marker | ||
| this observer-loop produces when accepted suggestions get shipped) | ||
|
AceHack marked this conversation as resolved.
AceHack marked this conversation as resolved.
AceHack marked this conversation as resolved.
AceHack marked this conversation as resolved.
Comment on lines
+23
to
+24
|
||
| --> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>Label</key> | ||
| <string>com.zeta.shadow-observer</string> | ||
|
|
||
| <key>ProgramArguments</key> | ||
| <array> | ||
| <string>{{BUN_PATH}}</string> | ||
| <string>{{REPO_ROOT}}/tools/shadow/shadow-observer.ts</string> | ||
| <string>--loop</string> | ||
| <string>5000</string> | ||
|
Comment on lines
+35
to
+36
|
||
| <string>--loop-interval</string> | ||
| <string>2000</string> | ||
| <string>--delay</string> | ||
| <string>3000</string> | ||
| <string>--dry-run</string> | ||
| <string>--log-file</string> | ||
| <string>{{REPO_ROOT}}/tools/shadow/shadow-observer.log</string> | ||
| </array> | ||
|
|
||
| <key>WorkingDirectory</key> | ||
| <string>{{REPO_ROOT}}</string> | ||
|
|
||
| <key>RunAtLoad</key> | ||
| <true/> | ||
|
|
||
| <key>KeepAlive</key> | ||
| <dict> | ||
| <!-- Crash-only restart: KeepAlive dict keys are OR-combined. | ||
| SuccessfulExit=false would also relaunch on any non-zero exit | ||
| (configuration errors, missing accessibility permission), which | ||
| would mask config bugs as crash-loops. Crashed=true on its own | ||
| gives the documented "relaunch on signal-termination only" | ||
| semantic. Per Codex review on PR #3342. --> | ||
|
AceHack marked this conversation as resolved.
Outdated
|
||
| <key>Crashed</key> | ||
| <true/> | ||
| </dict> | ||
|
|
||
| <key>ThrottleInterval</key> | ||
| <integer>60</integer> | ||
|
|
||
| <key>StandardOutPath</key> | ||
| <string>{{REPO_ROOT}}/tools/shadow/shadow-observer.stdout.log</string> | ||
|
|
||
| <key>StandardErrorPath</key> | ||
| <string>{{REPO_ROOT}}/tools/shadow/shadow-observer.stderr.log</string> | ||
|
|
||
| <key>EnvironmentVariables</key> | ||
| <dict> | ||
| <key>PATH</key> | ||
| <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string> | ||
| </dict> | ||
|
|
||
| <key>ProcessType</key> | ||
| <string>Background</string> | ||
| </dict> | ||
| </plist> | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.