内容来源:Bartek Paczesny。https://dev.paczesny.pl/blog/en/how-to-setup-claude-code-hooks-skills
原题:Claude Code Hooks, Skills and Subagents: a Practical Setup
原发布时间:2026-07-03

准确性说明(核对于 2026-07-22):Command 与 HTTP Hooks 由运行框架分发,但可能失败或超时;基于 Prompt 和 Agent 的 Hooks 则会使用模型判断。示例 Stop Hook 会在 stop_hook_active 为 true 时放行下一次 Stop,因此不能从物理上阻止所有存在未推送内容的退出;当前默认行为还会在连续阻止八次且没有进展后强制覆盖。其 Git 检查忽略未跟踪文件,在缺少上游数据时会把领先提交数视为零,也无法证明部署确实成功。跨项目防护不是沙箱,工作树隔离和权限控制能提供更强的边界。本地 ~/.claude 配置并非在远程浏览器或 Cowork 会话中普遍可用。请参阅 HooksSkills子智能体参考资料。文中的效果百分比与次数均来自作者自行观察。

Claude Code Hooks、Skills 与子智能体:一套实用配置

引言

嗨!今天不聊 Proxmox,也不聊 Docker,而是聊一款我使用时间已经超过浏览器的工具——Claude Code。具体来说,是我如何不再盲目信任它,并进行配置,让它就算想搞砸事情也做不到。

先讲一个小场景:我让智能体修复 Bug 并部署。它钻进代码,完成提交,然后自豪地报告任务已完成。一个小时后,客户发来消息,说什么都没变。这是因为我的部署由 git push 触发,而智能体根本没有推送。它觉得完成提交就等于完成工作;提交留在我的磁盘上,所谓部署也就到此为止。真要命。

第二次事故更新鲜,发生在 7 月初:项目 A 中启动的会话悄悄开始编辑项目 B 的文件,而当时另一个智能体正在项目 B 中工作。两个互不知情的智能体同时折腾同一个仓库,简直是灾难配方。最后只需要做一点清理,已经算是奇迹。

经历这两场混乱之后,我认为是时候制定规则了。本文会提供三样东西:

1、我在 CLAUDE.md 中使用的真实规则,智能体会在每次会话开始时读取;
2、两个可以从物理上阻止智能体做蠢事的 Hooks(附可复制代码);
3、Skills 与子智能体,也就是如何避免第十次向智能体解释同一件事。

英文世界里已经有一些相关资料,Anthropic 最近也介绍了引导 Claude Code 的所有方式,但里面没有真实的生产脚本,所以这里分享我的版本。和往常一样,如果我遗漏了什么,还请见谅,我也仍在学习,哈哈。

CLAUDE.md、Skill、Hook 与子智能体分别是什么

Claude Code 可以通过四种方式控制。CLAUDE.md 是包含固定指令的文件,会在会话开始时载入上下文。Skill 是 Markdown 文件中的一套流程,由智能体按需加载。Hook 是 Claude Code 程序在特定事件发生时自动运行的脚本。子智能体则是拥有独立上下文、为某项子任务启动的单独会话。

机制 它是什么 何时生效 智能体能否忽略
CLAUDE.md 上下文中的固定指令 始终生效,从会话开始时起 理论上不会,但实际上偶尔会
Skill 按需加载的流程(SKILL.md 文件) 由你调用,或与任务匹配时 会逐步执行,但它终究还是模型
Hook 由运行框架执行的脚本 事件发生时:工具运行前、会话结束时等 不能,因为这是代码,不是请求
子智能体 用于子任务的独立会话 启动时 拥有自己的上下文与权限

本文最重要的一句话是:Hook 是唯一无法被模型忽略的机制,因为它由运行框架(也就是 Claude Code 程序本身)执行,而不是由模型执行。CLAUDE.md 与 Skills 都是写给模型的指令;和任何模型一样,它有时会忽略指令。Hook 则是普通代码:无论智能体心情如何,都会运行。

CLAUDE.md:把规则一次写清

CLAUDE.md 是一份指令文件,Claude Code 会在每次会话开始时将其载入上下文:全局文件位于 ~/.claude/CLAUDE.md,项目专用文件则位于仓库目录中。它是第一道防线,也适合存放那些你原本需要反复向智能体解释的事情。下面是我的全局文件中的几条真实规则:

