[RESOLVED - not a CLI bug] Orchestrator SIGTERMed its own agents every 300s; surfaced as aborted_tools with no logged cause
Summary
In non-interactive -p mode with --input-format stream-json, turns abort mid-flight at ~300s of process lifetime. The CLI begins a graceful shutdown while a tool call is still running, which fails that tool and ends the turn with:
"subtype": "error_during_execution"
"terminal_reason": "aborted_tools" (or "aborted_streaming")
"errors": ["[ede_diagnostic] result_type=user last_content_type=n/a stop_reason=tool_use"]
Exit code is 143. The transcript's tool_result reads "The user doesn't want to proceed with this tool use" and "[Request interrupted by user for tool use]", but no user or parent process sent an interrupt — permission_denials is empty.
Environment
- Claude Code 2.1.212 (Homebrew cask), macOS 15 / arm64
- Invoked as:
claude -p --output-format stream-json --verbose --input-format stream-json --model sonnet --effort <level> --permission-mode acceptEdits --allowedTools "Bash Edit Write Read Glob Grep TodoWrite" --json-schema <schema> - stdin is held open for the session (messages streamed in), stdout consumed as stream-json
- Several MCP servers configured (stdio + remote)
Frequency, and the strongest correlation
Across 5 days of automated runs on one machine (same binary, same host, same credentials):
| effort | aborts | total | rate |
|--------|--------|-------|------|
| max | 19 | 45 | 42% |
| high | 76 | 192 | 40% |
| medium | 7 | 126 | 6% |
Controlling for workload, the effect only appears on the heavier of two repositories:
| repo | effort | aborts | total | rate |
|------|--------|--------|-------|------|
| small | max | 0 | 7 | 0% |
| small | high | 0 | 9 | 0% |
| large | max | 19 | 38 | 50% |
| large | high | 76 | 183 | 42% |
| large | medium| 7 | 121 | 6% |
So it appears to need high/max effort combined with a large workload (long tool calls, ~600k cached input tokens, 265-file test suite). Neither alone reproduces.
What the CLI's own --debug-file shows
Abbreviated from a failing run (process start 17:19:36, shutdown 17:24:38 — 301.9s):
17:24:24.594 Stream started - received first chunk
17:24:26.122 [Stall] tool_dispatch_start tool=Bash permissionDecisionMs=7
17:24:26.123 Spawning shell without login (-l flag skipped)
17:24:38.186 MCP server "<remote>": PROXY connection closed after 298s (cleanly)
17:24:38.228 MCP server "<stdio-a>": Sending SIGINT to MCP server process
17:24:38.279 MCP server "<stdio-b>": Sending SIGINT to MCP server process
17:24:38.354 LSP server manager shut down successfully
17:24:38.429 Cleaned up session snapshot
17:24:38.640 [Stall] tool_dispatch_end tool=Bash outcome=error durationMs=12518
17:24:38.640 Bash tool error (12518ms): Shell command failed
The debug log records no interrupt received, no stdin EOF, and no error before the shutdown — the reason the session ends is not captured anywhere. The Bash failure is a consequence of the teardown, not its cause (that tool was 12.5s into a command that takes ~25s standalone).
Note the remote MCP proxy closing "after 298s" immediately before teardown; unclear whether cause or coincidence.
Ruled out (each by measurement, not assumption)
- Not a wall-clock cap — an equivalent single 420s tool call at
--effort mediumcompleted normally (428s), and again at--effort max(553s) - Not the account/quota —
rate_limit_eventreportsstatus: allowedon aborted and successful runs alike; aborts don't cluster by position in the 5h window (21/21/30/8/8% by hour) - Not a CLI version change — binary unchanged for 13 days before onset
- Not OS signals — macOS unified log shows no signal/jetsam for those PIDs
- Not host pressure — 32G host, 43% memory free; aborts occur at load 5.8 as well as 18
- Not the parent process — the supervising process sends no interrupt (verified by instrumenting its own control channel), and its timeout/idle paths never fire (
timed_outis false) - Not stdin lifecycle — holding stdin open for 900s with a long tool completed fine
- Not the working directory, env allowlist, credentials, shell pipe, or build cache — each tested in isolation
What would help
The CLI knows why it decided to end the session; the debug log doesn't say. Even a single line at that decision point (session ending: <reason>) would make this diagnosable by users. Happy to supply a full unredacted --debug-file privately — it contains credentials and paths so I have not attached it here.
I do not have a minimal reproduction: every synthetic repro I built completed successfully. The signal is statistical (~40% of high/max-effort turns on a large workload) plus the debug trace above.
4 Comments
Correction from the reporter — this is my configuration error, not a CLI bug
I have found the actual cause and it is not what this issue describes. Apologies for the noise; leaving the full correction here in case anyone hits the same symptom and searches for it.
Root cause: I invoked the CLI with
--permission-mode acceptEdits. That auto-approves file edits but still gatesBash. In-p(non-interactive) mode there is nobody to answer the prompt, so any Bash command not covered by an explicitpermissions.allowrule is denied — and a denial mid-turn ends the run.My host had
permissions.defaultMode: dontAskin settings, which would have permitted these commands, but the explicit--permission-modeflag overrides it. So the CLI behaved exactly as configured.The symptom is confusing because the transcript reports it as an abort rather than a denial:
tool_resultsays "The user doesn't want to proceed with this tool use" — literally true, but reads like a user interrupt[Request interrupted by user for tool use]subtype: error_during_execution,terminal_reason: aborted_toolspermission_denialsis[], which actively pointed me away from permissionsConcretely:
git status …andgrep -n …succeeded (matchingBash(git *)/Bash(grep *)allow rules), thencd infra/images && python3 test_workspace_agent.pymatched nothing, was denied, and killed the turn.Why my correlations were wrong. The ~300s clustering and the effort/abort tables in the report above are real numbers but not causal. Denials fire whenever the agent happens to reach for a command outside the allowlist, so duration varies (I have examples at 25s, 230s and 300s). Higher effort correlated only because more reasoning means more varied commands. My 18 synthetic reproductions all passed because they used commands that happened to match existing allow rules.
Two suggestions that would have saved me a day, if they're of any interest:
permission_denialswhen a tool is denied. It was[]on every one of these runs, which is what convinced me permissions weren't involved.terminal_reason: permission_denied— and/or make thetool_resulttext say the tool was denied by policy rather than that the user declined. In-pmode there is no user, so "the user doesn't want to proceed" is misleading.Neither is a bug; both are diagnosability gaps around an already-correct behaviour. Closing, since the defect was mine.
Second correction — my previous "root cause" was also wrong
The comment above blamed
--permission-mode acceptEditsgating Bash. That was wrong too. I re-ran with--permission-mode bypassPermissions(confirmed present in argv) and the aborts continue unchanged, including runs at 301s and 304s that still contain the"The user doesn't want to proceed with this tool use"tool_result.So that string is not a permission denial. It appears to be the generic text written into a
tool_resultwhenever an in-flight tool is cancelled — for any reason, including the session terminating underneath it.permission_denials: []was accurate the whole time; I overrode that direct evidence because the message text read like a denial.My "51 of 76 aborts carried a denial" statistic was circular: I was counting the abort's own symptom as if it were the cause.
What actually stands, stripped of theories
A
-pmode session with--input-format stream-jsonends at ~300s of process lifetime while a tool is still running. From--debug-file, at that moment the CLI performs an orderly teardown — SIGINT to MCP servers, LSP shutdown, session-snapshot cleanup — having logged no interrupt received, no stdin EOF, and no error. The in-flight Bash tool then fails, and the turn reports:Exit code 143.
Excluded by direct measurement (each tested, not assumed): account/quota state, CLI version, model, effort level, permission mode, wall-clock duration (a 553s single-tool run at
--effort maxcompleted cleanly), working directory, environment allowlist, credentials, shell pipes, build caches, OS signals (nothing in the macOS unified log), host memory and load, stdin lifecycle (held open 900s, fine), turn count, and context size. Also excluded: the supervising process, which sends no interrupt and whose own timeout paths never fire.I have no reproduction. Every synthetic case I built completed successfully, which is itself a clue — the failures only occur inside real multi-turn sessions doing varied work.
The one concrete ask
The CLI knows why it ended that session; the debug log does not say. A single line at that decision point —
session ending: <reason>— would make this diagnosable. Right now an orderly shutdown with no stated cause is indistinguishable from an external kill, which is what sent me down five wrong paths in a day.Secondary: the
tool_resulttext "The user doesn't want to proceed with this tool use" is actively misleading in-pmode, where there is no user. If it also covers cancellation-by-teardown, wording that distinguishes denied from cancelled would help.Happy to provide a full unredacted
--debug-fileprivately.Duration histogram — the ~300s boundary is sharp
Adding this because it is the most concrete signal I have. All exit-143 aborts over one 20-hour period on one machine:
| duration | count |
|----------|-------|
| < 1 min | 5 |
| 1–3 min | 6 |
| 3–4.7 min | 9 |
| 4.7–5.2 min | 64 |
| > 5.2 min | 1 (319s) |
75% land in a 30-second window around 300s, and essentially nothing survives past ~310s. That looks like a hard boundary rather than a random failure.
Yet it is not a universal cap: two synthetic single-tool-call sessions I ran on the same binary and host went 428s and 553s to clean completion. So whatever the ~300s boundary applies to, it is a property of these real multi-turn sessions and not of the process lifetime as such.
The ~20 sub-300s aborts may well be a second, unrelated mode; I am not claiming they share a cause.
Environment recap:
claude -p --output-format stream-json --verbose --input-format stream-json --model claude-sonnet-5 --effort high --permission-mode bypassPermissions --allowedTools "…" --json-schema …, stdin held open, several MCP servers configured (2 stdio, several remote), macOS 15 arm64, CLI 2.1.212.Resolved — this was my bug, not a CLI defect. Closing.
Root cause found and fixed. Nothing here needs action from Anthropic; I'm leaving the detail because the symptom is genuinely misleading and someone else will search for it.
What was actually happening. My supervising process reconciles state on a 300-second timer. One branch handles "this PR was closed on GitHub while we still own a live pipeline thread" by notifying an operator and terminating the owning process group. The notification was deduplicated; the terminate was not. So the notification fired once (24 July) and looked handled, while every subsequent tick re-sent SIGTERM to whatever agent that job was running — for five days.
The discriminator, once I finally grouped by ticket across ~400 runs:
| job | had an externally-closed PR | abort rate |
|-----|------|------|
| A | yes (PR #103) | 42/46 = 91% |
| B | yes (PR #102) | 73/87 = 84% |
| 16 others | no | 0-9% |
Fixed by gating the terminate on first observation. A job that previously died at 291s now runs 347s to
exit 0, and exit-143 aborts have stopped entirely.Why I misread it as a CLI problem for a day — this is the part that may be worth something to you:
--debug-fileshowed a clean teardown with no stated reason, which is indistinguishable from an external kill.terminal_reason: aborted_toolsplus atool_resultof "The user doesn't want to proceed with this tool use" reads unmistakably as a user/permission decision. There was no user, andpermission_denialswas[]. I wasted hours on permission modes because of that wording.[ede_diagnostic] result_type=user last_content_type=n/a stop_reason=tool_usegave me nothing actionable.Two suggestions, both small and neither a bug:
session ending: <cause>, including "signal received: SIGTERM"). A graceful teardown with no cause recorded is the single thing that made an external kill undiagnosable from the CLI side.-pmode there is no user, so "the user doesn't want to proceed" is actively misleading when the real cause is an in-flight tool cancelled by teardown.Sorry for the noise, and thanks for
--debug-file— the teardown sequence it showed is what eventually told me the process was being signalled rather than failing.