Headless -p mode hangs forever reusing a server-closed idle keep-alive connection after a long tool execution

Status Open
Reported on v2.1.214
Maintainer reply None cached
Activity 0 comments · opened Jul 18, 2026

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.com are 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:

  1. The agent starts, streams its first assistant text normally.
  2. It begins its first tool round-trip whose local execution spans ~2+ minutes (e.g. reading/verifying a repo).
  3. The next API request apparently reuses a pooled connection closed during the gap: lsof -p <pid> shows every TCP fd to 160.79.104.10:443 in CLOSED state 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.

View original on GitHub ↗