Sub-agent API 401 (auth expired) is reported to parent agent as "Request interrupted by user for tool use"
Summary
When a sub-agent (launched via the Agent tool with a subagent_type) makes an API call that returns HTTP 401 because the user's auth token has expired, Claude Code reports the failure to the parent agent as [Request interrupted by user for tool use]. The actual authentication_failed / 401 error is hidden, and the parent agent has no way to distinguish a real user cancellation from an upstream auth failure.
Environment
- Claude Code version:
2.1.191 - Platform: macOS (Apple Silicon, Homebrew install)
- Session ID (truncated for privacy):
eca7fe60-...
Evidence
The sub-agent's session JSONL transcript contains the real synthetic assistant message (sensitive ids redacted):
{
"type": "assistant",
"model": "<synthetic>",
"stop_reason": "stop_sequence",
"error": "authentication_failed",
"isApiErrorMessage": true,
"apiErrorStatus": 401,
"content": [{
"type": "text",
"text": "Please run /login · API Error: 401 The security token included in the request is expired"
}]
}
Yet the parent agent's transcript for the same moment shows only:
[Request interrupted by user for tool use]
So the harness saw a synthetic assistant message with isApiErrorMessage: true, apiErrorStatus: 401, and error: "authentication_failed", but rewrote that into a user-interrupt wrapper before handing it to the parent. The parent never sees the auth-error class.
Reproduction
- Launch a long-running sub-agent via the Agent tool (e.g. one that runs many Bash/Read/Edit cycles over 5+ minutes — long enough that an auth token can plausibly expire mid-run).
- Wait for the user's auth token to expire mid-run (or simulate by revoking it).
- The next API call the sub-agent attempts fails with HTTP 401 (
authentication_failed). - Observe: the parent agent sees
[Request interrupted by user for tool use]as the sub-agent's result and acts on it (typically: asks the user what to do, treats work as user-cancelled). - Expected: the parent should see an explicit auth-expired / API-error class (not a user-cancel wrapper), so it can retry after
/login, prompt the user to re-auth, or otherwise pick a correct recovery path.
Why this matters
- Long-running multi-agent workflows become unreliable. Orchestrator -> specialist sub-agent patterns (e.g. per-story TDD delivery cycles) cannot distinguish an auth blip from a deliberate user stop.
- Parent agents take the wrong recovery path. "User cancelled" -> ask the user for direction. "Auth expired" -> retry after
/login. These are not interchangeable, and conflating them wastes turns and confuses the user. - Users are blamed for cancellations they didn't perform. The phrasing "Request interrupted by user" tells the model (and any human reading the transcript) that the user hit Esc. They didn't.
- In-flight sub-agent work is silently dropped with no diagnostic trail. Committed work survives on disk; in-memory partial reports and uncommitted edits do not, and there is no signal to the parent that the loss happened for an auth reason rather than a deliberate one.
- Post-hoc diagnosis requires manual forensics on the local jsonl transcripts. This is not a workflow most users will know to perform.
Suggested fix
- Surface API errors (401, 429, 5xx) verbatim to the parent agent with HTTP status code and original error message intact. The sub-agent transcript already carries
apiErrorStatus,error(class), andisApiErrorMessage: true-- propagate these fields. - Use a distinct error class -- e.g.
auth_expired,rate_limited,api_error-- separate fromuser_interrupted. The current wrapper conflates the two failure modes into one string. - (Stretch) Offer to resume the sub-agent automatically once auth recovers, preserving its in-progress tool-call queue. At minimum, give the parent enough signal to choose between "ask user" and "retry on auth recovery."
Workaround
None. The parent agent has no way to distinguish a real user cancel from an upstream auth failure from the surfaced signal alone. Inspecting the local session jsonl can identify the cause after the fact, but that requires the user to know to look.
Related
- #12447 -- OAuth token expiration disrupts autonomous workflows (root condition: auth expires mid-run; this issue is about how that failure is reported to a parent agent, which #12447 doesn't cover).
- #64177 -- Workflow counts API-errored subagents as "completed" (adjacent mis-accounting of sub-agent API errors; different surface and different misclassification, but same shape: parent's view of sub-agent outcome doesn't match what actually happened).
- #62311 -- Distinguish an explicit user interrupt (Esc) from a message that merely arrived mid-turn (related but inverse: that issue is about distinguishing user-interrupt from queued-message; this issue is about distinguishing user-interrupt from auth-failure).
- #25818 -- Orchestrator has no diagnostic context when subagent fails (same family of "parent receives degraded failure info"; closed as stale).
4 Comments
This is a serious ergonomics problem in multi-agent workflows -- the parent agent losing diagnostic context on a 401 is worse than just a confusing message. It picks the wrong recovery strategy (asks the user "did you cancel?" when the real answer is "your token expired, run /login").
The error conflation is the most impactful part: auth_expired, rate_limited (429), and user_cancel are three completely different situations that all land as [Request interrupted by user for tool use] right now. Each needs a different recovery path.
Surfacing the apiErrorStatus to the parent would let orchestrators implement proper retry logic -- e.g. pause the fan-out, bubble a /login prompt to the top-level session, then resume. Without it, any multi-agent workflow that runs longer than a token TTL is fragile by design.
Update: same misclassification pattern observed in a different upstream cause — connection drops mid-response, not auth expiry.
In a long-running multi-agent session this week, the parent agent received
[Request interrupted by user for tool use]on a sub-agent dispatch. The session JSONL transcript shows the actual upstream signal was NOT a 401:Two independent instances in one session (~6 hours apart). The 401 case (which this issue filed) was also present once in the same session — three
<synthetic>events total: oneauthentication_failed/401plus twoserver_error/null.Both variants exhibit the same harness behaviour: a
<synthetic>model message withisApiErrorMessage: true, bubbled to the parent agent as[Request interrupted by user for tool use]— so a fix targeting just the 401 case would leave the connection-closed case unaddressed.Suggested scope expansion: any
<synthetic>isApiErrorMessageevent (regardless oferrorclass —authentication_failed,server_error, future variants) should propagate to the parent with the original error class and status preserved, not collapsed into the user-interrupt class.---
TL;DR — additive independent repro; please do NOT auto-close as duplicate. This
confirms and extends #70958; it is not a restatement of it, and it is distinct from
every near-neighbor:
pkill bun(an external process kill), andit's misfiled under a model-behavior title. Ours involves no pkill; the trigger
is an upstream 401/429 returned to a subagent mid-tool-use.
commands timeout on linux," an unrelated bug, then locked. This is subagent API
failure → fake user-interrupt, with transcript evidence.
state reconciliation. This is a different layer: the run genuinely dies because
agents never resolve and no terminal event is ever written.
New vs #70958 itself: (1) also fires on 429, not just 401; (2) hits
Workflow-tool subagents, not only Agent-tool ones; (3) the downstream effect
silently kills the entire workflow (journal frozen at
started:N/result:M, taskstuck "running", nothing surfaced).
---
Independent repro of this on Workflow-tool subagents, with two additions: the
trigger isn't only 401, and there's a downstream consequence that silently kills
whole workflows.
Environment: Claude Code (macOS, Apple Silicon). Observed across two unrelated
projects ("Definitely-Not-Prod" and "Held-Together-With-Duct-Tape").
Same mechanism, wider trigger. Subagents spawned inside a Workflow-tool run hit
upstream API failures mid-tool-use — both 401 (auth expired) and 429 (rate
limit) — and the failure is reported to the parent as
[Request interrupted by user for tool use]/ "The user doesn't want to proceedwith this tool use." No user acted. Raw signature counts in the affected agent
transcripts: run A = 27×401 / 6×429; run B = 49×401 / 67×429. Each dead agent's
final entry is a
tool_usewithstop_reason: tool_use, immediately followed by thebogus user-interrupt marker. So the fix shouldn't be scoped to auth (401) alone — any
upstream subagent failure (429 included) is being laundered into a fake user
cancellation.
Downstream consequence (new): the whole workflow silently dies. A workflow run's
journal.jsonlis an append-only heartbeat — one line per agent start, one per agentresult. When subagents are killed by this mislabeled interrupt, they never emit a
result, so the journal is left at
started:N / result:M(M<N) with no terminalaborted/error event. Nothing surfaces this:
/workflowsshows nothing, nonotification fires, and the task stays "running" indefinitely. Of 14 terminal runs
sampled, 2 died exactly this way (~14%) — intermittent, and correlated with periods
of upstream 401/429.
How to detect it from disk (no special tooling — the journal is enough). Match
agents on their
keyfield (a hash of prompt+opts, stable across resumes; rawstarted/result counts are not, since the journal is cumulative across resumes). A key
with a
startedand noresult, on a run whose journal has stopped being written,is a dead agent:
import json, os, time
def classify(run_dir, stall=300):
journal = os.path.join(run_dir, "journal.jsonl") # .../subagents/workflows/wf_*/
started, resolved = {}, set()
for line in open(journal):
line = line.strip()
if not line:
continue
try:
ev = json.loads(line)
except json.JSONDecodeError:
continue
key = ev.get("key") # prompt+opts hash; resume-safe
if ev.get("type") == "started" and key:
started[key] = ev.get("agentId")
elif ev.get("type") == "result" and key:
resolved.add(key)
unresolved = [a for k, a in started.items() if k not in resolved]
idle = time.time() - os.path.getmtime(journal) # age of newest heartbeat
if idle < stall: # still writing → alive (maybe mid-think)
return "ALIVE", unresolved
return ("SILENT_DEATH" if unresolved else "COMPLETE"), unresolved
Pointed at an affected run dir, this returns SILENT_DEATH plus the agentIds that
started but never resolved — the same ones whose transcripts carry the false
"user rejected" marker.
Asks:
5xx/API errors) into
[Request interrupted by user for tool use]— surface thereal status to the parent/workflow.
journal.jsonlinstead of leavingstarted:N / result:M./workflowsrather than leaving thetask "running".
Related: #6836 (meta tool_use/tool_result mismatch), #69856 (same workflow-journal
silent-death signature, misfiled under a model-behavior title).
Thanks @kristoffersingleton-yale for the careful differentiation from the neighboring reports; that precision is what keeps this thread from being folded into the wrong cluster. Your trigger matches what we observe on our fleet: an upstream auth failure returned to a subagent mid-tool-use, no external process kill, surfaced to the parent as a user interrupt. Your 429 flavor landing in the same wrapper strengthens the case that this message is a catch-all for subagent API failures rather than anything user-initiated.