用 PreToolUse 钩子把 agent 越权的那一步拦下来,顺带留一份能查的日志

2026-09-29 Claude CodeAgent安全钩子

提示词里写”不要删文件""不要访问外网”,这两句没有强制力。模型的目标是完成你给的任务,当限制挡在任务前面,它会自己找路。近几个月公开的事故复盘里,模式都一样:agent 绕过应用层的安全控制,去完成它被指派的任务。

能拦住它的地方在工具调用之前。Claude Code 的 PreToolUse 钩子就是这个位置——它在每次工具调用执行前触发,而且只有这个位置能真正拦下来。

装什么

两件事:一个拒绝危险命令的判断,一条无条件落盘的审计记录。前者防出事,后者保证出事之后你能回答”它到底做了什么”。

配置写在 .claude/settings.json(项目级,可提交)或 ~/.claude/settings.json(用户级):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.sh"
          }
        ]
      },
      {
        "matcher": "Bash|PowerShell",
        "hooks": [
          {
            "type": "command",
            "command": "powershell.exe",
            "args": ["-NoProfile", "-File", "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.ps1"]
          }
        ]
      }
    ]
  }
}

matcher 匹配的是 tool_name。只含字母数字下划线连字符时是精确匹配,Bash|PowerShell 表示两个都要;含其他字符时按 JavaScript 正则处理,比如 mcp__memory__.* 匹配某个 MCP 服务器的全部工具。

${CLAUDE_PROJECT_DIR} 指向项目根。用 args 数组的形式是 exec 调用,不走 shell,能避开一堆引用问题。

macOS / Linux 脚本

.claude/hooks/guard.sh,记得 chmod +x:

#!/bin/bash
set -u
INPUT="$(cat)"
LOG="$HOME/.claude/audit/$(date +%Y-%m-%d).jsonl"
mkdir -p "$(dirname "$LOG")"

# 1) 无条件留痕:整条工具调用原样追加
printf '%s\n' "$INPUT" >> "$LOG"

CMD="$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty')"
[ -z "$CMD" ] && exit 0

BLOCK_RULES=(
  'rm -rf /'
  'rm -rf ~'
  ':(){'
  'mkfs'
  'chmod -R 777 /'
  'curl .*| *(ba)?sh'
  'wget .*| *(ba)?sh'
  'git push .*--force.*(main|master)'
)

for RULE in "${BLOCK_RULES[@]}"; do
  if printf '%s' "$CMD" | grep -Eq -- "$RULE"; then
    echo "Blocked by guard: matched '$RULE'" >&2
    exit 2
  fi
done

exit 0

依赖 jq。Windows 上没有就用下面那份。

Windows 脚本

.claude/hooks/guard.ps1:

$ErrorActionPreference = 'Stop'
$input = [Console]::In.ReadToEnd()

$logDir = Join-Path $env:USERPROFILE '.claude\audit'
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
Add-Content -Path (Join-Path $logDir ("{0}.jsonl" -f (Get-Date -Format 'yyyy-MM-dd'))) -Value $input

$json = $input | ConvertFrom-Json
$cmd  = if ($json.tool_input -and $json.tool_input.command) { $json.tool_input.command } else { '' }

$rules = @(
  'Remove-Item\s+.*-Recurse.*(C:\\|\\$|~)',
  'Format-Volume',
  'Invoke-Expression.*\(Invoke-WebRequest',
  'iwr\s+.*\|\s*iex',
  'git\s+push\s+.*--force.*(main|master)'
)

foreach ($r in $rules) {
  if ($cmd -match $r) {
    [Console]::Error.WriteLine("Blocked by guard: matched '$r'")
    exit 2
  }
}

exit 0

退出码这件事必须搞清楚

这一条写错,前面全白干:

  • exit 2 = 阻塞工具调用。不管你 stdout 打了什么,exit 2 一定会拦下来,连 JSON 里写 "allow" 也覆盖不了。阻塞原因取自 stderr。
  • exit 0 且不打 JSON = 没有决定,工具调用继续走正常权限流程。沉默不等于批准,只是不拒绝。
  • exit 1 不会阻塞。它会被当成非阻塞错误,动作照常继续。Unix 那套”1 表示失败”在这里不适用。
  • 想用 JSON 精细控制(放行、询问、改写参数),stdout 输出 hookSpecificOutput.hookEventName = "PreToolUse" 配 permissionDecision 为 deny / ask / allow,还可以用 updatedInput 改写工具参数。
  • 钩子超时不阻塞。默认超时 600 秒,超时后工具调用照常执行。别把”跑得慢的钩子”当闸门用,要强制就 exit 2。

怎么知道自己做对了

跑通一次不算。下面 5 条全过才算:

  1. 危险命令必须被拦。 让 agent 执行 rm -rf /tmp/anything,它应该被拒,并且把 stderr 里那句 Blocked by guard 显示出来。没有被拒,说明脚本根本没跑——先检查 matcher 和脚本的 +x 权限。
  2. 正常命令必须放行。 npm test、git status 这类要照常执行。如果它们也被拦,说明你的规则写宽了,agent 会开始绕,反而更糟。
  3. exit 1 的对照组要真的不拦。 临时把脚本里的 exit 2 改成 exit 1 跑一次,同一个危险命令应该继续执行。这是验证你理解对了退出码语义的唯一办法。
  4. 日志里有完整的入参。 跑几轮之后打开 ~/.claude/audit/2026-09-29.jsonl,每一行应该是一个完整的工具调用 JSON,含 tool_name、tool_input、session_id、cwd。只有拒绝记录没有全量记录,等于只记了”它没干坏事”,出事那天你查不到东西。
  5. 换个目录仍然生效。 在子目录里启动 Claude Code 再试一次。钩子在会话起始目录、项目根、家目录或临时目录里都可能被解析,路径写死相对路径会在这里翻车——用 ${CLAUDE_PROJECT_DIR} 就是为了避开这个。

几个坑

  • 钩子只管工具调用,管不了 agent 想什么。 它能挡住 rm -rf /,挡不住 agent 换个写法达到同样效果。这套东西的价值是”把明显的越界挡在门外”,不是完整的沙箱。
  • 规则别写太长。 一张 50 条正则的黑名单,agent 会花掉大量上下文去试探边界。10 条以内,只留真正不可逆的那些。
  • 别在钩子里做网络请求。 延迟会叠加到每一次工具调用上,超时还不阻塞,等于给自己造了个假的安全感。
  • stdout 只能是 JSON 或什么都不出。 shell profile 里那些 echo 会污染输出,让 JSON 解析失败,钩子退化成非阻塞错误。
  • 审计日志会记下密钥。 工具调用的输入里可能有 token、连接串。日志目录的权限自己看好,别同步到公开仓库。

顺带一句

一个人干活最容易犯的错,是把”我提醒过它了”当成”我限制住它了”。这两件事之间的距离,今年已经被好几家大厂用真实事故量出来了。

钩子不解决所有问题,但它把这条线从”靠模型的自觉”挪到了”靠进程的边界”。这一步不需要买硬件,20 分钟能装完,装完立刻能验收。

原文:https://code.claude.com/docs/en/hooks