## 部署与“完成”的定义
- 只有完成提交、推送到正确分支,并验证部署后,任务才算完成。
  部署只由 git push 触发。

## 密钥
- 绝不要在对话中输出密钥。以脱敏模式读取 env 文件:
  只显示变量名,不显示值。

## 范围
- 在本次会话的项目目录内工作。绝不要修改相邻项目:
  另一个智能体可能正在处理它。

## 风格
- 无论使用什么语言,都不要使用破折号。改用逗号、句号或冒号。

(没错,禁止使用破折号是一条真实规则。读过 AI 生成文本的人都知道为什么,哈哈。)

但 Markdown 中的指令终究只是请求,不是保证。大约 95% 的时候,它都表现得很完美。但在长会话中,随着上下文增长,模型可能会“忘记”规则。对于“不要闯进别人的仓库”这类规则,我宁愿不要亲自验证“实际上偶尔会”究竟有多频繁。这正是 Hooks 的用武之地。

Claude Code Hooks:智能体无法绕过的护栏

Claude Code 中的 Hook 是一段脚本(或任意命令),由运行框架在特定事件发生时自动执行:工具运行前(PreToolUse)、工具运行后(PostToolUse)、尝试结束会话时(Stop),以及其他几个事件。脚本通过标准输入获取事件的完整上下文,可以选择放行、阻止或添加内容。模型对此没有发言权。

我有两个 Hooks 在负责这项工作,它们都诞生于惨痛教训。

Stop Hook:终结幽灵部署

还记得引言中的场景吗?现在用代码把这个漏洞堵上。智能体认为工作完成、准备把控制权交还给你时,会触发 Stop 事件。我的 unpushed-work-guard.sh 会检查仓库中是否存在未推送提交或尚未提交的变更。如果存在,会话就会挨一下手板:

#!/usr/bin/env bash
# unpushed-work-guard.sh, hook Stop:
# don't let the session end with unpushed work
set -euo pipefail

input=$(cat)

# if the hook already blocked this Stop, let it pass (otherwise, it loops)
stop_hook_active=$(printf '%s' "$input" | jq -r '.stop_hook_active // false')
[ "$stop_hook_active" = "true" ] && exit 0

cwd=$(printf '%s' "$input" | jq -r '.cwd // empty')
repo=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || exit 0
git -C "$repo" remote get-url origin >/dev/null 2>&1 || exit 0

# commits ahead of origin + changes in files tracked by git
ahead=$(git -C "$repo" rev-list --count '@{u}..HEAD' 2>/dev/null || echo 0)
dirty=$(git -C "$repo" status --porcelain | grep -cv '^??' || true)
[ "$ahead" = "0" ] && [ "$dirty" -eq 0 ] && exit 0

reason="Unpushed work in $(basename "$repo"): commits ahead of origin: ${ahead}, changed files: ${dirty}. Deploy only runs on git push. When the job is done: commit and push. If you intentionally don't push, write it directly and only then finish."
jq -n --arg reason "$reason" '{decision: "block", reason: $reason}'

机制很简单:Hook 在标准输出中返回一段 JSON,其中包含 decision: "block"reason 字段。运行框架不会结束会话,智能体则会把 reason 作为指令接收,继续工作。实际效果是这样的:智能体刚写下“完成!”,忽然又自行补充一句“对了,我还得推送”,然后执行推送。简直像魔法,哈哈。

实际使用中还发现了两个细节:

stop_hook_active 是运行框架提供的标记,表示“这次 Stop 已经被阻止过”。如果不做这项检查,Hook 可能会让会话无限循环。
• 完整脚本还会把仓库状态签名(HEAD、领先 origin 的提交数、脏文件标记)保存到文件中,只有签名发生变化时才会发出提醒。否则,在交互式会话中,Hook 每遇到一次 Stop 都会唠叨。新提交会改变签名,因此护栏会重新启用。

PreToolUse Hook:如何阻止 Claude Code 编辑其他项目的文件

这是我的第二场事故。所有项目都放在同一个工作目录中,因此项目 A 的会话从技术上说可以看到项目 B 的文件。PreToolUse 事件在每次工具执行前触发,也是唯一能够阻止工具执行的事件。我的 sibling-project-guard.sh 会确保一个项目中的会话不能修改另一个项目的文件。脚本核心如下:

deny() {
  jq -n --arg r "$1" '{hookSpecificOutput: {hookEventName: "PreToolUse",
    permissionDecision: "deny", permissionDecisionReason: $r}}'
  exit 0
}

# Edit / Write: compare the target with the session's project
if [ "$tool" = "Edit" ] || [ "$tool" = "Write" ]; then
  fp=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
  if target_outside "$fp"; then
    deny "Blocked write to another project: session is in ${session_proj}, and the target is ${fp}. Another agent might be working on that project."
  fi
fi

Hook 返回包含 permissionDecision: "deny" 的 JSON,Claude 会在编辑实际发生前收到拒绝结果。拒绝信息中包含原因,因此智能体知道为什么无法执行,也不会陷入反复重试的循环。完整版还会拦截修改其他项目的 Bash 命令(git commit/push/checkoutrmsed -inpm install 等),但允许读取操作(catgrepgit log)。查看相邻项目的代码可能合理,修改它则不合理。

如何接入 Hook

Hooks 在 settings.json 中声明:全局配置位于 ~/.claude/settings.json,项目级配置位于 .claude/settings.json。每项配置由事件、可选的工具名称 matcher 和需要执行的命令组成:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|NotebookEdit|Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.claude/hooks/sibling-project-guard.sh",
            "timeout": 5
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.claude/hooks/unpushed-work-guard.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

脚本通过标准输入接收 JSON,并有两种回应方式:返回退出码(0 表示放行;2 表示阻止,并向智能体显示标准错误),或像前面的示例一样向标准输出写入 JSON。此外还可以设置 timeout,避免卡死的 Hook 拖住整个会话。事件和字段的完整列表请参阅 Hook 文档

Skills:用流程取代从头解释

Claude Code 中的 Skill 是一份包含流程的 SKILL.md 文件,智能体只会在需要时加载:要么它识别出 Skill 与任务匹配,要么你通过 /name 手动调用。与 CLAUDE.md 不同,Skill 不会始终占用上下文,因此可以写得更长、更详细,而且只在使用时产生成本。

我最常使用的是 ship。它直接源于第一场事故:既然智能体认为“完成”就是“已提交”,那我就给它一套流程,逐步定义任务究竟怎样才算结束:

---
name: ship
description: 当一个工作单元已经完成并需要发布,或者 Bartek 说“ship”、
  “push”、“send it”,或询问部署是否成功时使用。
---

# 发布

完成工作:验证、提交、推送、观察部署并进行冒烟测试。

1. 预检:运行项目的测试/Lint。如果出现问题,报告并停止。
2. 提交:只提交相关变更,使用简短说明。
3. 推送到项目的部署分支(以 CLAUDE.md 为准,绝不要猜)。
4. 观察部署:轮询线上 URL,直到它返回新版本(2~5 分钟)。
5. 冒烟测试:向生产环境发送一次真实请求,不能靠猜测。
6. 报告:SHA、部署状态、冒烟测试结果。

(这是缩短并翻译后的版本;我的英文原版还包含针对不同托管平台的细节。)现在,我不必再写一整段解释,只要说“发出去”,智能体就知道,只推送而不验证部署不能算完成。我在 Coolify 上配置 SearXNG时,处理的正是这类工作:提交、推送、观察构建,并检查网站是否正常。现在,一项 Skill 就能把全部步骤收尾。

我还有其他几个 Skills:oracle(在 VPS 上工作,任务结束后更新文档与 Grafana 仪表板)、preview(启动开发服务器,并给我一个检查链接),以及 go-live(在 Coolify 上发布新网站的检查清单)。都不是什么大工程,却恰好是我以前每次会话都要从头解释的事情。

子智能体:当一个会话不够用时

子智能体是一个独立的 Claude 会话,拥有自己的上下文、系统提示词和权限,由主会话针对某项具体子任务启动。我会在两种情况下使用它们:任务彼此独立,可以并行运行;或者某项杂乱搜索会在主上下文中制造海量垃圾,而我只需要最终结论。

以我自己的配置为例:本文介绍的整套系统,起点就是我让一个智能体检查过去的 Claude Code 会话(超过 200 个),并提出问题:“哪些事情反复出现,哪些地方经常出错?”主会话没有亲自阅读那些日志,而是交给子智能体;最终返回的是一份列出摩擦点的报告。本文中的 Hooks 与 Skills 正是源于这份报告。

