Anyone who has run Claude Code subagents seriously has hit the same wall: the agent is "running" according to the UI, but nothing is coming back. No logs, no errors, no progress — just a cursor blinking while you wonder whether to hit Ctrl+C. I have reflexively killed dozens of them before realising that the first thing to do is not to kill, but to classify.
Subagents are convenient, but they turn into black boxes the moment something unexpected happens inside them. Below is the diagnostic flow I settled on after getting stuck more times than I want to admit, grouped by the symptom you're actually seeing.
Classify the symptom — three questions first
Staring at logs without a hypothesis burns time. Start by answering these three questions explicitly — the answers narrow the root cause far more than any single log line will.
- Did the subagent terminate quickly, or is it still running?
- Is there any output at all, or is the channel completely silent?
- Did the parent agent receive an error, or did the call end silently?
Cross these three axes and the root cause narrows fast:
- Terminated × no output × error raised → prompt issue or missing tool permission
- Long-running × no output × no error → tool-call loop or context saturation
- Terminated × output present × no error → prompt interpretation drifted
- Long-running × output present × no error → streaming not being drained on the parent side
The classifier is not a theoretical exercise. Each combination points at a completely different fix, so the time you spend on a one-minute mental sort pays back multiples of itself in debugging time.
Preserve state before you debug
Before you poke anything, capture the current session state. Recovering first and then saying "it doesn't reproduce" is how root causes get lost. This is the single discipline that has saved me the most hours.
# Tail the session log while the run continues
tail -f ~/.claude/logs/session-$(date +%Y%m%d)*.log > /tmp/cc-debug.log &
# Identify any running subagent processes
ps aux | grep -E "claude-(agent|code)" | grep -v grep
# Export the current parent conversation for later inspection
claude --export-conversation /tmp/cc-conv.json
# Capture a snapshot of open file descriptors — sometimes reveals a stuck Bash tool
ls -l /proc/$(pgrep -f "claude-agent" | head -1)/fd 2>/dev/null | head -30Only after this snapshot should you run /subagent stop <name>. Stop it earlier and you lose the context that would have told you why it was stuck. I keep these four commands in a shell alias called cc-snap so they always run together.
Pattern 1: "running but silent" — the tool-call loop
This is by far the most common case. The subagent keeps calling the same tool in a cycle, so from the outside it looks active but never makes progress.
If your log shows consecutive calls like this, you're in a loop:
[subagent:tester] -> Bash: "npm test" (turn 7)
[subagent:tester] -> Bash: "npm test" (turn 8)
[subagent:tester] -> Bash: "npm test" (turn 9)The usual cause is a subagent that has no failure-analysis step. The test fails, and instead of diagnosing, the agent immediately re-runs it on the assumption that "maybe it was flaky." Then the same failure happens again. Then the cycle repeats until token budget or max_turns cuts it off.
The fix is to spell out the analysis step in the prompt and cap max_turns:
---
# .claude/agents/tester.md
name: tester
tools: Bash, Read
max_turns: 15
---
You are a test execution subagent. Follow these rules strictly.
1. Run the test suite exactly once.
2. On failure, list the failing test name plus three hypotheses for the cause.
3. Pick one verifiable hypothesis and confirm it by reading the relevant code.
4. If a fix is needed, describe it under a "Proposed Fix" section.
5. Before re-running the same test, write down why it failed last time.With max_turns set, any runaway loop terminates at turn 15. When you see max_turns reached in the log, that is direct evidence the agent was looping — it saves you an hour of guessing. In practice I use 15 for test-running subagents, 8 for linting subagents, and 25 for refactoring agents that genuinely need more turns.
Pattern 2: "it finished fine" — errors that never surface
Sometimes a subagent swallows an error internally and reports success to the parent. A file write fails, but the final output is a cheerful "Done." — a silent failure that can go unnoticed until the next day, or the next deploy, or the next customer bug report.
The fix is to require a structured exit report from every subagent. The parent prompt should enforce it:
{
"status": "success | partial | failed",
"files_modified": ["src/utils/retry.ts"],
"tests_passed": true,
"unresolved_issues": [],
"next_action_recommended": "..."
}State explicitly in the subagent prompt: "the final output must be exactly this JSON shape, and missing any field counts as failure." That single sentence does more work than any amount of "please be careful" nudging. If work was incomplete, the agent must report "partial" or "failed" and enumerate what went wrong under unresolved_issues.
On the parent side you can then decide deterministically whether to accept, retry, or escalate. No more guessing whether "Done" means "all green" or "I ran out of ideas and gave up."
I have come to prefer this pattern even for small agents because the reviewable trail it leaves is worth more than the 30 seconds of prompt engineering it costs. When a bug appears weeks later, the exit report JSON is the first thing I look at.
Pattern 3: "returns, but the content is off" — no output schema
When the subagent responds but not in the shape you expected, the root cause is almost always that the prompt never defined what "the shape" is. Free-form text is ambiguous by default, and subagents will happily produce plausible-looking output that does not fit your parser.
Two things to do:
- Show at least one full example of the expected output. One concrete sample eliminates more ambiguity than a paragraph of description. LLMs imitate patterns far better than they follow abstract rules.
- Validate on the parent side and retry exactly once with a targeted hint if the schema is violated.
// Parent-side pseudocode
const result = await runSubagent('refactor-agent', task);
const parsed = tryParseJSON(result);
if (!parsed || !parsed.status) {
// Schema violation — retry once with a specific reason
return await runSubagent('refactor-agent', {
...task,
retry_reason: 'Missing `status` field. Return exactly the JSON shape shown earlier.',
});
}
// Only one retry. Beyond that, the design itself is the problem.
return parsed;Unbounded retries just trade a silent failure for an infinite loop. Cap it at one. If the retry also fails, log the offending output and fall back to a human review path rather than another retry — because a pattern that fails twice in a row is rarely fixed by a third attempt.
Pattern 4: "returns eventually, but far too slowly"
A related but less-discussed symptom: the subagent does return, but only after five or ten minutes of silence. Usually the parent appears hung because it stops rendering streamed tokens while the subagent runs.
Two quick checks help here. First, verify the parent is actually draining the subagent's streaming output — if you invoked it from a script without attaching stdout, the buffering itself may be the issue. Second, look at whether the subagent is using a Bash call with a long-running command (a test suite, a webpack build) without an explicit timeout. Adding a sensible timeout (say, timeout 120s npm test) turns "hangs forever" into a crisp failure with a reason.
Still stuck? Reload agent definitions
If you changed .claude/agents/*.md but the behaviour hasn't changed, the definition is probably cached.
# Clear the cached agent definitions — harmless to run during development
rm -rf ~/.claude/cache/agents/
# Start a fresh session if the current one is wedged
claude --new-sessionI was caught by this one embarrassingly many times. Updating the file, rerunning, and seeing the old behaviour convinced me I had a subtler bug — when really the file had never been re-read.
For the step beyond single-agent debugging, Claude Code subagent orchestration covers role separation across multiple agents. If your issue sits upstream — a long session losing coherence — read it alongside preserving context in long sessions. When tool chains themselves are what's breaking, production debugging for Claude Code Hooks pairs well with the patterns above.
When I had subagents running the Dolice Labs auto-posting as an indie developer, nothing drained me more than this exact silence. An error you can chase; a quiet hang gives you nothing to grab onto. Adding max_turns and a structured exit report didn't make every bug disappear, but it did guarantee that 'where it stopped' is always on record.
What to try next
If you have a subagent that is currently hanging, add max_turns to its definition file today. That single change makes most loop-based "never returns" problems visible instead of mysterious. It is the lowest-effort, highest-leverage fix in this article.
Once that is in place, move on to enforcing a JSON-shaped exit report. The moment output format is fixed, the "I have no idea what's happening" feeling stops reappearing — which, honestly, is half the battle.