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-04-24Advanced

Debugging Claude Code Hooks in Production — Where to Start When Logs Are Missing

Your Claude Code hooks work locally but go silent in production. Here is the three-layer observability pattern I use on real projects, plus a 60-second minimal reproduction recipe for isolating failures.

hooks14claude-code129debugging8production111observability20

Anyone who runs Claude Code hooks in production long enough eventually hits the same wall. A script that worked fine locally goes completely silent on the CI runner or a teammate's machine. No echo, no log file, no visible effect on Claude's behavior. You are staring at a problem with no obvious handhold.

This article is the debugging playbook I have built up from running hooks on real projects. It skips the surface-level "check your settings.json" advice and goes one level down: why you cannot see the logs in the first place, and how to reproduce something that refuses to reproduce.

The first question is not "did it fire" — it is "where did the output go"

Most people start debugging a silent hook by asking whether it was called at all. In my experience that is the wrong first question. Nine times out of ten the hook is firing; you just cannot see the output because stdout and stderr are being swallowed somewhere along the way.

Claude Code forks the hook as a child process and feeds it JSON over stdin. Stdout is reserved for the protocol contract with Claude itself, and stderr often ends up in a place the user never looks at. The print statements you saw in your terminal during development are invisible once Claude Code is launched from a different parent process in production.

So the first thing to fix in a production debugging session is not the script content — it is the output destination.

#!/usr/bin/env bash
# hook-entry.sh — always write to a known location
LOG_DIR="${HOME}/.claude/hooks-debug"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/$(date +%Y%m%d).log"
 
{
  echo "---"
  echo "time: $(date -Iseconds)"
  echo "pid: $$"
  echo "hook: $CLAUDE_HOOK_EVENT"
  echo "tool: $CLAUDE_TOOL_NAME"
  echo "cwd: $(pwd)"
  echo "stdin: $(cat)"
} >> "$LOG_FILE" 2>&1

Wrap your real script with this tiny shim. The goal is to guarantee that between "hook fires" and "user logic runs" there is at least one line written to a known location. That single line decides whether you are debugging "the hook never got called" or "the hook ran but my logic broke."

I once spent two hours tearing apart the internals of a hook only to discover the matcher was wrong and nothing was being called at all. Prove the binary question — did it fire — separately from script quality. It saves hours.

Build a three-layer observability stack

If you are running hooks in production, set up observability in three layers. I call this the "hook visibility stack."

Layer 1 — proof of fire. The shim above. It records only that the hook was invoked. Nothing about the internals.

Layer 2 — I/O capture. The JSON received on stdin, the stdout the script tried to return, and the exit code. With this you have the full protocol trace of what Claude sent and what your hook replied.

Layer 3 — internal tracing. Script-specific logging for the actual logic. This varies by project, but it sits on top of layers 1 and 2.

A quick one-file implementation that ties the first two together:

#!/usr/bin/env bash
set -euo pipefail
LOG_DIR="${HOME}/.claude/hooks-debug"
mkdir -p "$LOG_DIR"
TRACE="$LOG_DIR/trace-$(date +%Y%m%d-%H%M%S)-$$.log"
 
# Layer 1: fire
echo "[fire] $(date -Iseconds) event=$CLAUDE_HOOK_EVENT tool=${CLAUDE_TOOL_NAME:-}" \
  >> "$LOG_DIR/fire.log"
 
# Layer 2: I/O capture
STDIN_JSON=$(cat)
echo "$STDIN_JSON" > "$TRACE.in.json"
 
RESULT=$(echo "$STDIN_JSON" | your-actual-hook 2>> "$TRACE.err")
EXIT=$?
 
echo "$RESULT" > "$TRACE.out.json"
echo "exit=$EXIT" > "$TRACE.exit"
 
echo "$RESULT"
exit $EXIT

It looks like overkill until the first production outage. Hooks are a system where nothing happens when things are working. When they break, you are starting from zero information unless you invested in visibility ahead of time. The time saved when things break is enormous compared to the cost of writing the shim.

Build a 60-second minimal reproduction

The hardest part of "works locally, breaks in production" problems is reproducing them. Production configurations are complex and hard to replicate. I optimize for speed instead: I make sure I can stand up a minimal environment with only the failing hook in under sixty seconds.

The trick is to never copy config files from the production project. Keep a tiny scratch project that tests one hook at a time.

mkdir -p /tmp/hooks-repro && cd /tmp/hooks-repro
cat > .claude/settings.json <<'JSON'
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "/tmp/hooks-repro/hook.sh" }
        ]
      }
    ]
  }
}
JSON
 
cat > hook.sh <<'SH'
#!/usr/bin/env bash
echo "[$(date -Iseconds)] hook fired" >> /tmp/hooks-repro/fire.log
cat > /tmp/hooks-repro/stdin-$$.json
exit 0
SH
chmod +x hook.sh