当时启动任务的提示词一字不差地是:

使用子智能体审计我最近的 Claude Code 会话。把反复出现的摩擦点分类,
然后提出新的 Skills、自动化、Hooks 和 CLAUDE.md 改进方案。

会话记录位于 ~/.claude/projects/,智能体有大量材料可以分析。提前提醒一句,结果可能有些扎心:原来我一个月内输入了 13 次“你推送了吗?”,哈哈。

这个话题还能深入很多(自定义子智能体定义、限制其工具、为简单任务使用更便宜的模型),但这些内容留待下一篇文章。现在请先参阅子智能体文档

有什么变化

我不会假装自己已经测量了半年效果:CLAUDE.md 中的规则确实经过了数周完善,但目前形式的 Hooks 才刚使用几天(引言中的两场事故都很新鲜,这也正是本文存在的原因)。不过,差异立刻就能看出来:

幽灵部署归零。 会话无法在存在未推送提交时结束。Stop Hook 会把智能体赶回去继续工作;如果任务确实要保持未完成状态,智能体必须明确写出来,而不是悄悄把提交留在磁盘上。
闯入其他项目的情况归零。 护栏会在跨项目修改发生前将其终止。读取仍然有效,因此智能体可以查看相邻项目的代码,但不能做出更改。
新会话从第一秒就知道规则。 不必再花十分钟解释“我的部署是这样工作的,密钥保存在那里”;智能体会从 CLAUDE.md 获取全部信息。

有什么令人烦恼的地方?Hooks 本身就是代码,因此必须像维护代码一样维护。第一版护栏会产生误报:我的一个仓库名称中包含单词 switch,而 git switch 是修改性命令,所以护栏甚至会阻止该项目中的无害读取,哈哈。这就是为什么脚本会先移除 -C <path> 参数再进行匹配,也解释了为什么旁边还放着一份包含回归测试的 sibling-project-guard.test.sh。既然断路器会跳闸,那至少应该在正确的时候跳。

常见问题

Claude Code 中 Skill 与 Hook 有什么区别?
Skill 是由模型执行的 Markdown 指令,因此很灵活,但可能执行得不好。Hook 是 Claude Code 程序在事件发生时运行的脚本,因此每次都会执行。Skills 用于流程,Hooks 用于硬性规则。

Hook 能阻止命令执行吗?
可以。PreToolUse Hook 会在工具运行前,通过标准输入获取完整工具调用,并可以返回 permissionDecision: "deny"。工具不会执行,智能体会收到拒绝原因,必须调整做法。

Hook 配置应该保存在哪里?
全局配置保存在 ~/.claude/settings.json 中,项目配置保存在项目的 .claude/settings.json 中。每项配置包含事件、可选的工具名称 matcher 和要执行的命令,还可以选择性地设置超时。

只有 CLAUDE.md、不使用 Hooks,够用吗?
一开始够用,而且很值得先从它入手,因为它无需额外成本就能覆盖大多数情况。但 CLAUDE.md 终究只是向模型提出的请求。那些违反后会造成重大损失的规则(部署、密钥、其他项目),最好再用 Hook 封死,因为 Hook 无法被忽略。

这些功能只能在终端中使用吗?
不是。Claude Code 可用于 CLI、桌面应用、浏览器和 IDE 扩展(VS Code、JetBrains)。Hooks 与 Skills 位于你的 ~/.claude 目录中,因此只要相应环境能够访问该目录,就能使用它们。

总结

就是这样!简单来说:CLAUDE.md 负责写下规则,Skills 把反复解释的内容变成流程,Hooks 则把最重要的规则变成模型无法忽略的硬性安全网。本文中的所有脚本都可以自由复制和修改。

最后还有一个颇具讽刺意味的彩蛋。我构建整套系统,是为了约束智能体。结果发现,Stop Hook 最常打手板的对象竟然是……我自己。因为我才是那个手动提交、说着“等一下就推送”,然后跑去泡茶的人。护栏原本用来监管 AI,最终主要监管了人类。算了,至少它确实有效,哈哈。

下一篇文章已经开始成形:我构建了自己的 MCP 服务器,让智能体能够读取这个博客的分析数据(今天这个选题其实就是这样决定的,不过细节下次再说)。如果有什么对你无效,请告诉我。保持酷!