本文经原作者授权转载,版权归原作者所有。原作者:实践哥MinLi(@MinLiBuilds)。查看原文 →


上周四晚上十一点多,我正准备关电脑。

瞄了一眼 git diff,发现 Claude 把我的 .env 改了。加了一行调试变量。本身没啥——但我当时连着生产库。

导读:用了仨月 Claude Code,之前天天帮它擦屁股。后来发现 Hooks 之后 10 分钟设好,之前手动检查的活全自动了。回不去了。原文 下面是翻译+实测。

你有没有过这种经历:让 Claude Code 做某件事,它就是不做?

你说"格式化代码"——它没做。你说"别碰那个文件"——它碰了。

你说"完成前跑一下测试"——它忘了。

原因在于,CLAUDE.md 只是一个建议。

Claude 读了它,大概 80% 的时间会遵守。但 Hooks 不同。它们是自动触发的动作——每当 Claude 编辑文件、执行命令或完成任务时,都会自动执行。

下面 8 个 Hooks,直接复制到 settings.json,设完就忘。

Article image

30 秒搞懂原理

Hooks = 绑在 Claude Code 动作上的脚本。设一次,后台一直跑。

  • PreToolUse:Claude 动手之前。exit code 2 = 拦住。门卫。
  • PostToolUse:Claude 动手之后。跑格式化、跑测试。质检员。
hook在哪:
.claude/settings.json         项目级(git 同步全组)
~/.claude/settings.json       用户级(所有项目)
.claude/settings.local.json   本地(不进 git)

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

Article image

1. 自动格式化

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null; exit 0"
          }
        ]
      }
    ]
  }
}

Python 换 black,Go 换 gofmt,Rust 换 rustfmt。

之前在 CLAUDE.md 里写了"永远运行 Prettier"。管用,但偶尔会忘。偶尔忘一次就够你在 PR 里丢一次脸了。

装完之后再没出过格式问题。所有项目标配,没有之一。

2. 拦截危险命令

Claude 真的会跑 rm -rf。概率低,但概率低跟零之间,隔着你的生产数据库。

创建.claude/hooks/block-dangerous.sh:

Create .claude/hooks/block-dangerous.sh:
#!/usr/bin/env bash
set -euo pipefail
cmd=$(jq -r '.tool_input.command // ""')

dangerous_patterns=(
  "rm -rf"
  "git reset --hard"
  "git push.*--force"
  "DROP TABLE"
  "DROP DATABASE"
  "curl.*|.*sh"
  "wget.*|.*bash"
)

for pattern in "${dangerous_patterns[@]}"; do
  if echo "$cmd" | grep -qiE "$pattern"; then
    echo "Blocked: '$cmd' matches dangerous pattern '$pattern'. Propose a safer alternative." >&2
    exit 2
  fi
done
exit 0
Then add to your settings.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ]
  }
}

再往 settings.json 添加:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ]
  }
}

exit code 2 = 出错拦截,0 = 放行,这个很简单,告诉 Claude 原因出错了,并让它换方案。

装了这个之后晚上终于能睡踏实了。之前有段时间做梦都梦到 Claude 跑了个什么奇怪的命令,醒来第一件事就是打开终端检查。你说荒不荒谬,一个程序员被自己的工具搞出 PTSD 了。

3. 敏感文件上锁

Claude 重构的时候顺手改了 package-lock.json。后面两个小时我都在跟诡异的依赖冲突搏斗,最后发现就是它动了不该动的文件。

#!/usr/bin/env bash
set -euo pipefail
file=$(jq -r '.tool_input.file_path // .tool_input.path // ""')

protected=(
  ".env*"
  ".git/*"
  "package-lock.json"
  "yarn.lock"
  "*.pem"
  "*.key"
  "secrets/*"
)

for pattern in "${protected[@]}"; do
  if echo "$file" | grep -qiE "^${pattern//\*/.*}$"; then
    echo "Blocked: '$file' is protected. Explain why this edit is necessary." >&2
    exit 2
  fi
done
exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

停一下。

这个 Hook 干的事说白了就一个字:防。

有人会觉得矛盾——你用 AI 写代码,又防着它?你花钱请它来帮忙,结果第一件事是给它画红线?

但我越想越觉得,这可能就是我们这代程序员跟 AI 打交道的常态了。你离不开它,但你也不敢完全放手。就像你第一次把车钥匙交给刚拿驾照的孩子——你坐在副驾上,脚一直悬在刹车上方。

区别在于,Hooks 让你可以把脚从刹车上挪开。因为刹车已经自动化了。

顺便说:不会拖慢 Claude。毫秒级的事,实测过。

好,防灾到此为止。下面聊提效。

说真的,我一开始也觉得没必要搞这些。我的项目又不大,Claude 又挺聪明的。直到那天晚上 .env 的事。10 分钟设好这些 Hook,之后它们拦住的那一次事故,可能值你一整天。

顺便问一句——你用 Claude Code 多久了?有没有被它坑过?我特别好奇有多少人跟我有类似经历。