Launch Claude Code against this and run a tool call. If the hook fires here, you have a known-good baseline: the hook mechanism itself works on this machine. If it fails, the problem is infrastructure — config file location, permissions, Claude Code version — not your logic.

I strongly prefer building up from minimum toward production rather than subtracting from production toward minimum. Additive debugging exposes the exact addition that breaks things.

Understand the hook protocol contract precisely

The second classic failure mode is "the hook fires but Claude's behavior does not change." Almost always this means exit codes and stdout are being misused.

The Claude Code hook protocol has a few rules that matter:

  • Stdin carries event JSON from Claude
  • Stdout carrying JSON is interpreted by Claude as a control signal
  • Exit 0 means "let the flow continue"; non-zero means "the hook made a decision"
  • Specific exit codes defined by Anthropic (for example "block") stop tool execution

The failure I see constantly is printing human-readable messages to stdout. A casual echo "starting analysis" becomes unknown JSON from Claude's perspective and poisons whatever control signal you meant to return.

My rules are:

  • Human logs go to stderr or a dedicated log file, never stdout
  • Stdout is exclusively protocol JSON
  • Terminate JSON with a newline — some implementations expect line-delimited output
  • Exit codes are binary by design: 0 means pass, a chosen non-zero means block

Freeze these conventions in your hook template. It survives staff turnover better than documentation alone.

Multi-user environments: permissions, paths, race conditions

Teams running Claude Code together hit three issues that solo developers rarely see:

  1. Missing execute permission. The hook script lacks +x. Git only preserves a subset of permissions, so this breaks silently across machines.
  2. User-dependent absolute paths. /home/alice/bin/hook.sh in settings.json means something different for Bob.
  3. Concurrent write collisions. Multiple editor sessions write to the same log file and corrupt the tail.

Triage in this order: permissions first (ls -l, enforce with .gitattributes or a pre-commit hook), then paths (use $HOME or, better, a project-relative .claude/hooks/xxx.sh), then concurrency (include $$ — PID — in log filenames).

If you run hooks in CI, watch out for root-like users where $HOME resolves to /root. Avoid $HOME in production hooks entirely. Self-contained project-relative paths are safer over the long haul.

Routine practices that kill "cannot reproduce" bugs before they happen

Everything above is reactive. The practices below are preventive, and they matter more.

Practice 1 — always install a sentinel hook. Before any "real" hook runs, have a tiny one that only records the fact that it was invoked. Then "did it fire" is a question you can answer in one grep.

Practice 2 — log version information. Claude Code version, script git SHA, OS, shell version. Eight out of ten "only breaks in production" bugs trace to a version mismatch somewhere. Knowing the versions at a glance closes the case fast.

Practice 3 — run schema tests on captured stdin. Claude Code updates occasionally reshape the JSON shape delivered to hooks. Schedule a test that runs against the layer-2 archive and fails if expected keys disappear. You catch protocol drift before it burns you.

Teams that install these three practices almost never have the "suddenly broke" class of incident. Teams that skip them eventually hit a big one.

A real debugging walkthrough from production

A concrete example from my own work. The symptom: a secrets scanner running as a PreToolUse hook worked for everyone except one teammate.

I spent the first hour staring at settings.json, looking for a matcher mismatch. No progress. Installing the layer-1 sentinel hook changed everything. In the problem teammate's environment the fire log stayed empty. That single data point narrowed the investigation to the config layer — the hook script itself was off the suspect list.

Claude Code merges user, project, and local settings in a defined order. I opened .claude/settings.local.json in the teammate's checkout and found an old experimental hook configuration that was overriding the PreToolUse array wholesale. That was the bug.

The interesting part is not the root cause. The interesting part is that without layer 1 I would have spent a full day. The moment "did not fire" became a known fact, my search space collapsed to files under .claude/.

Debugging Claude Code hooks in production is less about clever tricks and more about patient, cheap observability. Logging costs little; not logging costs hours when something breaks. That asymmetry is what separates teams who ship hooks safely from teams who quietly abandon them after the first incident.

If you only adopt one piece of this article, install the layer-1 sentinel hook today. The next time something goes wrong you will have handed your future self several hours of sanity.

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-04-29
Observability for Claude Code with OpenTelemetry — A Production-Grade Tracing Guide for Agentic Workflows
Trace Claude Code agent runs end to end with OpenTelemetry. Hook integration, per-tool spans, MCP propagation, cost attribution, and sampling patterns that survive thousands of runs per day.
Claude Code2026-04-25
Secret Management and Trust Boundaries for Claude Code — A Production Guide for the Agent Era
A field-tested approach to secrets in a Claude Code workflow: trust-boundary modeling, three injection patterns, leak-prevention hooks, and rotation runbooks — with working code for .env, MCP, and OS Keychain integrations.
Claude Code2026-07-09
Is the Draft That Passed the Gate the Same One You Published?
In unattended pipelines, the file your quality gate inspected and the file you actually publish can quietly diverge. Here is a digest-bound gate receipt design, with working code and measured results from 180 days of running it.
📚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 →