Skip to content

feat(oauth): persist CLAUDE_CODE_OAUTH_TOKEN across macOS reboots#277

Open
Ballestar wants to merge 3 commits into
kunchenguid:mainfrom
Ballestar:fm/oauth-token-durable-z4
Open

feat(oauth): persist CLAUDE_CODE_OAUTH_TOKEN across macOS reboots#277
Ballestar wants to merge 3 commits into
kunchenguid:mainfrom
Ballestar:fm/oauth-token-durable-z4

Conversation

@Ballestar

Copy link
Copy Markdown

What Changed

  • Add bin/fm-oauth-token-install.sh and bin/fm-oauth-token-load.sh plus a com.firstmate.oauth-token.plist launchd agent so CLAUDE_CODE_OAUTH_TOKEN is loaded from a configured source (file, env, or 1Password op:// reference) and persists across macOS reboots.
  • Resolve token sources by marker prefix, with 1Password references passed to op read using the full op:// scheme to avoid colliding with a bare op: prefix, and warn on insecure token-file permissions.
  • Document the feature in docs/oauth-token.md, register the new scripts in docs/scripts.md, note the env-var overrides in docs/configuration.md, and cover the behavior with tests/fm-oauth-token.test.sh.

Risk Assessment

✅ Low: Well-bounded, macOS-scoped, thoroughly tested additive change; the one prior error (op:// scheme collision) is fixed and now covered by an assertion, and no new material issues surfaced.

Testing

Completed 1 recorded test check.

Pipeline

Updates from git push no-mistakes

⏭️ **intent** - skipped

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • 🚨 bin/fm-oauth-token-load.sh:88 - The op: source path strips the descriptor's op: prefix (ref=${src#op:}), but docs/oauth-token.md tells users to write the full 1Password reference op://Vault/Item/field. Stripping op: from op://Private/Item/field yields //Private/Item/field, and op read "//Private/Item/field" fails because op read requires the op:// scheme. The op: marker convention collides with 1Password's own op:// URI scheme, so the documented op-source setup is broken. The test (test_op_source_uses_op_cli) doesn't catch it because its op stub ignores the reference argument and returns a fixed value. Fix: detect op:// and pass the whole descriptor to op read unchanged (only strip a bare op: prefix when it isn't already op://).
  • ℹ️ bin/fm-oauth-token-load.sh:68 - The token-file permission check uses stat -f '%Lp' (BSD) with a stat -c '%a' (GNU) fallback. This is correct on macOS (the only supported platform, gated by launchctl) but on Linux CI stat -f means --file-system and produces spurious output/exit behavior, so the perms warning path can misbehave there. Not user-facing on the supported platform; noting only because the test suite exercises this path.

🔧 Fix: fix op:// scheme collision in oauth token source resolution
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Ballestar added 3 commits July 5, 2026 15:05
Add a per-user LaunchAgent that runs `launchctl setenv
CLAUDE_CODE_OAUTH_TOKEN <value>` at login, so firstmate-launched agents
inherit the token across reboots without a manual re-export.

Chose the LaunchAgent + launchctl setenv approach over env-passthrough
in a daemon plist: firstmate runs interactively in a terminal/tmux
session, not as a launchd-managed daemon, so there is no existing
firstmate plist to inject EnvironmentVariables into. launchctl setenv
at login matches the actual process tree (GUI terminal -> login shell
-> tmux server -> crewmate agents) without re-architecting how
firstmate launches.

The token is never committed. The plist template
(bin/launchd/com.firstmate.oauth-token.plist) only references the
helper by placeholder; bin/fm-oauth-token-load.sh reads the token at
run time from a gitignored secure source (a 0600 file, a
config/oauth-token-source cmd:/op: reference, or FM_OAUTH_TOKEN_FILE).
bin/fm-oauth-token-install.sh renders and loads the plist idempotently
and supports --uninstall/--status.

docs/oauth-token.md covers setup, rotation, security notes, and the
design choice; docs/configuration.md, docs/scripts.md, README.md, and
CONTRIBUTING.md cross-reference it. tests/fm-oauth-token.test.sh
covers helper modes, source resolution, error paths, plist shape, and
installer idempotency hermetically (stubbed launchctl/tmux).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant