Backgrounded Bash task reported exit code 0 for a command that failed (false-green completion notification)
Summary
A backgrounded Bash-tool task's reported exit code can disagree with the process's real exit status in the false-green direction: a test gate that failed was reported to the agent as [exited with code 0], and the completion notification said "completed (exit code 0)".
Concrete incident
2026-09-12, an agent session running a repo's test gate:
- A backgrounded
npm test(jest) finished with 3 failing suites — jest's own summary printedTest Suites: 3 failed, …. - The harness's task completion notification reported exit code 0.
- The agent caught it only because it read jest's own summary line out of the captured output instead of trusting the reported code. Had it trusted the notification, it would have committed the broken suites.
This is the mirror image of a false-RED shape we had already documented (a backgrounded dev server whose captured log read empty and whose task reported a nonzero exit while the server was healthy and serving). Together they suggest exit-status plumbing for backgrounded tasks can lose fidelity in both directions — and the false-green direction is the dangerous one for any agent using backgrounded commands as CI-style gates: a false red ends with a wasted re-run, a false green ends with a commit.
What we could and could not reproduce
The incident notes proposed two candidate mechanisms, which predict different things:
- The harness reports the exit status of the backgrounding wrapper rather than of the command. This would reproduce with a trivially-failing short command started in the background.
- The command's exit status is lost when a long-running foreground task is auto-migrated to background (the ~600 s foreground cap — the incident's
npm testruns ~9 minutes, so it takes this path by default, without the agent opting in).
We tested candidate 1 on Claude Code 2.1.236 / Linux: a command started with run_in_background that sleeps 5 s, prints a success-shaped summary (Tests: 3 failed, 10 passed, 13 total), and exits 57 was reported correctly as failed with exit code 57. So plain background exit reporting is fine there, which points at candidate 2 (the foreground→background migration path) and/or a platform difference — the incident occurred on a Windows (Git Bash) host.
Suggested discriminating repro for the migration path: a foreground command that prints a summary and exits non-zero only after exceeding the foreground cap (so the harness migrates it), e.g.:
node -e "setTimeout(() => { console.log('Tests: 3 failed, 10 total'); process.exit(57); }, 630000)"
then compare the task notification's reported exit code against 57.
Expected
The reported exit code of a backgrounded (or background-migrated) task matches the process's real exit status — or, if the status genuinely cannot be known (e.g. lost across a migration), the notification says so explicitly rather than reporting 0.
Environment
- Incident: Claude Code agent harness, Windows host (Git Bash), long-running jest gate auto-moved to background; exact CC version at the time not recorded (2026-09-12).
- Candidate-1 ruled out on: Claude Code 2.1.236, Linux.
Workarounds we carry
- A repo rule stating the backgrounded exit code is untrustworthy in both directions — read the runner's own summary line (jest's
Tests:/Test Suites:lines, TAP's# fail N) instead of the status. - A gate-capture helper that writes an explicit
=== GATE VERDICT: … EXIT:n ===line into the captured output, so the verdict travels with the log and the harness cannot lose it.