Exit Code 2: How Claude Hooks Turn Agentic Rules Into Runtime Barriers

Exit Code 2: How Claude Hooks Turn Agentic Rules Into Runtime Barriers

退出代码 2:Claude Hooks 如何将智能体规则转化为运行时屏障

This article was originally published on EthereaLogic.ai. The first article in this series introduced the five-layer governance stack and made a single load-bearing claim: the layers that live in documents are necessary, and the layers that run as code are what make the system trustworthy. This article goes inside the highest-leverage code layer — runtime enforcement via Claude Hooks — and shows what one looks like at the level of detail an engineering team would actually need to build it.

本文最初发布于 EthereaLogic.ai。本系列的第一篇文章介绍了五层治理架构,并提出了一个核心论点:存在于文档中的层级是必要的,而以代码形式运行的层级才是系统可信的保障。本文将深入探讨最高效的代码层——通过 Claude Hooks 进行运行时强制执行,并展示工程团队在实际构建时所需的细节水平。

The thesis of the first article was that an instruction in CLAUDE.md or AGENTS.md can be ignored, reasoned around, or context-windowed out of an agent’s working memory, but a hook that exits with status 2 cannot. The thesis of this article is that turning the abstract idea of a hook into a guard that holds under real subagent traffic is its own engineering discipline — one with a small but distinct set of patterns, failure modes, and tests. Most teams who reach the hook layer underestimate that engineering discipline. The empirical evidence in this article comes from one of those underestimations.

第一篇文章的论点是:CLAUDE.md 或 AGENTS.md 中的指令可能会被忽略、被绕过,或者因为上下文窗口限制而被挤出智能体的工作记忆,但以状态码 2 退出的 Hook 是无法被绕过的。本文的论点是:将 Hook 的抽象概念转化为能在真实子智能体流量下生效的防护机制,本身就是一门工程学科——它包含一套虽小但独特的模式、故障模式和测试方法。大多数触及 Hook 层的团队都低估了这门工程学科。本文的实证证据就来自其中一次低估。

Layer 4 sits between the agent’s intent and the tool call’s effect. The hook receives the tool payload as JSON on stdin, decides allow or block, and on block exits with status 2 — the only exit code the Claude Code harness treats as a hard refusal whose reason is surfaced back to the model.

第 4 层位于智能体的意图和工具调用的效果之间。Hook 通过标准输入(stdin)接收 JSON 格式的工具负载,决定允许或拦截;如果拦截,则以状态码 2 退出——这是 Claude Code 工具链唯一视为“硬拒绝”并会将原因反馈给模型的退出代码。

The Failure Mode: Documents Cannot Close

故障模式:文档无法闭环

Every team that operates an agentic coding workflow eventually meets the same failure mode: a rule that exists in the project’s documentation, in the agent’s instructions, in the user’s persistent memory, and is still violated by a subagent operating at speed. The rule is not unclear. The rule has not changed. The agent has read it before. None of that prevents the next violation, because none of those locations are points of execution. They are points of intent. The tool call is the point of execution.

每个运行智能体编码工作流的团队最终都会遇到同样的故障模式:一条规则明明存在于项目文档、智能体指令和用户的持久化记忆中,却仍然被高速运行的子智能体违反。规则并不模糊,规则也没有改变,智能体也曾阅读过它。但这些都无法阻止下一次违规,因为这些位置都不是执行点。它们只是意图点。工具调用才是执行点。

In the GovForge project on April 11, 2026 at 19:56 Pacific time, an automated /implement subagent produced commit 3f3b7f9 and pushed it directly to main. The rule “no direct pushes to main” was written in AGENTS.md, in the project Constitution as principle P1 (“protected branches are hard boundaries”), and in user memory as feedback_no_direct_push_main.md. Every one of those locations had been read by the subagent’s parent context.

在 GovForge 项目中,太平洋时间 2026 年 4 月 11 日 19:56,一个自动化的 /implement 子智能体生成了提交 3f3b7f9 并将其直接推送到 main 分支。关于“禁止直接推送到 main”的规则已写在 AGENTS.md 中、项目宪章的原则 P1(“受保护分支是硬边界”)中,以及用户记忆的 feedback_no_direct_push_main.md 中。子智能体的父上下文已经读取了所有这些位置。

The push happened anyway, because three enabling conditions stacked: the project’s pre-tool-use.js hook existed in the repository as an empty stub copied from a sibling project’s scaffold; .claude/settings.json had no PreToolUse registration, so even a non-empty hook would not have been invoked; and the operator’s home-level Claude Code settings had skipDangerousModePermissionPrompt: true, which suppressed the confirmation dialog that would otherwise have caught the destructive operation.

推送依然发生了,因为三个促成条件叠加在一起:项目中存在一个从兄弟项目脚手架复制过来的空 pre-tool-use.js Hook 存根;.claude/settings.json 中没有 PreToolUse 注册,因此即使 Hook 非空也不会被调用;操作员在主目录下的 Claude Code 设置中开启了 skipDangerousModePermissionPrompt: true,这禁用了本可以拦截该破坏性操作的确认对话框。

