--print returns "Not logged in" when $USER is absent/wrong: keychain OAuth lookup keys on $USER with no uid fallback (macOS)
Summary
On macOS with subscription (OAuth) auth stored in the login Keychain, claude --print fails with a synthetic Not logged in · Please run /login envelope whenever the USER environment variable is absent or does not match the credential's Keychain account — even though the credential is valid and the same invocation succeeds the moment USER is set correctly. The CLI appears to use $USER as the Keychain account lookup key with no fallback to the process's real uid (getpwuid / os.userInfo()).
This silently breaks any spawn context that constructs a minimal child environment: launchd agents, cron jobs, CI runners, containers, and custom agent tooling that deliberately avoids spreading the parent env. The failure mode is particularly confusing because the JSON envelope reports is_error: true with api_error_status: null and the "Not logged in" text only appears in result — it looks like an auth outage, not a missing env var.
Environment
- Claude Code 2.1.238 (also reproduced across 2.1.23x)
- macOS 26.6.1 (Apple Silicon)
- Subscription (OAuth) login; credential present in the login Keychain as generic password service
Claude Code-credentials, account = the macOS username - Interactive
claudeworks normally in the same account
Reproduction
# Baseline — full env: succeeds
echo "Say OK" | claude --print --model sonnet --tools "" --output-format json \
--setting-sources "" --system-prompt "Reply OK"
# → {"is_error":false, ..., "result":"OK"}
# Minimal env (HOME + PATH only, no USER): fails
echo "Say OK" | env -i HOME="$HOME" PATH=/usr/bin:/bin \
claude --print --model sonnet --tools "" --output-format json \
--setting-sources "" --system-prompt "Reply OK"
# → {"is_error":true, "api_error_status":null, ..., "result":"Not logged in · Please run /login"}
# Same minimal env + correct USER: succeeds
echo "Say OK" | env -i HOME="$HOME" PATH=/usr/bin:/bin USER="$(id -un)" claude --print ...
# → {"is_error":false, ..., "result":"OK"}
# Same minimal env + wrong USER: fails
echo "Say OK" | env -i HOME="$HOME" PATH=/usr/bin:/bin USER=bogus claude --print ...
# → {"is_error":true, ..., "result":"Not logged in · Please run /login"}
The bisect shows USER is used as a lookup key (wrong value fails), not a presence check, and that nothing falls back to the real uid. The macOS Security framework itself does not consult $USER for login-keychain access, so the dependency is in the CLI's choice of account string.
Expected
Credential resolution should not depend on $USER. When it is absent (or arguably even when set), derive the account from the process uid (getpwuid(getuid()) / os.userInfo().username under Node), or store/look up the credential under a fixed account string.
Actual
$USER absent or mismatched → Not logged in · Please run /login as a synthetic result despite a valid Keychain credential.
Impact seen in the wild
A downstream agent framework that spawns hooks with a deliberately constructed minimal env (HOME+PATH) had every nested claude --print call fail this way for weeks — hundreds of silent failures that looked like API/auth errors (is_error=true api_error_status=null) until bisected to the missing USER variable.
One extra wrinkle for anyone hitting this from Bun-based tooling: Bun's os.userInfo().username currently mirrors $USER instead of calling getpwuid like Node (returns "unknown" when unset), so "just set USER from userInfo()" reintroduces the bug; /usr/bin/id -un is a reliable workaround source.