Hook entries without `timeout` block SDK/CLI forever when the command doesn't exit — default timeout would prevent silent hangs
GitHub Issue Draft — file at anthropics/claude-code
Suggested title: Hook entries without timeout block the SDK/CLI forever when the command doesn't exit on its own — default timeout would prevent accidental hangs
Labels: bug, sdk, hooks
---
Summary
A SessionStart hook entry pointing to a long-running bash watcher (an infinite while true loop) blocks Claude Code subprocess startup indefinitely when no timeout field is specified. The 6-minute stall we traced had no error message, no log entry, and no user-facing indication that a hook was responsible — just a silent gap between MCP connection completion and the first /v1/messages request.
This surfaced through a third-party integration (ClaudeClaw OS by earlyaidopters) that uses a Paperclip-specific hook extension (asyncRewake: true) to signal that a hook should be treated as an async watcher. Stock Claude Code CLI 2.1.112 / claude-agent-sdk 0.2.111 do not recognize that field and run the hook synchronously.
Related: #274 (claude-agent-sdk-typescript, "Control request timeout: initialize"), #41792 (MCP_CONNECTION_NONBLOCKING docs), obra/superpowers#515 (slow startup blamed on plugin cache — a similar user-visible symptom).
Environment
- Claude Code CLI: 2.1.112
@anthropic-ai/claude-agent-sdk: 0.2.111- Node: v22.22.2
- macOS 26.4 arm64
- Integration: ClaudeClaw OS v1.1.0 spawning per-message claude subprocess
Minimal reproduction
- In a test project
~/repro/, create.claude/settings.json:
{
"hooks": {
"SessionStart": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "bash -c 'while true; do sleep 5; done'"
}]
}]
}
}
- Run
cd ~/repro && claude --dangerously-skip-permissions -p "say done"
Expected: response within a few seconds.
Actual: hangs until the hook is SIGKILLed externally or the agent-level timeout fires. No error message, no log hint that a hook is blocking.
Debug log trace (from the real case)
A silent 354-second gap between MCP connection completion and SIGINT:
2026-04-17T19:12:09.707Z [DEBUG] [MCP] Server "firecrawl" connected with subscribe=false
<354 seconds of silence, no log lines>
2026-04-17T19:18:03.391Z [DEBUG] MCP server "jcodemunch": Sending SIGINT to MCP server process
During the gap: kevent wait, no CPU, no network to api.anthropic.com. All MCPs already connected in ~3 sec.
Why this is easy to miss
- Hooks are loaded very early in subprocess startup but produce no log output when they're running.
- The hook timing is not surfaced in
--debug-to-stderr. MCP_CONNECTION_NONBLOCKING=true,CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL=1, and similar env-var workarounds do not apply to hooks, but the stall pattern is very similar to those they do address, sending investigators down wrong paths.- A user looking at the hook definition sees custom fields like
asyncRewake: trueand assumes they are honored. The SDK silently ignores unknown fields.
Suggested remediation
- Emit a DEBUG log line when each hook command begins and completes, with elapsed time and exit code. Today there's nothing.
- Default
timeoutfor SessionStart hooks to something finite (e.g. 30 s), with a warning in logs when a hook is killed by the default. - Warn on unknown hook fields (
asyncRewake,rewakeMessage,rewakeSummaryin the hook entry) so third-party extensions are surfaced rather than silently ignored. - Document in the hooks reference: infinite-loop commands MUST specify
timeout.
Workaround
Adding "timeout": 3 to the offending hook entry. In our case this changed subprocess startup from 6 min timeout to 9 seconds.
{
"type": "command",
"command": "bash /path/to/watcher.sh",
"timeout": 3
}This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