"Not logged in" when the process tree starts at launchd, despite valid unexpired Keychain credentials (macOS)
Summary
On macOS, claude -p reports "Not logged in · Please run /login" whenever its process tree starts at launchd (e.g. spawned by a LaunchAgent app), even though the same binary with the same environment authenticates fine when the process tree starts at a terminal — and even though the Keychain credentials are present, unexpired, and readable from that same launchd context.
This breaks headless/automation use of a subscription-authenticated CLI from any macOS background app (LaunchAgents, cron-like schedulers).
Environment
- macOS 26.5.1 (Apple Silicon)
- Claude Code 2.1.207, native install (
~/.local/bin/claude) - Subscription (OAuth) auth; credentials in the login Keychain (
Claude Code-credentials), not expired (expiresAtin the future)
Evidence matrix
| Context | env | rc files | security find-generic-password -w -s "Claude Code-credentials" | claude -p |
|---|---|---|---|---|
| Terminal-descended, env -i HOME PATH, zsh -c (no rc) | minimal | none | exit 0 | works |
| launchd one-shot agent, zsh -lc | full login-shell env | .zprofile/.zshenv | exit 0 (silent, no prompt) | "Not logged in" |
Same binary, same $HOME, same Keychain item, no CLAUDE_*/ANTHROPIC_* env vars anywhere. The only variable is process ancestry. claude --debug -p prints nothing before the "Not logged in" line, so whatever check fails happens before debug logging starts.
Repro
<!-- /tmp/cc-test.plist -->
<?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">
<plist version="1.0"><dict>
<key>Label</key><string>cc-test</string>
<key>ProgramArguments</key><array>
<string>/bin/zsh</string><string>-lc</string>
<string>echo hi | claude -p "reply ok" > /tmp/cc-test.out 2>&1</string>
</array>
<key>RunAtLoad</key><true/>
</dict></plist>
launchctl bootstrap gui/$(id -u) /tmp/cc-test.plist
sleep 20; cat /tmp/cc-test.out # → "Not logged in · Please run /login"
launchctl bootout gui/$(id -u)/cc-test
Run the same zsh -lc 'echo hi | claude -p "reply ok"' from a terminal (even with env -i HOME=$HOME PATH=...): it works.
Workarounds found
CLAUDE_CODE_OAUTH_TOKENenv var works fine in the launchd context — both aclaude setup-tokentoken and the currentaccessTokenread from the Keychain item authenticate successfully. So the token itself is valid in that context; only the CLI's own credential lookup fails there.
Expected
claude should authenticate from launchd-descended processes the same way it does from terminal-descended ones (the Keychain item is demonstrably readable in that context).