The gate that closes the same way every time
Ask Claude Code to refactor a module and it will read files, propose edits, run commands, and decide — on its own, turn by turn — what to do next. That is the entire value of an agentic coding tool: judgment, applied continuously, without a human approving each step. It is also, for a specific and recurring class of requirement, exactly the wrong property to depend on. Never let this tool write to a credentials file. Always run the formatter after an edit. Log every shell command this session executes, with a timestamp, to a file nobody inside the session can quietly edit. Do not let the agent report a task complete until the test suite has actually run and passed. None of these are requests you want answered by a model’s best effort. They are requests you want answered the same way every time, by something that does not get talked out of it, does not lose the instruction to a full context window, and does not have an opinion.
Claude Code’s answer to that class of requirement is a mechanism called a hook, and it is worth being precise about what a hook actually is, because the precision is the argument. A hook is not a smarter prompt, a stronger system instruction, or a more emphatic bullet point in a project’s memory file. Mechanically, a hook is a shell command — or, in current configurations, an HTTP call, an MCP tool invocation, or a constrained model query — that Claude Code’s harness invokes automatically at a defined point in its execution lifecycle, independent of whether the model wants it to run [1]. That “independent of” is the whole mechanism. The rest of this article works through what that buys an engineering team, and what it costs them, because the two turn out to be the same fact viewed from different sides.
What fires, when, and what it is allowed to say back
Claude Code recognizes several dozen named lifecycle events, and Anthropic’s own documentation groups them usefully by cadence [1]. Some fire once per session: SessionStart when a session begins or resumes, SessionEnd when it terminates, Setup for one-time preparation in scripted or CI contexts. Some fire once per conversational turn: UserPromptSubmit before Claude processes what a user typed, Stop when Claude finishes responding, StopFailure when a turn ends on an API error rather than a normal completion. The largest group fires on every tool call inside the agentic loop itself: PreToolUse before a tool executes and can block it outright, PostToolUse after a tool call succeeds, PostToolUseFailure after one fails, PostToolBatch once a set of parallel tool calls has resolved, and PermissionRequest when a tool call needs a permission decision. A long tail of narrower events covers configuration changes, working-directory changes, context compaction, subagent lifecycle, and MCP elicitation.
A hook is registered against one of these events inside a JSON settings file, scoped to a matcher — a tool name, a list of names, or a regular expression — that narrows which occurrences of the event actually trigger it [1]. When the event fires, Claude Code serializes the relevant context as JSON and pipes it to the hook’s command on standard input: a session identifier, the working directory, which event fired, and, for a tool event, the tool’s name and the exact arguments the model passed it. A PreToolUse hook watching Bash sees the literal command string the model is about to run before it runs. The hook script reads that JSON, does whatever checking or side-effecting it exists to do, and communicates its verdict back through nothing more exotic than an exit code and, optionally, a further block of JSON on standard output.
The exit code is where the mechanism’s determinism actually lives, and it is stricter than most Unix conventions would suggest. Exit code 0 means the hook has no objection; for most events that is genuinely the end of the story, though for a handful — UserPromptSubmit and SessionStart among them — anything written to standard output on a zero exit is added directly to Claude’s context rather than merely logged. Exit code 2 is different: on the events that support blocking, it is the one code that blocks the action through the exit status alone, regardless of any JSON the hook also printed. Anthropic’s reference documentation states plainly that this is not the conventional Unix failure code doing conventional Unix-failure things: “For most hook events, exit code 2 is the only exit code that blocks through the code alone. Without valid JSON on stdout, Claude Code treats exit code 1 as a non-blocking error and proceeds with the action, even though 1 is the conventional Unix failure code. If your hook is meant to enforce a policy, use exit 2.” [1] Every other exit code, absent a JSON payload that says otherwise, is treated as noise and the action proceeds. A hook author who reaches for the familiar exit 1 on a validation failure has, by the documentation’s own framing, written a hook that fails to enforce anything.
Where a hook is registered also matters, because Claude Code layers settings files into a fixed precedence: enterprise-managed policy settings override everything, then command-line overrides, then a project’s local — gitignored, personal, unshared — settings, then the project’s shared settings file, then the user’s own global settings, applied lowest [3]. A hook committed to a project’s shared .claude/settings.json travels with the repository and runs for every teammate who opens it, a fact that will matter considerably later in this article, for reasons that have nothing to do with what the hook was written to accomplish.
Why “ask the model nicely” does not scale
It is worth being honest about the alternative hooks replace, because the alternative is not nothing — it is a plausible-looking instruction in a project’s memory file or system prompt: always run the linter after editing, never touch files under a secrets directory. Those instructions work, in the sense that a competent model reads them and, most of the time, follows them. “Most of the time” is doing a great deal of work in that sentence, and it is now backed by more than intuition.
A 2026 study of behavioral reproducibility in tool-calling agents built a benchmark of nineteen tasks across five categories, paired with ten deterministic simulated tools designed specifically to isolate a model’s own variance from any noise in its environment, then ran each task ten times under identical conditions across several current models [10]. Holding the task, the tools, and the environment fixed, the same model given the same instructions produced between 1.6 and 3.3 distinct tool-call sequences per ten runs, depending on model and task category — not zero, not one. A run whose tool sequence diverged from a reference trace was, in the paper’s own words, “nearly three times more likely to fail,” and the paper’s structural-consistency metric predicted task correctness with a large effect size (d = 0.81) while a separate argument-level consistency metric did not, ruling out the trivial explanation that the model was simply “consistently wrong” [10]. This is not a claim about Claude Code specifically — the paper studies tool-calling agent behavior generally, across multiple model families, and its point is architectural rather than about any one vendor’s tuning. But the architectural point is exactly the one that matters here: a natural-language instruction is processed by the same stochastic machinery that decides everything else the model does on a given turn, and asking that machinery to also be the enforcement mechanism for a hard rule means the hard rule inherits the machinery’s variance.
A command hook does not have this problem, not because it is smarter, but because it is not doing the same kind of thing at all. It is not a request competing for the model’s attention alongside a long context, a user’s latest message, and whatever the model has decided is currently important. It is a program the harness runs and an exit code the harness checks, mechanically, every time the matching event fires. “Deterministic” here does not mean the overall agent session becomes predictable — the model upstream of the hook is exactly as stochastic as it ever was. It means one narrow, specific question — did this file path match a protected pattern, did this exit code equal two — is answered by ordinary software instead of by inference.
Enforcement: a rule that cannot be forgotten
Anthropic’s own documentation walks through this trade with a concrete example rather than an abstract one: a PreToolUse hook, matched to the Edit and Write tools, that reads the target file path from the tool’s arguments, checks it against a list of protected patterns such as .env, package-lock.json, and anything under .git/, and exits with code 2 — printing an explanation to standard error — the moment a match is found [2]. The blocked edit never reaches the file. Claude receives the script’s message as feedback and can adjust its approach, but it cannot simply retry with a differently worded request and succeed, because nothing about the block depended on how the request was phrased.
Contrast that with the identical rule written into a project’s memory file as a plain instruction not to edit that file. Both versions are, in the loose sense, “in the system.” Only one of them is checked by a process that has no context window to lose the instruction from, no earlier message that might implicitly countermand it, and no reason — statistical or otherwise — to weight a recent, emphatic-sounding request over an older, quieter one. Anthropic ships a reference implementation of the same pattern applied to shell commands rather than file paths: an example PreToolUse validator, published in the Claude Code repository, that inspects the literal Bash command string against a small set of rules — for instance, flagging a bare grep in favor of rg — and exits 2 with an explanatory message when a rule matches [4]. The mechanism generalizes cleanly: any check reducible to inspecting JSON on standard input and returning an exit code becomes a rule nothing downstream needs to remember to apply.
The documentation is equally direct about the failure mode on the other side of the same mechanism. Hooks can also auto-approve actions that would otherwise prompt for permission, by writing a JSON decision to standard output rather than merely exiting non-zero, and the guidance is blunt about the risk of getting the scope wrong: “Keep the matcher as narrow as possible. Matching on .* or leaving the matcher empty would auto-approve every permission prompt, including file writes and shell commands.” [2] A determinism mechanism aimed at the wrong target does not become less deterministic for having been misconfigured; it simply enforces the wrong policy with the same reliability it would have enforced the right one.
A record nobody has to remember to keep
Enforcement is one use of the mechanism; the second is logging, and it rests on the identical property. A PostToolUse hook matched to Bash, doing nothing more than extracting the command string with a small JSON query and appending it to a file, produces a complete, contemporaneous record of every shell command a session ran — not a summary the model produced afterward when asked what it did, but a line written by a separate process at the moment the command executed [2]. Anthropic’s documentation describes the same pattern applied to configuration itself: a ConfigChange hook fires whenever a settings or skills file is modified during a session, appending a timestamped entry to an audit log, useful for compliance review or for detecting an unauthorized change to policy while a session is live [2].
The reason this needs to be a hook rather than a habit of asking the model to narrate its own actions is the same reproducibility problem from the previous section, applied to record-keeping instead of rule-following: an agent’s self-report of what it did is generated by the same process that decided what to do, and is not a ground-truth trace of it. A log produced by code that runs independently of the model’s account of itself is a different kind of evidence, closer to a flight recorder than to a passenger’s recollection. Command hooks are not the only route to it: Anthropic’s specification also supports an HTTP handler type that posts the same event JSON to an external endpoint rather than running a local script, explicitly framed as useful for “a shared audit service that logs tool use events across a team” [1], which turns the same per-session mechanism into organization-wide telemetry without changing anything about why it works.
Verification without asking the thing being verified
Not every judgment a hook needs to make is a fixed pattern match, and Anthropic’s hook system has grown handler types that acknowledge this directly. A type: "prompt" hook sends the event’s JSON to a language model — Haiku by default, chosen for cost — and asks for nothing more than a yes-or-no verdict as structured JSON; a type: "agent" hook goes further, spawning a subagent that can read files and run its own commands before returning the same yes-or-no shape, at the cost of a longer timeout and up to fifty tool-use turns [2]. Anthropic’s own worked example uses exactly this pattern on the Stop event, asking a subagent to actually run the test suite and check the results before letting Claude consider a turn finished, rather than accepting Claude’s own claim that it did [2].
This is a meaningfully different use of the mechanism than the pattern-matching cases above — it reintroduces a model into the loop, and a model is exactly the stochastic thing the previous sections have argued cannot be relied on for hard guarantees. What it does not reintroduce is the same model checking its own work from inside the same context. The verifying model or subagent sees only what the hook’s JSON gives it, runs independently of the primary session’s reasoning trace, and its single job is the narrow question it was asked, not the open-ended task the primary agent was pursuing. That separation — a second, narrowly scoped process checking a claim, rather than the original process attesting to its own claim — is doing real work here, even though neither side of it is individually deterministic. Anthropic’s documentation flags agent-based hooks as experimental and recommends command hooks for production use wherever a check can be reduced to one [2], which is itself a tacit ranking: determinism where it can be had, a constrained second opinion where it cannot.
The permission a hook actually holds
Everything above describes what hooks are for. What they are, mechanically, is the same fact turned around, and Anthropic states it as plainly as a vendor states anything about its own product’s risk: “Command hooks execute shell commands with your full user permissions. They can modify, delete, or access any files your user account can access. Review and test all hook commands before adding them to your configuration.” [1] That sentence is not a hedge. A hook is not sandboxed relative to the person running Claude Code, is not restricted to files inside a project directory, and is not subject to the same permission prompts that gate the model’s own tool calls — the documentation notes that a PreToolUse hook fires and can block before any permission-mode check, in every mode including one launched with --dangerously-skip-permissions, precisely because the hook is meant to be a floor the user cannot casually bypass [1]. The same design that makes a hook trustworthy as an enforcement mechanism against the model’s own behavior makes it, by construction, unrestricted with respect to everything else on the machine.
Anthropic’s documented security guidance for hook authors is a short, specific list rather than a general warning: validate and sanitize any input before acting on it, quote shell variables ("$VAR", never a bare $VAR), check explicitly for path-traversal sequences in file paths, use absolute paths when invoking scripts, and avoid ever touching known-sensitive files such as .env or anything under .git/ from inside a hook itself [1]. Every item on that list is standard shell-scripting hygiene, which is itself the point: a hook is an ordinary shell script with an unusual trigger, and it inherits every ordinary way a shell script can go wrong.
What happened when the trigger fired first
Standard shell-scripting hygiene assumes the person who wrote the script and the person running it are the same trust boundary. A hook configured in a project’s shared settings file breaks that assumption, because that file can arrive inside a git repository written by someone else — and Check Point Research demonstrated, in a report disclosed to Anthropic and later published alongside two assigned CVEs, exactly what follows from that [5]. Researchers Aviv Donenfeld and Oded Vanunu found that a malicious .claude/settings.json committed to a repository could define hooks that executed automatically when a developer simply opened that project in Claude Code, firing at session start, before the tool’s own trust dialog had been shown or accepted [5]. Their own framing of the underlying shift is worth quoting directly: “configuration files effectively become part of the execution layer,” and, as a consequence, “the risk is no longer limited to running untrusted code – it now extends to opening untrusted projects” [5].
The disclosure timeline is a matter of public record. Check Point reported the hooks-based execution issue to Anthropic on 21 July 2025; Anthropic shipped a fix on 26 August 2025; the issue was published as CVE-2025-59536 on 3 October 2025, and the U.S. National Vulnerability Database assigned it a CVSS 3.1 base score of 8.8, rated high severity, describing a flaw in versions of Claude Code prior to 1.0.111 that allowed code contained in a project to execute before a user approved the startup trust dialog [6]. A related second issue, tracked as CVE-2026-21852 and disclosed in the same research, showed that a project’s configuration could redirect Claude Code’s API traffic to an attacker-controlled endpoint through the ANTHROPIC_BASE_URL setting, causing the tool to send requests — potentially including a user’s own API credentials — before any trust prompt was shown; the fix shipped in version 2.0.65 in January 2026, and Anthropic acknowledged the underlying behavior directly, stating that “Claude Code would issue API requests before showing the trust prompt, including potentially leaking the user’s API keys” [7]. Independent reporting on the disclosure noted that because API access and shared-workspace access were not cleanly separated, a single compromised credential could expose an entire team’s files, not just the one developer’s [8].
Both fixes are, mechanically, about sequencing rather than about hooks being disabled or weakened as a feature — a SessionStart hook still fires at session start, and a project can still commit hooks that run automatically for a teammate who opens it. What changed is where the trust dialog sits relative to that firing, and how tightly outbound API configuration is gated before a user has explicitly accepted a project. It is a narrow, specific fix for a narrow, specific ordering bug, not a redesign of the trust model hooks depend on.
The trifecta arrives without a prompt injection
Security researcher Simon Willison’s widely cited account of agent risk — since applied broadly across the industry, not written about Claude Code — describes a “lethal trifecta”: an agent that combines access to private data, exposure to content it did not itself generate, and a channel for communicating externally is one where a single piece of untrusted content can cause data to leave the system regardless of what the agent’s operator intended [9]. The usual illustration of the pattern involves prompt injection: a webpage or document the agent is asked to summarize contains hidden instructions, and the model, unable to reliably distinguish an instruction that arrived in its context from one its operator gave it, follows the hidden one.
Applying that framework to the hooks vulnerabilities above is analysis, not a claim either Willison or Anthropic has made, but the structural fit is close enough to be worth stating plainly: a malicious hook committed to a shared settings file is a version of the same trifecta with one property changed. The untrusted content is still untrusted — a configuration file arriving via git clone from someone else’s repository — and the external-communication channel is still there, whether as an outbound shell command or a redirected API endpoint. What is different is that no prompt injection, and therefore no dependence on the model’s judgment at all, is required to trigger it: a command hook does not ask the model anything before it runs. The trifecta collapses into an even narrower and more mechanical version of itself, because the piece that used to require fooling a language model has been replaced by a piece that only requires a developer to open a folder.
Fixing the order, not just the warning
The organizational answer sits at the same layer as the mechanism itself: the settings hierarchy. Enterprise-managed policy settings, delivered centrally and unable to be overridden by a project’s own configuration, support a specific switch — allowManagedHooksOnly — that restricts execution to hooks defined in managed settings, an SDK, or plugins an administrator has explicitly force-enabled, blocking user-, project-, and other plugin-defined hooks outright [3]. For a team that has decided it does not trust arbitrary repositories to bring their own hooks, this is the actual fix available today, and it is a policy decision layered on top of the mechanism rather than a change to the mechanism’s fundamental permissions.
For everyone else, the practical guidance converges on the same short list already quoted from Anthropic’s own documentation: treat a hook exactly as what it is, a script with unrestricted shell access and an unusual trigger, and apply the same review a team would apply to any code with that description before letting a repository’s own configuration decide what runs on a machine that opens it [1]. That is a smaller ask than it sounds. The property that makes hooks valuable — a rule enforced by code instead of by request — is exactly the property a reviewer can check once, in the hook script itself, rather than needing to re-verify every time the model runs.
What this predicts, and what would prove it wrong
Two forecasts, clearly separated from the sourced material above and given a horizon of two years from publication.
First. Hook-related CVEs will continue to cluster around trust-boundary sequencing — where a trigger fires relative to a consent prompt or a credential being loaded — rather than around the hook mechanism’s fundamental permissions, because Anthropic’s own security guidance already documents the permission model as intentional rather than as a bug. Assumption: the shell-access design is treated as a stable requirement, not something scheduled for removal. Disconfirmed if a future Claude Code release sandboxes command hooks by default, restricting their filesystem or network access below the invoking user’s own permissions.
Second. Managed-settings controls that let organizations restrict which hooks may run — the allowManagedHooksOnly pattern — will see the fastest adoption growth of any hooks-adjacent feature among teams running Claude Code against externally sourced repositories, because it is the only current control that removes the trust decision from individual developers entirely. Observable indicator: documentation and enterprise guidance increasingly presenting managed hook restrictions as a default recommendation rather than an advanced option. Disconfirmed if adoption instead concentrates on per-repository trust prompts and warnings, with centralized hook allowlisting remaining a niche configuration two years out.
What a hook is, restated
A hook is a shell command, or one of a small number of typed alternatives, that Claude Code’s harness runs automatically at a named point in its lifecycle, and reports back to with nothing more than an exit code and an optional block of JSON. That mechanical narrowness is the entire source of its value: a rule enforced this way cannot be argued out of, forgotten under context pressure, or selectively applied depending on how a request was phrased, because nothing about how it runs depends on the model’s judgment at all. It is also, for the identical reason, a script running with the full permissions of whoever is running Claude Code, triggered by configuration that a project’s own repository can supply — and the two documented vulnerabilities examined above are what happens when that configuration arrives from somewhere the user did not choose to trust. Neither fact cancels the other. A mechanism built to be the one thing in an agentic system that does not depend on judgment is, by the same design, a mechanism that does not ask permission either, and an engineering team adopting it needs both halves of that sentence, not just the half that solves its immediate problem.