Headless service account cannot run `claude -p`: silent exit 0 when the OS user has no onboarding state, despite `auth status` reporting `loggedIn: true`
Version: 2.1.271 (also reproduced on 2.1.152) · Linux x64 · npm global install
Summary
Under a systemd service user, claude -p exits 0 after ~475 ms with empty stdout and stderr, and never reaches the API. claude auth status reports loggedIn: true, authMethod: api_key_helper, apiKeySource: apiKeyHelper for the same user — the apiKeyHelper resolves correctly. --bare does not change this.
The only property that correlates with success is whether the invoking OS user has previously completed interactive onboarding (hasCompletedOnboarding + oauthAccount in $HOME/.claude.json).
Evidence
Identical HOME, cwd, PATH, settings file and credential file for every row; only the invoking OS user differs. The credential is the same deliberately-invalid placeholder in all rows, so a reaching run is expected to produce 401s.
| user | uid class | login shell | onboarding state in $HOME/.claude.json | apiKeyHelper invoked |
|---|---|---|---|---|
| A | 0 | bash | present | 7× |
| B | regular (>1000) | bash | present | 7× |
| C | regular (=1000) | bash | absent | 0 |
| D | system (<1000) | nologin | no profile | 0 |
| E | system (<1000) | nologin | no profile | 0 |
uid, login shell and system-account status do not correlate. Onboarding state correlates without exception.
Rows C and D/E together rule out the obvious explanations: C is a regular user with /bin/bash and still fails, so neither uid range nor nologin is the cause.
Diagnostics
--debug-file on a failing run stops at 58 lines, last entry:
[DEBUG] Git remote URL: null
[DEBUG] No git remote URL found
A succeeding run continues past that point to 190 lines:
[DEBUG] No git remote URL found
[DEBUG] Ripgrep first use test: PASSED (mode=embedded, path=.../claude.exe)
...
[DEBUG] [API REQUEST] /v1/messages x-client-request-id=... source=sdk
[ERROR] API error (attempt 6/11): 401 {"type":"error","error":{"type":"authentication_error","message":"API key is invalid."}}
strace -f: 3 execve calls on the failing run versus 52 on the succeeding one. The failing run ends in a voluntary exit_group(0) — no failed syscall, no crash, no signal.
An instrumented apiKeyHelper (appending a line per invocation) confirms it is never called on the failing run, while claude auth status for that same user still reports loggedIn: true.
Reproduction
- Create an OS user that has never run
claudeinteractively. - Provide credentials via
apiKeyHelperin a settings file:
``json``
{ "apiKeyHelper": "/path/to/helper.sh" }
- Run as that user, with a writable
HOMEand a cwd the user can read:
``bash``
claude --bare --settings /path/to/settings.json -p "ping"
- Observed: exit 0, no stdout, no stderr, helper never invoked.
Expected: either an API call, or a clear error explaining what is missing.
Questions
- Is there a supported way to run
claude -punder a non-interactive service account that has never completed interactive onboarding? - If not, could the onboarding gate be bypassed when a valid
apiKeyHelper/ANTHROPIC_API_KEYcredential is present — particularly in--baremode, which already documents that it "never reads OAuth credentials or the system keychain"? - Independently of the above: could this path fail loudly? The silent exit 0 with no stdout, stderr or debug-log entry is indistinguishable from a successful empty result for any calling process. A caller that treats empty output as an error will burn its full retry budget without ever learning the cause.
- Is pre-writing
hasCompletedOnboardingin~/.claude.jsonsupported for provisioning, or explicitly discouraged? The docs describe that file as state "Claude Code writes for itself", so we have deliberately not done it.
Context
We run Claude Code from a systemd service to continue paused agent runs after a human approval. The service account is nologin by design and is provisioned with credentials via systemd LoadCredential + apiKeyHelper, so that no key value appears in the process environment. Everything in that chain verifies; only the onboarding precondition blocks the run.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