CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/Claude Code
Claude Code/2026-05-21Intermediate

Claude Code Hook `command timed out`: Timeout Settings and Split-Execution Patterns That Actually Work

Fix Claude Code's `command timed out` hook failure without just bumping the timeout. Includes practical split-execution, detached background jobs, and a settings.json layout that keeps your session fast.

claude-code129hooks14troubleshooting87timeout9settings7

The morning I added eslint --fix to PostToolUse, my terminal filled with a red wall of Hook 'lint-all' command timed out after 60s. The hook was firing fine — but my over-grown monorepo lint config couldn't finish in the default sixty seconds. As an indie developer who has been shipping apps since 2014 (around 50M cumulative downloads across wallpaper, healing, and manifestation titles), I've watched that exact scenario play out more than once. Frontend tooling bloats quietly, and Claude Code's hook watchdog is the first thing that notices.

This failure is distinct from a hook returning a non-zero exit code: the command starts correctly, but Claude Code's internal watchdog sends SIGTERM at the timeout boundary. Here's how I diagnose it and the configuration patterns I now reach for.

What Actually Happens When command timed out Fires

When Claude Code runs a hook, it tracks the wall-clock time of the spawned process tree. If you exceed the configured (or default) timeout, the watchdog sends SIGTERM to the entire tree. The default I see on my client is 60 seconds, though this varies by version. The hook script is a child process, so Claude Code's main session stays alive — only the hook dies mid-flight.

The easiest trap to fall into is backgrounding inside the hook: writing npm run lint & doesn't help, because Claude Code is watching the whole process tree, not just the shell. Unless you fully detach the child (which I'll cover further down), it's still subject to the timeout.

The patterns that reliably blow past 60 seconds are predictable: ESLint over the full project, Prettier on a monorepo, tsc --noEmit, the full Jest suite, or Docker image builds. None of these are wrong commands — they're just wrong for a per-tool-call hook.

The First Question Is "What's the Hook Granularity," Not "How Big Is the Timeout"

When the timeout hits, the instinctive move is to bump timeout from 60 to 300 seconds. That rarely solves the real problem. Hooks fire on every matching tool call, so a five-minute lint means you wait five minutes every time you edit a single file. Claude Code's speed advantage evaporates. The fix is almost always to make the hook itself terminate in seconds.

A practical pattern: narrow the PostToolUse matcher to Edit / Write / NotebookEdit, then write the hook body so it only inspects the file Claude Code just touched. Claude Code exposes tool_input.file_path through the hook payload — read it from stdin and dispatch per extension.

#!/usr/bin/env bash
# .claude/hooks/post-edit-lint.sh
set -euo pipefail
 
# Extract file_path from the JSON payload Claude Code passes in
FILE="$(jq -r '.tool_input.file_path // empty' <<< "${CLAUDE_HOOK_INPUT:-}")"
[[ -z "$FILE" ]] && exit 0
[[ ! -f "$FILE" ]] && exit 0
 
# Route to the right tool based on extension; everything else is a no-op
case "$FILE" in
  *.ts|*.tsx|*.js|*.jsx)
    npx --no-install eslint --fix --max-warnings=0 "$FILE"
    ;;
  *.md|*.mdx|*.json|*.yaml|*.yml)
    npx --no-install prettier --write "$FILE"
    ;;
esac

This never scans the whole project. On a repo that used to take 90 seconds for eslint ., the per-file version finishes in 2–3 seconds — well below the watchdog ceiling and barely noticeable in the loop.

Writing the timeout Field Correctly When You Truly Need More Time

There are legitimate cases where a hook needs to run longer than 60 seconds — a build script triggered from PostToolUse, or a larger test suite hanging off a Stop hook. Claude Code's settings.json lets you set timeout per hook entry.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/post-edit-lint.sh",
            "timeout": 30
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/run-build.sh",
            "timeout": 300
          }
        ]
      }
    ]
  }
}

The unit is seconds, not milliseconds. I see people occasionally treat it as milliseconds and end up with surprisingly long waits — 500 means 500 seconds. Stop hooks fire when the user has already decided to end the turn, so a longer timeout there doesn't hurt perceived performance. PostToolUse fires on every edit, so I keep it at 30 seconds maximum.

