Claude Code 实战入门(七):我每天真用的十个 Hooks,附完整代码

从参考仓库的 100 个脚本里挑出 10 个,每一个都给:它做什么、JS 全文、settings.json 的接线方式、它会咬人的地方。PreToolUse 守安全,PostToolUse 做卫生,那些朴素但救命的脚本。

第 5 篇是 Hooks 的概念巡讲,这一篇是田野手册。在那个 100 脚本的参考仓里,有 10 个在我每个正经项目里都会进配置。下面把这 10 个一一过完,附上代码。

例子假设 Node 18+,脚本放在 ./hooks/ 下,chmod +x,在 .claude/settings.json 里这样接线:

1
2
3
4
5
6
7
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Read|Grep", "hooks": [{ "type": "command", "command": "node ./hooks/block-env-read.js" }] }
    ]
  }
}

1. block-env-read——保护密钥

ROI 最高的那一个。拦住 ReadGrep 去碰 .envid_rsacredentials.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/usr/bin/env node
const chunks = [];
for await (const c of process.stdin) chunks.push(c);
const t = JSON.parse(Buffer.concat(chunks).toString());
const p = t.tool_input?.file_path || t.tool_input?.path || "";
const sensitive = ['.env', 'credentials.json', 'secrets.yaml', 'id_rsa', '.aws/credentials'];
if (sensitive.some(s => p.includes(s))) {
  console.error(`Blocked: ${p} matches a sensitive file pattern.`);
  process.exit(2);   // 2 = 拦截,仅 PreToolUse 有效
}
process.exit(0);

接到 Read|Grep|MultiEdit|Edit|Write 上。Exit code 2 在 PreToolUse 里拦截调用;stderr 的文本会被回喂给模型,让它知道为什么被拦。

2. bash-blacklist——挡住 rm -rf /

最常见的脚踝。PreToolUse 接 Bash

1
2
3
4
5
6
const cmd = JSON.parse(/* stdin */).tool_input?.command || "";
const banned = [/rm\s+-rf\s+\/(\s|$)/, /:\(\)\s*\{.*:\|:.*\}\s*;:/, /mkfs\./, /dd\s+if=.*of=\/dev\//];
if (banned.some(re => re.test(cmd))) {
  console.error("Blocked: dangerous command pattern.");
  process.exit(2);
}

正则故意写得短。长黑名单一旦误伤就会被你自己关掉。

3. bash-whitelist——给挨着生产的机器

反过来——给那种碰生产的仓库。只放过显式列出的二进制:

1
2
3
const allow = new Set(['ls','cat','grep','rg','git','npm','node','python','python3','curl','jq','head','tail']);
const first = (cmd.trim().split(/\s+/)[0] || "").split('/').pop();
if (!allow.has(first)) { console.error(`Not on allowlist: ${first}`); process.exit(2); }

白名单赢在黑名单输的地方:你不可能意外放过新东西。

4. block-git-push——不允许偷推

我从来不希望 Claude 不打招呼就 push。PreToolUse 接 Bash

1
2
3
4
if (/^\s*git\s+push\b/.test(cmd) || /git.*push.*--force/.test(cmd)) {
  console.error("Blocked: git push must be human-initiated.");
  process.exit(2);
}

错了的代价比"我自己敲 git push"的麻烦大得多得多。

5. format-on-write——Prettier 走 PostToolUse

PostToolUse 接 Write|Edit|MultiEdit

1
2
3
4
const path = t.tool_input?.file_path;
if (path && /\.(ts|tsx|js|jsx|json|md|css)$/.test(path)) {
  require('child_process').execSync(`npx prettier --write ${path}`, { stdio: 'inherit' });
}

PostToolUse 在编辑之后跑,所以 exit 2 拦不住任何东西。它的目的是卫生,不是策略。

6. test-on-edit——快速失败

PostToolUse 接 Edit|MultiEdit,只对源码生效:

1
2
3
4
if (/\/(src|lib)\/.*\.(ts|js)$/.test(path)) {
  try { require('child_process').execSync('npm run -s test:related -- ' + path, { stdio: 'inherit' }); }
  catch { console.error("Tests failed after edit"); process.exit(1); }
}

Exit code 1 把失败上抛给模型,它看到测试输出就会再试一次。这一个 hook 是唯一让 Claude 在我仓库里写出越来越好代码的那个。

7. backup-before-edit——安全网

PreToolUse 接 Edit|Write|MultiEdit

1
2
3
4
const fs = require('fs');
if (fs.existsSync(path)) {
  fs.copyFileSync(path, `/tmp/cc-backup-${Date.now()}-${path.replace(/\//g,'_')}`);
}

很便宜的保险。我只从 /tmp 救回来过两次文件,每次都值一年的 cron 工资。

8. log-tool-calls——可观测性

PostToolUse 接 *

1
2
3
const fs = require('fs');
const line = JSON.stringify({ ts: Date.now(), tool: t.tool_name, input: t.tool_input }) + "\n";
fs.appendFileSync('.claude/tool-calls.jsonl', line);

平时不会去看这个文件。需要看的那一天,你会感谢它在那儿。

9. read-before-write——禁止盲改

PreToolUse 接 Edit|MultiEdit

1
2
3
4
const fs = require('fs');
const seen = JSON.parse(fs.existsSync('.claude/seen.json') ? fs.readFileSync('.claude/seen.json') : "{}");
if (t.tool_name === 'Read') { seen[path] = Date.now(); fs.writeFileSync('.claude/seen.json', JSON.stringify(seen)); process.exit(0); }
if (!seen[path]) { console.error(`Blocked: ${path} was not Read in this session.`); process.exit(2); }

强制模型在改文件前先读它。专治那种"模型按它的先验改而不是按文件当前状态改"的隐蔽 bug。

10. work-hours-only——人道的边界

PreToolUse 接 Bash

1
2
3
4
5
const h = new Date().getHours();
if (h < 9 || h >= 22) {
  console.error("Outside work hours. Refuse.");
  process.exit(2);
}

我把它装在专门处理下班后 ping 的那台机器上。如果 bot 在凌晨 2 点想做破坏性操作,那基本就是误触。

把它们绑到一起的三条规矩

我自己摔出来的三条:

  1. PreToolUse 管策略,PostToolUse 管卫生。 别想在 PostToolUse 里撤销什么——副作用已经发生了。
  2. stderr 是反馈,exit code 是判决。 Exit 2 拦截(仅 PreToolUse 有效)。stderr 里的内容会原文喂给 Claude。两个一起用。
  3. Hooks fail closed。 一个写崩的 hook 会拦住你所有的工具调用。接线之前用 echo '{"tool_name":"Read","tool_input":{"file_path":"/tmp/x"}}' | node hook.js 先测一下。

10 个 Hooks 听起来不多,足够把一个 Yolo 模式的 session 变得讲规矩。

翻完了?

去 GitHub 关注一下,新一篇通常隔一周就到。

GitHub