"Not logged in" when the process tree starts at launchd, despite valid unexpired Keychain credentials (macOS)

Open 💬 0 comments Opened Jul 13, 2026 by ikhoon

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 (expiresAt in 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_TOKEN env var works fine in the launchd context — both a claude setup-token token and the current accessToken read 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).

View original on GitHub ↗