Hero Background

Next-Gen App & Browser Testing Cloud

Trusted by 2 Mn+ QAs & Devs to accelerate their release cycles

Next-Gen App & Browser Testing Cloud
AIAgent TestingCoding

Claude Code Hooks: Deterministic Rules for an Agentic Workflow

Hooks fire on lifecycle events rather than on model judgement, which makes them the one part of an agentic workflow you can actually guarantee.

Author

Bhawana

Author

Author

Shahzeb Hoda

Reviewer

Published on: August 27, 2026

You tell the agent, in CLAUDE.md, to run the formatter before it commits. It does, for eleven turns. On the twelfth it is deep in a refactor, the instruction is buried under forty thousand tokens of context, and it commits unformatted code.

Nothing malfunctioned. You wrote a suggestion and expected a guarantee.

Hooks are where that expectation belongs. They are the part of the workflow that does not consult the model.

TL;DR

Claude Code hooks are handlers the agent runs at fixed points in its own lifecycle, before a tool call, after one, or when a turn ends. They fire whether or not the model decides they should, which is what makes them the deterministic layer wrapped around a probabilistic agent.

  • The trigger surface - Claude Code documents more than thirty lifecycle events, running from session start through every tool call to the end of a turn, and a hook can attach to any of them.
  • The blocking signal - a command hook that exits with code 2 blocks the action outright, no matter what it printed, and its standard error becomes the message the agent reads back.
  • The verification gate - the highest-value hook does not format or log. It runs a real check against the running application and reports what that check actually proved.
  • The honest limit - a hook that exceeds its timeout is cancelled and its output discarded, so a rule can stop applying without anything reporting that it stopped.

What a Claude Code Hook Actually Is

A hook is a handler bound to a lifecycle event in the agent's own loop. When the event happens, the handler runs. The model is not asked whether it should.

That single property is the whole reason hooks exist. Instructions in CLAUDE.md are read by the model and weighed against everything else in context. A hook is executed by the runtime.

The surface is larger than most teams use. Anthropic's Claude Code hooks reference documents more than thirty distinct events, covering session lifecycle, prompt submission, every stage of a tool call, subagents, compaction, and file or configuration changes on disk.

  • Guaranteed, not requested - the handler runs on the event, so the rule holds on turn one and turn two hundred alike.
  • Outside the context window - a hook consumes none of the budget that a long instruction file spends and then loses.
  • Auditable - the configuration is a file in your repository, so what enforces the rule is reviewable in a pull request.
  • Not a permission system - hooks decide what runs around a tool call, while permission rules decide whether the call is allowed at all.

That last distinction matters more than it looks. We mapped the prevention side separately in pre-action checks for AI coding agents, which compares hooks against permission modes, sandboxes, and branch rules.

Which Events Can You Hook

All of them are available, but a small group carries almost every practical workflow. Grouping them by what they let you do is more useful than reading the full list top to bottom.

EventFires whenWhat it is good for
SessionStartA session begins or resumesInjecting current branch, ticket, or environment state as context
UserPromptSubmitYou submit a prompt, before Claude processes itRejecting a prompt outright, or attaching context the request implies
PreToolUseBefore a tool call executesDenying a destructive command, or rewriting its input
PostToolUseAfter a tool call succeedsFormatting a file that was just written, or linting the change
PostToolUseFailureAfter a tool call failsFeeding the real error back so the agent stops guessing
StopClaude finishes respondingRunning the verification that decides whether the turn is actually done
SubagentStopA subagent finishesChecking a delegated unit of work before its result is trusted
PreCompactBefore context compactionPersisting state that is about to be summarised away

Two of these are worth singling out. PostToolUseFailure exists because an agent that cannot see the real error invents a cause and fixes the wrong thing.

Stop is the one most teams leave empty, and it is the only event that fires exactly when the agent believes it is finished.

How a Hook Is Configured

Hooks live under a top-level hooks key in settings.json, keyed by event name. Each entry pairs a matcher with one or more handlers.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$CLAUDE_FILE_PATHS\"",
            "timeout": 60,
            "statusMessage": "Formatting the file that just changed"
          }
        ]
      }
    ]
  }
}

The matcher accepts three forms, and picking the wrong one is the usual reason a hook never fires.

  • Match everything - omit the matcher, or set it to an empty string or an asterisk.
  • Exact or alternation - a plain tool name such as Bash, or several separated by a pipe such as Edit|Write.
  • Regular expression - a pattern such as ^Notebook, or mcp__.* to catch every tool coming from an MCP server.
  • Narrower still - the optional if field takes permission-rule syntax such as Bash(git *) or Edit(*.ts), so one handler can target a subset of a matched tool.

A handler does not have to be a shell command. Five handler types are supported, and the choice changes what the hook can reach.

  • command - runs a shell command or script, the default for anything local, with a 600 second timeout unless you set one.
  • http - posts to a URL and reads the decision back from a 2xx response body, useful for a shared policy service.
  • mcp_tool - calls a named tool on a configured MCP server, with the tool input templated from the event.
  • prompt - asks a model to evaluate something, with a 30 second default timeout.
  • agent - hands the evaluation to a full agent, with a 60 second default timeout.