Settings changes don't hot-reload. Exit the Claude Code session (/exit) and start a new one so the new timeout takes effect.

Detaching Long Work So the Hook Returns Instantly

When you really do want long-running work but you don't want Claude Code waiting on it, detach the job entirely and let the hook exit immediately. setsid + nohup + disown removes the process from the parent's job table and the controlling terminal.

#!/usr/bin/env bash
# .claude/hooks/post-edit-build.sh
set -euo pipefail
 
LOG_DIR="${HOME}/.claude/hook-logs"
mkdir -p "$LOG_DIR"
TS="$(date +%Y%m%d-%H%M%S)"
LOG="${LOG_DIR}/build-${TS}.log"
 
# Fully detach from Claude Code's hook watchdog
setsid nohup bash -c "
  cd \"$(pwd)\"
  npm run build > '$LOG' 2>&1
  echo \"exit=\$?\" >> '$LOG'
" </dev/null >/dev/null 2>&1 &
disown || true
 
# Return success immediately
echo "build kicked off → $LOG"
exit 0

The hook finishes in milliseconds. The build keeps running in the background. The log file is your only signal — which is fine for things like deploy verification or AdMob revenue aggregation that you want triggered automatically but don't need feedback on in real time.

The trade-off is that build failures don't surface to Claude Code immediately. If you want failure visibility, add a Stop hook or a separate script that scans the latest log file and reports errors back into the next turn.

Where to Look When You're Stuck

Claude Code does log timeout events to stdout, but they scroll away quickly. I add a couple of lines at the top of every hook to log who ran, when, and with what file. This pays for itself the first time you need it.

# Top of every hook script
LOG_DIR="${HOME}/.claude/hook-logs"
mkdir -p "$LOG_DIR"
exec > >(tee -a "$LOG_DIR/hook-$(date +%Y%m%d).log") 2>&1
echo "[$(date '+%H:%M:%S')] hook=post-edit-lint file=$FILE"

Now you have a per-day file with timestamps and the file_path Claude Code passed in. If the timeout is hitting in a specific spot, turn on set -x temporarily to see which command burns the budget.

In my case, an iOS-side SwiftLint hook was crossing 60 seconds and the culprit turned out to be xcodebuild -showBuildSettings running every single call. The settings don't change between runs, so I added five lines to cache the result in /tmp for the day, and the hook went from 70 seconds to 3. The root cause of timeouts is rarely "this command is heavy" — it's usually "this heavy command is running on every call."

The Hook Rules I'm Settled On Right Now

A short list of rules I keep coming back to. First: PostToolUse only runs things that finish in 30 seconds or less. Anything bigger goes into a Stop hook or a fully detached background job. Second: every hook narrows its target — read tool_input.file_path and bail out fast on the wrong extension. Third: prefer npx --no-install so the first call isn't dragged down by a dependency download. Fourth: log to daily files; the time you save on diagnosis pays back many times over. Fifth: builds and end-to-end tests don't belong in hooks at all — push them to scheduled jobs or CI so they never block the session.

Hooks are powerful, but a wrong design tanks the perceived speed of the whole tool. When command timed out shows up, my rule is to look at granularity before reaching for the timeout knob. Thanks for reading — I hope this saves someone the morning I lost staring at a wall of red.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-06-18
When a Broken settings.json Stops Claude Code From Starting — Safe Mode and How to Split Your Config
How to find which config layer is broken when a settings.json syntax error stops Claude Code from starting, recover in minutes, and structure your settings so an automated pipeline can't quietly break itself.
Claude Code2026-04-25
When Claude Code Hooks Loop Forever — Stopping Self-Triggering PostToolUse Hooks
You wired up Claude Code hooks and now your terminal is a waterfall of tool calls that won't stop. Here's why PostToolUse hooks loop on themselves, and the patterns I use in real projects to make sure they never do again.
Claude Code2026-04-24
The Claude Code Error Handbook — Auth, Billing, Stalls, Tools & MCP, Diagnosed by Symptom
A field-tested reference for 40+ Claude Code error patterns, organized by visible symptom: authentication, billing, response stalls, tool failures, MCP connectivity, and hook issues. Each entry tells you where to look and what to change.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →