Teams add Claude Code hooks so AI agents cannot do damage, and then assume they work. Ours passed on every run while doing nothing. A hook that silently does nothing is worse than no hook, because everyone stops looking. The fix is four cheap checks any team can copy.
The short answer: a hook goes silent when its output goes nowhere, when its path does not resolve, or when its exit code gets rewritten. Check all three in CI. Then put the hook that matters most on the Stop event, so the agent cannot say "done" while tests are red.
How do Claude Code hooks block anything?
A hook is a shell command that Claude Code runs on an event. PreToolUse runs before a tool call, Stop when the agent wants to finish. The command gets the event as a JSON object on stdin.
The exit code decides:
Exit 0 lets it pass.
Exit 2 blocks. What the hook wrote to stderr goes back to the agent as the reason.
Any other code is a non-blocking error. Claude Code carries on.
That is the whole contract. Everything that broke for us broke inside it.
Why did our hooks do nothing?
Two hooks in our company config stayed silent for a week, from day one. Three causes, each harmless alone:
Muted. Both wrote their only message to stderr. The command in settings ended in 2>/dev/null. The agent never heard a word.
Missing. Both used a relative path. In a session started in a nested repository the path did not exist. Exit 127, command not found.
Masked. A trailing || true turned that 127 into 0. On a blocking event it turns a 2 into 0 just the same. Every run "passed".
Nobody noticed, because a silent hook looks exactly like a healthy one. We only found it in an analysis of our own Claude Code sessions.
We added a check to our validator, which runs in CI across our repositories. Four greps over .claude/settings.json, nothing executed:
Check
What it catches
Fix
The script exists
A path that does not resolve
"$CLAUDE_PROJECT_DIR"/... or an absolute path
The script is executable
A hook that only runs by luck
chmod +x
stderr is not discarded when the script writes to stderr
A muted hook
Drop 2>/dev/null
A PreToolUse hook does not end in || true
A block rewritten into a pass
Drop || true
We tested the check against the real failure. The old config raised critical errors, the fixed one passed.
Not every || true is a bug. Our session greeting runs on SessionStart with 2>/dev/null || true, on purpose. A greeting must never break a session. The check only flags it where a block is expected.
The lesson in one line, as it stands in our attribution hook: "A rule is a request. This is the lock." And a lock needs its own test.
Not long ago the agent had its rules only in rule files, and sometimes it ignored them. So we turned them into checks it cannot get around.
Filip Papranec
Founder, Digitaliko
Which hook actually gates?
The Stop hook. An agent that says "done" while tests are red costs a senior a review round. A Stop hook makes the agent fix it first.
Our version from the public starter, slightly shortened:
#!/bin/bash
# Stop hook: the agent can't finish while the checks are red.
in=$(cat)
[ "$(printf '%s' "$in" | jq -r '.stop_hook_active // false')" = true ] && exit 0
cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null || exit 0
out=$(GATE_SKIP_INSTALL=1 GATE_SCRIPTS="typecheck lint test" bash scripts/gate.sh checks 2>&1) && exit 0
jq -n --arg r "Checks are red. Fix them before you say the task is done:
$out" '{decision: "block", reason: $r}'
Step by step it does this:
01
Reads stop_hook_active
If it is true, the agent is already continuing because of this hook. The hook lets it go, so it never loops.
02
Runs the checks
Typecheck, lint and tests, the same as in CI.
03
Blocks with a reason
When the checks are red, it returns decision: block with the failure log as the reason.
04
The agent fixes the cause
It reads the log, fixes the error and tries to finish again.
It blocks with JSON, not an exit code. The agent keeps working, and the reason it gets is exactly what failed.
It is an early warning, not the gate. In our agent loop the authoritative check runs again after the session, in code the agent cannot touch. The hook only saves a round.
What broke in our own Stop hook?
Our agent loop's Stop hook had its own silent bug. It set a fixed, minimal PATH without the user-level tool directories, so repositories on Bun lacked the tools the checks need. It also looked for the gate in one fixed place that only existed on the agent's machine. One PR fixed both: the full PATH, and the gate found relative to the hook itself.
Claude Code hooks for a team: where to start?
Go through what your repositories already have. Each point is a few lines in settings or in CI:
The third point is the easiest to miss. Our attribution hook reads its input with jq, falls back to python3, then to sed. A missing jq must never turn the lock into a silent allow. Relative paths, in turn, break in nested repositories and worktrees.
We set up hooks, gates and CI right in a team's repository as part of AI-native SDLC for development teams. Is your team short of hands rather than rules? See a senior developer for your team. AI writes all of our code today, and a senior is accountable for the result. Since 2023 we have delivered 15+ production projects.
We get back to you within two business days, and together we pick what to fix first.
Shell commands that Claude Code runs on events, such as PreToolUse before a tool call or Stop when the agent wants to finish. A hook gets the event as JSON on stdin and decides with its exit code or a JSON answer.
How does a Claude Code hook block a command?
With exit code 2. Claude Code then blocks the action and passes what the hook wrote to stderr back to the agent as the reason. Exit 0 allows it. Any other code is a non-blocking error and the agent carries on.
Why does my Claude Code hook not run?
Most often a relative path that does not resolve in the session directory, stderr discarded with 2>/dev/null, or a trailing || true that rewrites the exit code. Start the path with "$CLAUDE_PROJECT_DIR" and check the hook config in CI.
What does stop_hook_active mean?
It is true when the agent is already continuing because a Stop hook blocked it. Exit early when it is true. Otherwise a check the agent cannot fix keeps blocking it forever.
Is a Stop hook enough as a quality gate?
No. It is an early warning inside the session. Run the same checks again in CI and in branch protection, where the agent cannot change them.
Ask AI about this article
The AI also draws on our other articles and experience from real projects.
Playwright MCP gives an AI agent a real browser. How we use it so the agent checks its own change in the running app and hands back PASS or FAIL with screenshots, what it catches that unit tests miss, and when plain Playwright scripts are the better tool.
LLM as a judge means a language model grades another model's work against written criteria. How we run one on every pull request our AI agent opens: machine checks first, a fresh read-only session told to refute, a typed verdict, and the rules we added after the judge got it wrong.
How we run Claude Code review on every pull request with one reusable GitHub Action: what the AI reviewer is told to look for, how it stayed green and silent, what it misses, and why only a person approves.