Note what the last two mean for determinism. A prompt or agent handler puts a model back inside the control path, so the hook fires reliably but its verdict does not have to be identical twice.

Configuration can come from user settings, project settings, gitignored local settings, managed policy, plugin bundles, and skill or subagent frontmatter. Hooks merge across those levels rather than overriding one another, so a project hook adds to your personal ones instead of replacing them.

Note

Note: TestMu AI's Kane CLI gives a hook something worth running: a plain-English objective, real Chrome, and a pass or fail. Try TestMu AI free!

What Hook Exit Codes Mean

A command hook communicates through its exit status, and only one value blocks anything.

Exit codeEffectWhere the message goes
0Success. Standard output is parsed for JSON control fieldsPlain text goes to the debug log, except on UserPromptSubmit, UserPromptExpansion, and SessionStart, where it is added to Claude's context
2Blocking error. The action is blocked regardless of any JSON printedStandard error becomes the blocking message the agent reads
Any other non-zeroNon-blocking error. Execution continuesIgnored entirely if the printed JSON passes schema validation

The third row is the one that bites. A script that exits 1 on failure, which is the convention almost every command-line tool follows, does not block anything in a hook.

For finer control, exit 0 and print JSON instead. A PreToolUse hook can return a hookSpecificOutput object carrying permissionDecision set to allow, deny, or escalate, a permissionDecisionReason string, additionalContext for the model, and an updatedInput object that rewrites the tool call before it runs.

Can a Hook Verify the Work, Not Just Police It

Yes, and this is where hooks stop being housekeeping. Nearly every hook example in circulation formats a file, writes a log line, or plays a sound. None of those answer whether the change works.

The Stop event fires when Claude finishes responding, which is precisely the moment its claim of done is worth testing.

A unit test run from that hook still only proves the code agrees with itself. To prove the interface works, something has to open the page. Kane CLI from TestMu AI is built for exactly that call: an objective in plain English, a real Chrome browser, and a machine-readable verdict.

  • No selectors to maintain - intent is anchored to the user-facing element, so a renamed CSS class does not turn the gate red.
  • Machine-readable by design - agent mode streams newline-delimited JSON, and the terminal run_end event carries the full result.
  • Evidence that outlives the session - each run seals a pack with per-step screenshots, a HAR network log, and console output under .testmuai/evidence in your repository.
  • Runs without a display - combining agent mode with headless mode is what makes it usable from a hook, from CI, and from a cron job.

Here is a real run of that shape against the TestMu AI Selenium Playground, on Kane CLI 0.8.4. Only the final line matters to a hook script, because that is the line the verdict lives on.

$ kane-cli run --agent --headless \
    "open the Simple Form Demo, type 'hooks gate' into the message field,
     click Get Checked Value, and assert the page shows 'hooks gate'" \
    --url https://www.testmuai.com/selenium-playground/ | tail -1

{"type":"run_end","status":"passed",
 "summary":"Entered 'hooks gate' in the message field and clicked the Get Checked
            Value button. The page displayed 'hooks gate' as the checked message.",
 "final_state":{"url":"https://www.testmuai.com/selenium-playground/simple-form-demo/",
                "checked_message":"hooks gate"},
 "reason":"Objective completed","duration":41.6,"bifurcated":false,"total_runs":1}

$ echo $?
0

Note the assertion was never written as a selector. The objective said what the page should show, and the run recorded which element it read that value from.

Wiring it to the Stop event takes one handler.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".claude/verify.sh",
            "timeout": 420,
            "statusMessage": "Verifying the change in a real browser"
          }
        ]
      }
    ]
  }
}

Installation is a single npm command, and the tool needs Node.js 18 or higher with Google Chrome on the PATH. The full command reference lives in the Kane CLI introduction documentation.

Get Kane CLI certified for free with TestMu AI

The Exit Code Collision Nobody Warns You About

Here is the trap in wiring any real verifier to a hook, and it is silent when you get it wrong.

Both systems use small integer exit codes, and they do not mean the same things. Kane CLI returns 0 for a pass, 1 for a failed assertion, 2 for an environment error such as an authentication failure or a Chrome crash, and 3 for a timeout.

Claude Code reads 2 as block and treats 1 and 3 as non-blocking noise. Pass the verifier's status straight through and you get the exact inversion of what you wanted.

Kane CLI exitsWhat actually happenedPassed through raw, the hookWhat you wanted
0The check passedAllows the turn to endAllow
1An assertion failed, the feature is brokenLogs a non-blocking error and ends the turn anywayBlock
2Chrome crashed or authentication failedBlocks and tells the agent its code is wrongReport an environment problem, not a code failure
3The run timed outEnds the turn as though nothing was checkedBlock, or retry once

The fix is a translation layer of about ten lines. Never pipe one tool's exit status into another tool's exit contract.

#!/usr/bin/env bash
# .claude/verify.sh - translate Kane CLI status into hook semantics

