Hook entries without `timeout` block SDK/CLI forever when the command doesn't exit — default timeout would prevent silent hangs

Resolved 💬 3 comments Opened Apr 17, 2026 by shrutidee108 Closed Apr 21, 2026

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

  1. In a test project ~/repro/, create .claude/settings.json:
{
  "hooks": {
    "SessionStart": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "bash -c 'while true; do sleep 5; done'"
      }]
    }]
  }
}
  1. 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: true and assumes they are honored. The SDK silently ignores unknown fields.

Suggested remediation

  1. Emit a DEBUG log line when each hook command begins and completes, with elapsed time and exit code. Today there's nothing.
  2. Default timeout for SessionStart hooks to something finite (e.g. 30 s), with a warning in logs when a hook is killed by the default.
  3. Warn on unknown hook fields (asyncRewake, rewakeMessage, rewakeSummary in the hook entry) so third-party extensions are surfaced rather than silently ignored.
  4. 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
}

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