Headless -p mode hangs forever reusing a server-closed idle keep-alive connection after a long tool execution
Summary
In print mode (claude -p with --output-format stream-json --verbose), Claude Code hangs indefinitely when the API request that follows a long local tool execution reuses a pooled keep-alive connection that the server closed during the idle gap. No timeout fires, no retry happens; the process sleeps forever (observed 30+ minutes, 0% CPU) until killed.
Environment
- Claude Code 2.1.214, macOS (Darwin 25.5.0, arm64)
- Network path via a VPN tunnel interface; idle keep-alive connections to
api.anthropic.comare closed by the remote after roughly 1–2 minutes of idle (this may simply be normal load-balancer behavior — the point is the client's reaction to it) - Observed on processes spawned headlessly by a background service (a CI-style validation daemon), auth via normal user OAuth
Observed behavior
Six out of six independent headless agents hung identically over one evening:
- The agent starts, streams its first assistant text normally.
- It begins its first tool round-trip whose local execution spans ~2+ minutes (e.g. reading/verifying a repo).
- The next API request apparently reuses a pooled connection closed during the gap:
lsof -p <pid>shows every TCP fd to160.79.104.10:443inCLOSEDstate while the process sleeps; CPU time stays at ~0.5–0.8 s total over 30+ minutes; no further output, no error, no exit.
--strict-mcp-config vs full MCP config made no difference. An env: {"API_TIMEOUT_MS": "300000"} block supplied via --settings <file> also did not rescue this mode (agents still hung well past 5 minutes) — either the variable is not honored via settings env in this path, or it does not cover reuse-of-dead-connection.
Transport-level confirmation
From a plain shell on the same machine, python3 http.client: open HTTPS connection to api.anthropic.com, one request, idle 135 s, second request on the same connection → RemoteDisconnected raised immediately. So the close is promptly detectable at the socket level; a reconnect-and-retry would succeed. Interactive Claude Code sessions on the same machine/network never exhibit the hang.
Expected behavior
On reusing a pooled connection that the server has closed (or on any request exceeding a sane transport timeout), the client should detect the dead connection and retry on a fresh one — which is evidently what interactive usage ends up doing.
Minimal repro recipe
On any path that closes idle keep-alives after ~60–120 s:
claude -p 'Use the Bash tool to run exactly: sleep 130. Then reply with exactly: DONE' \
--dangerously-skip-permissions --output-format stream-json --verbose
Expected DONE after ~135 s; the failure mode is a silent hang at the post-sleep API call. (Recipe reconstructed from the six production hangs — each died at exactly this shape of gap; the transport probe above verifies the idle-close precondition.)
Impact / workaround
Any unattended claude -p pipeline with tool executions longer than the path's idle-close window wedges permanently and needs external process supervision to recover. We worked around it by moving the affected pipeline to a different agent CLI, whose client recovers from the same idle closes.