Forty-nine minutes later, commit b404fbe replaced the empty stub with a real protected-branch guard and registered it under PreToolUse:Bash in .claude/settings.json. The same class of attempt has exited with status 2 every time since. The instruction existed. The instruction was not enough. The hook is the enforcement.

49 分钟后,提交 b404fbe 将空存根替换为一个真正的受保护分支防护程序,并在 .claude/settings.json 中将其注册为 PreToolUse:Bash。自那以后,同类尝试每次都以状态码 2 退出。指令存在,但指令是不够的。Hook 才是强制执行手段。

What a PreToolUse Hook Actually Is

什么是 PreToolUse Hook

A Claude Code hook is an executable that the harness invokes around tool calls. The protocol is small enough to describe in three sentences. The harness writes a JSON payload to the hook’s stdin describing the tool name and the tool input. The hook decides whether to allow or block, and exits with status 0 to allow or status 2 to block. On a block, the harness reads the hook’s stderr and surfaces it to the model as the refusal reason, which gives the agent a written explanation it can reason against on the next turn.

Claude Code Hook 是工具链在调用工具前后执行的可执行程序。其协议非常简洁,三句话即可描述:工具链将描述工具名称和输入的 JSON 负载写入 Hook 的标准输入;Hook 决定允许或拦截,以状态码 0 表示允许,状态码 2 表示拦截;拦截时,工具链会读取 Hook 的标准错误输出(stderr)并将其作为拒绝原因反馈给模型,这为智能体提供了可以在下一轮推理中参考的书面解释。

That last detail matters more than it sounds. The hook is not silent enforcement. It is enforcement that explains itself in natural language directly to the model that is now blocked, which means the same hook closes the failure mode and teaches the agent what to do instead — usually within the same turn.

最后一个细节比听起来更重要。Hook 并非静默执行。它是一种直接向被拦截的模型用自然语言解释原因的强制手段,这意味着同一个 Hook 既关闭了故障模式,又教会了智能体该如何做——通常在同一轮对话内完成。

In the GovForge guard, the stderr message names the policy file the rule comes from, names the specific sub-command that was blocked, names the reason, and tells the model exactly what fix to attempt: create a chore/feat/fix branch, retry the commit there, and open a PR. After the hook lands, agents that hit the guard typically resolve it on the next message without operator intervention.

在 GovForge 的防护程序中,stderr 消息会指明规则来源的策略文件、被拦截的具体子命令、原因,并告诉模型确切的修复方案:创建一个 chore/feat/fix 分支,在那里重试提交,并开启一个 PR。在 Hook 部署后,遇到防护的智能体通常会在下一条消息中自行解决,无需人工干预。

The registration is a small block in .claude/settings.json: 注册方式在 .claude/settings.json 中仅是一个小代码块:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/pre-tool-use.js"
          }
        ]
      }
    ]
  }
}

That is the entire interface. The matcher selects which tool calls the hook inspects — Bash here, because every git operation flows through the Bash tool. The command names the executable. There is no SDK. There is no daemon. There is one short script that runs, decides, and exits.

这就是全部接口。matcher 选择 Hook 检查哪些工具调用——这里是 Bash,因为所有的 git 操作都通过 Bash 工具流转。command 指定了可执行文件。没有 SDK,没有守护进程。只有一个简短的脚本在运行、决策并退出。

What the Guard Has to Catch

防护程序需要捕获什么

The hard part is not the protocol. The hard part is that “block direct commits and pushes to main” turns out to have a much larger surface than it appears. The naive implementation inspects the Bash command for the literal token sequence git commit or git push, checks for the word main, and exits 2 if both are present. That guard fails on day one against a competent agent.

难点不在于协议,而在于“阻止直接提交和推送到 main”的攻击面远比看起来要大。天真的实现方式是检查 Bash 命令中是否包含字面字符串 git commitgit push,并检查是否包含 main,如果两者都存在则退出 2。这种防护在面对有能力的智能体时,第一天就会失效。

Within the GovForge guard’s 320 lines, six categories of bypass are closed: 在 GovForge 防护程序的 320 行代码中,关闭了六类绕过方式:

  • Refspec rewriting. git push origin main is the obvious form. git push origin HEAD:main and git push origin +refs/heads/master are the same operation in different syntax. The guard has to parse the refspec, strip a leading +, split on the colon, normalize refs/heads/<branch>, and check the destination side against a PROTECTED_BRANCHES set. The source side is irrelevant; only where the commit lands on the remote matters.

  • 引用规范(Refspec)重写。 git push origin main 是显而易见的形式。git push origin HEAD:maingit push origin +refs/heads/master 是语法不同但操作相同的指令。防护程序必须解析引用规范,去除前导的 +,按冒号分割,规范化 refs/heads/<branch>,并将目标端与 PROTECTED_BRANCHES 集合进行比对。源端无关紧要,只有提交最终到达远程的哪个位置才重要。

  • Implicit refspec on a protected branch. git push with no refspec can update the current or upstream branch depending on push.default and the configured upstream.

  • 受保护分支上的隐式引用规范。 不带引用规范的 git push 可能会根据 push.default 和配置的上游分支更新当前分支或上游分支。