Przejdź do treści

Claude Code hooks that silently do nothing

Dla CTO i programistówHarness i kontrole

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. A hook can pass 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 goes wrong with hooks goes wrong inside it.

Why does a hook silently do nothing?

A hook can stay silent from day one without anyone noticing. Usually for three reasons, each harmless on its own:

  • Muted. The hook writes its only message to stderr, but the command in the settings ends with 2>/dev/null. The agent hears nothing.
  • Missing. The hook uses a relative path. In a session started inside a nested repository, the path does not exist. Exit 127, command not found.
  • Masked. || true at the end turns 127 into 0. On a blocking event it turns 2 into 0 just the same. Every run "passes".
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 notices, because a silent hook looks exactly like a healthy one. Only an analysis of Claude Code sessions or a check in CI finds it.

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?

Add a check to your validator that runs in the CI of every repository. Four greps over .claude/settings.json, nothing is 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

Test the check on a real bug: a broken config must report critical errors, the fixed one must pass.

Not every || true is a bug. A greeting hook on SessionStart can use 2>/dev/null || true on purpose, because a greeting must never break a session. The check should only flag places 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. The authoritative check should run again after the session, in code the agent cannot touch. The hook only saves a round.

What to watch for in a Stop hook?

A Stop hook can have a silent bug of its own. A fixed, minimal PATH without the user tool directories means repositories on Bun miss the tools the checks need. A gate looked up in one fixed place only works on one machine. Set the full PATH and look up the gate 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 Claude Code best practices for a team: 7 rules that stick. 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 most of our code. A senior reviews every change and is accountable for production. Since 2023 we have delivered 15+ production projects.

We get back to you within two business days with a few questions about your work. Then we agree on what we do first.

If you'd rather talk first, book a 15-minute intro call.

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.

Zapytaj AI o ten artykuł

AI przy odpowiedzi wykorzysta też nasze inne artykuły i doświadczenia z prawdziwych projektów.

ChatGPTClaudePerplexity
Engineering

Kolejne artykuły

Harness i kontrole

Context engineering for coding agents: what goes in a repo

Context engineering decides what an AI coding agent sees while it works. What goes into AGENTS.md or CLAUDE.md, which rules load only where they apply, how the task becomes context, and how to keep the whole thing small.

Harness i kontrole

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 i kontrole

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.

Czas przestać planować i zacząć działać.

Kontakt

  • Digitaliko s.r.o.
    Šípková 377/17
    958 03 Malé Uherce, Słowacja
  • Prosimy o kontakt
    info@digitaliko.com
    +421 950 595 506
    WhatsApp