Skip to content

Claude Code hooks that silently do nothing

For CTOs and engineersHarness and checks · Updated

Filip PapranecFounder, Digitaliko

Every run passed. The hook did nothing. Illustration: a settings file with a hook command ending in 2>/dev/null || true, next to a row of green checks that prove nothing.Every run passed. The hook did nothing. Illustration: a settings file with a hook command ending in 2>/dev/null || true, next to a row of green checks that prove nothing.

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".
Left, the hook that stayed silent: the command bash tools/hooks/guard.sh 2>/dev/null || true and three causes: a relative path (exit 127), stderr to /dev/null, and || true turning the exit code into 0. Right, a Stop hook in four steps: stop_hook_active, run typecheck, lint and tests, and on red return decision: block with the failure log.Left, the hook that stayed silent: the command bash tools/hooks/guard.sh 2>/dev/null || true and three causes: a relative path (exit 127), stderr to /dev/null, and || true turning the exit code into 0. Right, a Stop hook in four steps: stop_hook_active, run typecheck, lint and tests, and on red return decision: block with the failure log.

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.

The broken pattern, simplified:

{ "type": "command", "command": "bash tools/hooks/guard.sh 2>/dev/null || true" }

The fixed one, from our public starter:

{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/push-gate.sh" }

Which four checks catch a silent hook?

We added a check to our validator, which runs in CI across our repositories. Four greps over .claude/settings.json, nothing executed:

CheckWhat it catchesFix
The script existsA path that does not resolve"$CLAUDE_PROJECT_DIR"/... or an absolute path
The script is executableA hook that only runs by luckchmod +x
stderr is not discarded when the script writes to stderrA muted hookDrop 2>/dev/null
A PreToolUse hook does not end in || trueA block rewritten into a passDrop || 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
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:

  1. 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.

  2. Runs the checks

    Typecheck, lint and tests, the same as in CI.

  3. Blocks with a reason

    When the checks are red, it returns decision: block with the failure log as the reason.

  4. 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.

You can start from the Claude Code production starter: a push gate, a secret scan and a Stop hook, MIT licensed.

Where hooks sit next to rule files is covered in Rules for AI code in a team. How to roll this out to a whole team is in How to introduce AI into a development team. What the agent may do without asking, and when skipping permissions is safe, is in Claude Code dangerously skip permissions.

Want hooks that hold in your repositories?

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.

If you'd rather talk first, book a 15-minute meeting with Filip.

When did you last check that your hooks still block?

In short

  • A hook goes silent when its stderr is discarded, its path does not resolve or || true rewrites its exit code.
  • Four greps over .claude/settings.json in CI catch all three.
  • A Stop hook blocks "done" while tests are red, and blocks only once.
  • The authoritative check runs again in CI, where the agent cannot change it.

Frequently asked questions

What are Claude Code hooks?

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.

ChatGPTClaudePerplexity
Engineering

Other articles

Harness and checks

Playwright MCP: let the AI agent prove it works

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.

Harness and checks

LLM as a judge: a reviewer that tries to refute the work

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.

Harness and checks

Claude Code review in CI: what AI code review misses

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.

Time to stop planning and start solving.

Contact

  • Digitaliko s.r.o.
    Šípková 377/17
    958 03 Malé Uherce, Slovakia
  • Get in touch
    info@digitaliko.com
    +421 950 595 506
    WhatsApp