kane-cli run --agent --headless \
  "sign in as the demo user, open the dashboard,
   assert the revenue widget renders a number and not an error state" \
  > /tmp/kane.ndjson 2>/tmp/kane.err
status=$?

case $status in
  0) exit 0 ;;                                            # verified, let the turn end
  1) echo "Browser check failed. Fix the feature, do not adjust the assertion." >&2
     tail -1 /tmp/kane.ndjson >&2
     exit 2 ;;                                            # 2 is the only code that blocks
  2) echo "Kane CLI could not run: environment or auth problem, not your code." >&2
     exit 1 ;;                                            # non-blocking, surfaces without accusing
  3) echo "Verification timed out before it could decide anything." >&2
     exit 2 ;;
esac

Line four of that case block is the one that matters. Telling an agent its code is broken when Chrome simply failed to launch sends it off to rewrite something that was already correct.

What Hooks Cannot Make Deterministic

A hook guarantees that something runs. It guarantees nothing about what that something concludes.

  • A timeout fails open - the hook is cancelled, its output discarded, no decision rendered, and execution continues as if the rule were never configured.
  • Async handlers do not block - setting async true means the turn moves on without waiting, which is right for notifications and wrong for gates.
  • Matchers see names, not intent - a matcher on Bash cannot distinguish a harmless git status from a destructive command inside the same tool.
  • Model-backed handlers reintroduce variance - a prompt or agent handler fires deterministically and answers probabilistically.
  • Coverage is still yours to define - a Stop hook that checks one flow proves one flow, and says nothing about the other nine.

The timeout case deserves a specific defence, because it is the one that degrades quietly. Log every hook invocation to a file and watch for the day the count stops rising.

The deeper limit is structural rather than technical. A hook you configured checks what you already thought to check, which is the same blind spot we traced in whether coding agents can test their own code.

Note

Note: TestMu AI's Kane CLI returns standard POSIX exit codes, so a GitHub Actions step fails the build with no wrapper script. See the GitHub Actions setup

Which Hooks to Add First

Adding thirty hooks on day one produces a slow agent and a config nobody understands. Four earn their place immediately.

  • A PostToolUse hook on Edit and Write that formats the file, so formatting stops appearing in diffs and in review comments.
  • A PreToolUse hook on Bash that denies your handful of genuinely destructive commands, exiting 2 with the reason on standard error.
  • A PostToolUseFailure hook that returns the real error text, so the agent debugs the actual failure rather than its guess about it.
  • A Stop hook that runs one browser check on the flow you cannot afford to break, translating exit codes rather than forwarding them.

Start with the first one, because it is the cheapest way to confirm your configuration is even being loaded. Add the fourth once the first three are quiet.

Then treat the hook config as code. It sits in the repository, it changes behaviour, and it deserves the same review as anything else that can block a merge.

For the wider pattern this fits into, from the agent loop through to production, our walkthrough of continuous verification for AI-generated code covers each stage in order. And when the work is split across several agents rather than one, Claude Code agent teams covers how the same gates apply per teammate.

Author

...

Bhawana

Blogs: 76

  • Twitter
  • Linkedin

Bhawana is a Community Evangelist at TestMu AI with over 3 years of experience creating technically accurate, strategy-driven content in software testing. She has authored 50+ blogs on test automation, cross-browser testing, mobile testing, and real device testing. She also serves as Product Marketing Manager for Kane CLI, the command-line tool that runs browser automation from the terminal using natural-language flows in a real Chrome browser. Bhawana is certified in KaneAI, Selenium, Appium, Playwright, and Cypress, reflecting her hands-on knowledge of modern automation practices. On LinkedIn, she is followed by 6000+ QA engineers, testers, AI automation testers, and tech leaders.

Reviewer

...

Shahzeb Hoda

Reviewer

  • Linkedin

Shahzeb Hoda is the Associate Director of Marketing and a Community Contributor at TestMu AI, leading strategic initiatives in developer marketing, content, and community growth. With 10+ years of experience in quality engineering, software testing, automation testing, and e-learning, he has authored and reviewed 70+ technical articles on software testing and automation. Shahzeb holds an M.Tech in Computer Science from BIT, Mesra, and is certified in Selenium, Cypress, Playwright, Appium, and KaneAI. He brings deep expertise in CI/CD pipeline automation, cross-browser testing, AI-driven testing practices, and framework documentation. On LinkedIn, he is followed by 3,700+ engineers, developers, DevOps professionals, tech leaders, and enthusiasts.

Add to Google preferred sources

Summarise with AI

Copied to Clipboard!
...

3000+ Browsers. One Platform.

See exactly how your site performs everywhere.

Try it free
...

Write Tests in Plain English with KaneAI

Create, debug, and evolve tests using natural language.

Try for free

Claude Code Hooks FAQs

Did you find this page helpful?

More Related Blogs

TestMu AI forEnterprise

Get access to solutions built on Enterprise
grade security, privacy, & compliance

  • Advanced access controls
  • Advanced data retention rules
  • Advanced Local Testing
  • Premium Support options
  • Early access to beta features
  • Private Slack Channel
  • Unlimited Manual Accessibility DevTools Tests