◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
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-code132hooks22troubleshooting92timeout10settings9

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-10-05
I Wrote "I'll Do the Commits" in CLAUDE.md. The Next Morning git log Had One More Line.
When Claude Code commits despite being told not to: undo with git reset --soft, then stop it physically with permissions.deny plus a PreToolUse hook. Covers the git -c form that deny rules let through, and how to test the hook without starting Claude Code.
⟐ Claude Code2026-05-24
When Claude Code's Sequential Edits Cascade into Failure — Fixing 'old_string not found' and 'not unique' on the Same File
Run several Edits against the same file in one turn and the second or third one suddenly fails with 'not found' or 'not unique'. The cause is order-dependence inside a single context snapshot, and a Read + scoped patches usually rescue it.
⟐ Claude Code2026-05-10
When Claude Code's Read Doesn't Reach the End of a File — Catching the PARTIAL view Notice and Sizing offset/limit
When Read returns only part of a large file, Claude Code says so with a PARTIAL view notice. Here is how to catch it, and how to size offset and limit by byte budget rather than line count, with measurements from my own sandbox.