4 & 5. 测试这件事,我踩了两次坑才学乖

第一次:Claude 改完代码说"搞定了"。我信了。20 分钟后要 commit,测试全红。

第二次:Claude 写完功能一激动直接建了 PR。CI 全红。Reviewer 留了俩字:"认真?"

那个 Reviewer 是我 lead。当时恨不得找个地缝钻进去。

两个坑,两个 Hook。

改完代码自动跑测试:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npm run test --silent 2>&1 | tail -5; exit 0"
          }
        ]
      }
    ]
  }
}

tail -5 只留最后几行,别让 200 行日志撑爆 context。

建 PR 前强制测试通过:

.claude/hooks/require-tests-for-pr.sh:

#!/usr/bin/env bash
set -euo pipefail

if npm run test --silent; then
  exit 0
else
  echo "Tests are failing. Fix all test failures before creating a PR." >&2
  exit 2
fi
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__github__create_pull_request",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/require-tests-for-pr.sh"
          }
        ]
      }
    ]
  }
}

Claude Code 创造者 Boris Cherny 说过,这种实时反馈能让输出质量翻 2-3 倍。我自己的体感是确实快了不少,Claude 修 bug 不再是"盲猜"了。

6. 自动 Lint + 7. 审计日志

这俩不像前面几个有什么惊天大坑,但不装你会在小地方持续被恶心到。

Lint:有一次 merge 之后队友给我发了个白眼 emoji,一个字没说。我看了半天才明白——ESLint 没跑,一堆规则违反。那种被无声审判的感觉,比被骂还难受。加上了:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx eslint --fix $(jq -r '.tool_input.file_path') 2>&1 | tail -10; exit 0"
          }
        ]
      }
    ]
  }
}

跟 #1 搭配:Prettier 管格式,ESLint 管规则。到手就是干净的。

日志:上个月 Claude 在某次会话里覆盖了一个配置文件,我三天后才发现。问题是我不知道是哪次会话、哪条命令干的。翻了一下午 commit 也没头绪。从那以后我装了这个:

.claude/hooks/log-commands.sh:

#!/usr/bin/env bash
set -euo pipefail
cmd=$(jq -r '.tool_input.command // ""')
printf '%s %s\n' "$(date -Is)" "$cmd" >> .claude/command-log.txt
exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/log-commands.sh"
          }
        ]
      }
    ]
  }
}

.claude/command-log.txt 加 .gitignore。出事打开,精确到秒。

8. 自动 commit

放最后是因为这个最有争议。在我团队引发了一场小战争。

反对派说:自动 commit?这是对 git 的亵渎。commit message 呢?review 呢?

我说:你是想要每个任务一个干净 commit,还是要一天结束时那个叫 "various changes" 的垃圾山?

吵了半天,最后怎么着?两派各用各的,谁也没服谁。到今天还是这样。

.claude/hooks/auto-commit.sh:

#!/usr/bin/env bash
set -euo pipefail
git add -A
if ! git diff --cached --quiet; then
  git commit -m "chore(ai): apply Claude edit"
fi
exit 0
{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/auto-commit.sh"
          }
        ]
      }
    ]
  }
}

配合 claude -w feature-branch(worktrees),每个任务一个隔离分支。用过之后 git 历史第一次变得可读了。

你可以不喜欢。但建议先试一周再下结论。

完整 settings.json

这里把所有内容放一起,方便复制。

Article image

复制到 .claude/settings.json,脚本放 .claude/hooks/,chmod +x .claude/hooks/*.sh,commit。全组同步。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/block-dangerous.sh" },
          { "type": "command", "command": ".claude/hooks/log-commands.sh" }
        ]
      },
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/protect-files.sh" }
        ]
      },
      {
        "matcher": "mcp__github__create_pull_request",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/require-tests-for-pr.sh" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          { "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null; exit 0" },
          { "type": "command", "command": "npx eslint --fix $(jq -r '.tool_input.file_path') 2>&1 | tail -10; exit 0" }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/auto-commit.sh" }
        ]
      }
    ]
  }
}

今天先装 #1 和 #2。 就这俩,最常见的翻车已经干掉一大半。

Article image
实践哥:翻译完这里我突然想到一件事。
一年前我还在嘲笑那些不会用 AI 的同事。现在呢?我每天的工作流是:让 AI 写代码,然后检查 AI 写的代码,然后给 AI 装护栏防止它写出问题代码。你的 title 还是 Software Engineer,但你每天干的事越来越像 AI Babysitter。
Hooks 就是这个转变里唯一让我觉得舒服的部分——至少 babysitting 这件事,也可以自动化。
这个变化好不好?老实说我也没想明白。但它已经在发生了,不管你接不接受。
最后,那个问题我是真想听你的答案:该不该让 AI 自动 commit?
我见过两派人为这事差点闹翻。
A. 该,反正 squash merge 时会压掉,过程 commit 无所谓
B. 不该,commit 是工程师最后的体面
C. 我有更野的操作——说来听听