[RESOLVED - not a CLI bug] Orchestrator SIGTERMed its own agents every 300s; surfaced as aborted_tools with no logged cause

Status Fixed / completed
Reported on v2.1.212
Maintainer reply None cached
Activity 4 comments · opened Jul 29, 2026 · closed Jul 29, 2026

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 interruptpermission_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 medium completed normally (428s), and again at --effort max (553s)
  • Not the account/quotarate_limit_event reports status: allowed on 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_out is 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.

View original on GitHub ↗

4 Comments

oashaikh · 1 month ago

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 gates Bash. In -p (non-interactive) mode there is nobody to answer the prompt, so any Bash command not covered by an explicit permissions.allow rule is denied — and a denial mid-turn ends the run.

My host had permissions.defaultMode: dontAsk in settings, which would have permitted these commands, but the explicit --permission-mode flag 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_result says "The user doesn't want to proceed with this tool use" — literally true, but reads like a user interrupt
  • followed by [Request interrupted by user for tool use]
  • and the turn ends subtype: error_during_execution, terminal_reason: aborted_tools
  • permission_denials is [], which actively pointed me away from permissions
  • exit code 143 looks like a signal death

Concretely: git status … and grep -n … succeeded (matching Bash(git *) / Bash(grep *) allow rules), then cd infra/images && python3 test_workspace_agent.py matched 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:

  1. Populate permission_denials when a tool is denied. It was [] on every one of these runs, which is what convinced me permissions weren't involved.
  2. Distinguish "denied" from "interrupted" in the terminal reason — e.g. terminal_reason: permission_denied — and/or make the tool_result text say the tool was denied by policy rather than that the user declined. In -p mode 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.

oashaikh · 1 month ago

Second correction — my previous "root cause" was also wrong

The comment above blamed --permission-mode acceptEdits gating 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_result whenever 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 -p mode session with --input-format stream-json ends 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:

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"]
permission_denials: []

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 max completed 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_result text "The user doesn't want to proceed with this tool use" is actively misleading in -p mode, 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-file privately.

oashaikh · 1 month ago

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.

oashaikh · 1 month ago

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:

  1. The agent only ever received SIGTERM. It ran its handler and shut down in an orderly way — MCP servers SIGINTed, LSP down, session snapshot cleaned — so from inside the CLI nothing had gone wrong and nothing was logged as wrong. --debug-file showed a clean teardown with no stated reason, which is indistinguishable from an external kill.
  2. terminal_reason: aborted_tools plus a tool_result of "The user doesn't want to proceed with this tool use" reads unmistakably as a user/permission decision. There was no user, and permission_denials was []. I wasted hours on permission modes because of that wording.
  3. [ede_diagnostic] result_type=user last_content_type=n/a stop_reason=tool_use gave me nothing actionable.

Two suggestions, both small and neither a bug:

  • Log the reason a session ends, even one line (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.
  • Distinguish denied from cancelled in the tool_result text. In -p mode 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.