Claude Code 我已经用了不短的时间,刚开始跟大多数人一样,直接在终端里甩几句自然语言让它干活。表面看确实自由,可真放到项目里跑上两周,你就会碰到一个非常现实的问题:同一套评审规范、提交约束、测试流程,我几乎是每天重新解释一遍,而且无论我提示得多细,它在不同会话里的发挥依然忽高忽低。直到我静下心把常用指令全部整理成 Commands,又在它执行动作的链路上挂了 Hooks,才觉得 Claude Code 从“一个能写代码的高级聊天窗口”变成了“一个能被工程纪律约束的协作者”。这篇文章是把我在真实仓库里配置和使用这两套系统的经验做一次完整复盘,适合已经装好 Claude Code、想把它从玩具变成生产力工具的人。
先说结论:Commands 解决的是“怎么让模型稳定地听指令”,Hooks 解决的是“怎么在关键动作上卡住它、不让它乱来”。这两件事单独拎出来都不算复杂,但配合好了,能把一个通用 Agent 调教成符合你团队习惯的专用工具。
1. 为什么自然语言协作会失灵:Commands 想解决的真问题
1.1 我踩过的“同一件事每次重说一遍”的坑
很多人的第一反应是:Claude Code 连得上模型、能读仓库、能执行命令,那我还配置什么?直接打字不行吗?
行,但会非常累。
我最开始常让它做的事是“帮我按仓库规范生成提交说明”。第一次我写了很长一段:要看 git diff、要识别改动类型、要按 Conventional Commits 写标题、正文要解释为什么这么改、不允许提交调试代码。它干得不错,我很满意。第二次我让它在另一个分支上做同样的事,它开始自由发挥,标题风格不一样,正文写成了 changelog,甚至把调试开关也写进了提交。后来我把这段提示词复制出来,每次粘贴,还是有问题:粘贴得太长它抓不住重点,粘贴得太短它又回到了自由发挥。
再比如代码评审。我的团队对安全性、异常处理、测试补充有固定的检查顺序。靠自然语言我每次都要把顺序重申一遍,偶尔忘了一个环节,评审结果差异就很明显。
这些场景有个共同特征:任务本身是稳定、可枚举、有固定流程的,只不过每次喂给模型时的“输入”不一样。这就是 Commands 应该出场的时机。
1.2 命令文件如何把“嘱咐”变成模型上下文的一部分
Claude Code 的 Commands 不是一个独立程序,也不是一段要被解释执行的脚本。它的本质是斜杠触发的 Markdown 提示词模板。你把一段写好的指令放在指定目录里,然后在对话里输入 /命令名,这个文件的内容会被当成指令注入到当次请求里,模型就能稳定地“看到”一套完整规则。
这个设计我觉得非常聪明。区别于其他工具把命令做成菜单项、插件函数,Claude Code 把最重要的“人味”留给了提示词本身。你需要约束的不是某个函数的参数,而是模型在过程中的理解和判断。一份写得好、写得具体的 Markdown 模板,比任何封装都直接。
你可以把它理解成给一个实习生写工作手册:手册不能规定他打字时手指怎么放,但能规定他一接到任务该先看什么、后做什么、产出物长什么样、遇到冲突找谁确认。Commands 就是这份手册的载体。
1.3 两个层面的收益:个人效率与团队一致性
配置命令后的第一个收益是个人效率。不需要再回忆上次的措辞,一条命令天然保持上次的品味。对我这种经常同时维护几个仓库的人来说,这套机制等于把我的操作习惯搬到了云端。
第二个收益是团队一致性。.claude/commands/ 是项目目录下的普通文件,完全可以提交进 Git。新人克隆仓库后,输入 / 就能看到项目里预设的命令,不需要去翻几十页的 Wiki。他用命令生成的东西,和资深同事用命令生成的东西,风格基本一致,这直接降低了评审成本。
我还在团队里做过一个实验:让两个同事分别用自然语言和命令去生成同一份发布说明,前者需要来回打磨 3 轮,后者基本一次通过。差异不在模型聪明不聪明,而在后者接收到的上下文更完整、更稳定。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Commands 的落地姿势:目录、模板与参数化设计
2.1 命令文件放哪里:项目级与用户级的分工
命令不是装一个插件就有的,而是要你自己建立目录结构。Claude Code 会从两个位置加载自定义命令:
- 项目级:
.claude/commands/,跟随仓库走,团队共享; - 用户级:
~/.claude/commands/,只对当前用户生效,适合放自己跟项目无关的习惯命令。
我常用的策略是:凡是跟仓库规范、技术栈、交付流程相关的命令放项目级;凡是“帮我整理 commit message”“帮我解释这段代码”这类通用动作放用户级。这样既不污染团队配置,又能最大化自己的效率。
文件命名有几个细节值得注意。
我最初把所有命令扁平堆在一个目录里,结果没到十个就乱成一团。后来发现它支持按子目录组织,也就是说文件放 .claude/commands/code/review.md,触发的命令就是 /code/review。我按照「动作领域 + 具体场景」的规则建了几个子目录,命令库瞬间清爽了。
命名上还有个容易踩的小坑:文件名里的连字符会原样出现在命令里。叫 do-commit.md,敲的时候就要打 /do-commit,手感和视觉都不如直接叫 commit.md 舒服。建议尽量少用连字符。
同级文件夹下如果存在同名文件,项目级命令会覆盖用户级命令。这个行为是合理的:团队通过项目级配置统一规范,个人的习惯命令可以作为兜底,但一旦仓库里出现了同名命令,就以项目的为准。
2.2 一个最小命令文件的骨架
命令文件内容分两部分:文件头部的 YAML 元信息和主体 Markdown 指令。YAML 里的 description 字段尤其重要,它决定了命令列表里显示什么,也会影响模型对你意图的理解。不要小看这段描述,写得含糊的命令很容易被在错误场景触发。
下面是我的一个典型项目级命令 /review 的简化版本:
markdown复制---
description: 按团队规范检查当前改动,输出结构化 Code Review 意见
---
你是一名严格的高级代码评审员。请基于当前分支相对主干分支的 diff 进行检查。
执行步骤:
1. 先运行 `git status` 和 `git diff --stat` 了解改动范围。
2. 获取具体 diff 内容,逐文件阅读。
3. 按如下顺序检查:
- 是否存在安全问题(注入、敏感信息、权限缺失);
- 是否存在异常被吞掉的情况;
- 新代码是否有对应测试,测试是否断言了关键行为;
- 是否引入明显的不必要复杂度。
4. 输出格式:
- 先给一个总体结论:通过 / 需修改;
- 再按“问题严重程度”列出条目,每条包含文件位置、问题描述、修改建议;
- 只报告真实存在的缺陷,不要为了凑数而挑刺。
正文里不需要写“请友善”这类情绪词,多写“执行步骤”和“输出格式”这种硬约束。一个命令是否好用,通常取决于你对输出格式的定义是否精准。模型需要在最后知道你要什么形态的结果,否则它默认给你长篇大论。
2.3 命令背后的上下文加载:别把仓库全塞进去
命令模板虽然能承载提示词,但它没办法在文件里内置一份动态的代码 diff。所以一个好命令写作者的自我修养是:在命令里明确要求模型“自己去看数据”。
以上面那个 review 命令为例,模型执行时会主动调用 Bash 工具去获取 diff。你不需要把 diff 内容复制进 Markdown。前提是当前工作目录正确,且模型被允许执行这些命令。
如果你确实需要把某个说明文件作为固定上下文粘给模型,可以在命令正文里提示模型读取指定文件,比如“请先阅读 docs/contributing.md 中的评审规范”。我一般不建议把大段规则直接贴进命令里,因为会让每个命令都很臃肿。适用于整个仓库的长期规则,我更倾向于让模型读 CLAUDE.md,命令里只需要指出这个依据。
2.4 亲手搭一套个人命令库:我的分类抽屉
这里分享一份我实际在用的命令清单,供你搭自己的命令库时参照:
| 命令 | 作用 | 使用频率 |
|---|---|---|
/review |
基于 diff 的代码评审 | 每天多次 |
/fix |
让 Claude Code 修复某个测试失败,但有明确纪律 | 每天多次 |
/commit |
生成符合规范的提交说明 | 每天多次 |
/test |
为新增逻辑补测试,并完成一次真实运行 | 开发高频 |
/explain |
解释某段复杂实现,帮助排查历史代码 | 按需 |
/changelog |
对照最近提交生成更新日志 | 发版前 |
我的建议是,先不要贪多,从你本周已经重复说过三次以上的自然语言指令开始转化。把那条指令打磨成命令,连续用一周,再评估是否保留了它。命令库就像衣柜,多了也没用,经常穿的就那几件。
3. Hooks 的机制拆解:它在什么时候拦下 Agent
3.1 Hook 是 Agent 执行链路中的红绿灯
Commands 给了模型一份稳定的“办事指南”,但指南只是文本,模型可以选择遵不遵守。如果你想让某些规则在系统层面强制执行,那就得靠 Hooks。
Hooks 可以理解为挂在 Claude Code 生命周期上的回调。它会在特定事件发生时,自动执行你指定的命令或脚本。它不是让模型“尽量”做的事,而是一道闸门:脚本说可以,才放行;脚本说不行,模型这一步就被掐断。
有同事问过我:这跟定时任务有什么区别?
定时任务是到时间就执行,Hook 是跟着 Agent 的动作实时响应的。它和模型之间的交互,更像数据库里的约束和触发器——PreToolUse 类似检查约束,PostToolUse 类似数据被改写后的日志与后续联动。
具体项目里,我用它管住过几个非常让人头疼的问题:
- 模型在一个“只读”任务里突然尝试修改非预期文件;
- 模型在写完代码后自行宣称“测试通过”,实际上根本没跑过测试;
- 模型在处理到一半时跑出一个非常长的日志,把上下文撑爆;
- 别人误在某个不该使用的目录下启动了 Claude Code,导致危险命令被执行。
这些事靠提示词不是完全不能约束,但稳定性不够。Hook 是确定性机制,规则完全由脚本决定,不依赖模型当时的理解能力。
3.2 settings.json 中的 hooks 配置结构
Hooks 的配置集中在 .claude/settings.json(当前项目生效,亦可放在用户级配置目录)。这是一个相对完整的配置骨架:
json复制{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|NotebookEdit|MultiEdit",
"hooks": [
{ "type": "command", "command": "python .claude/hooks/guard_path.py" }
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "python .claude/hooks/collect_test_result.py" }
]
}
],
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/check_workspace.sh" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "python .claude/hooks/on_finish.py" }
]
}
]
}
}
我把常用事件的用途整理了一个速查表:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
SessionStart |
每次交互会话启动时 | 环境检查、目录准入,甚至阻止在敏感目录启动 |
UserPromptSubmit |
你提交一段提示词之后 | 对输入做检测、脱敏,或把输入转发给其他系统 |
PreToolUse |
某个工具被调用之前 | 路径白名单、危险命令拦截、权限控制 |
PostToolUse |
某个工具执行完成之后 | 提取工具输出、给模型补充信息、触发后续动作 |
Notification |
Claude Code 需要通知用户时 | 把通知转发到其他通道 |
Stop |
Claude Code 一次完整响应结束后 | 收尾检查、把对话记录归档、触发外部工作流 |
SubagentStop |
子代理完成任务后 | 汇总子代理结果,做后续处理 |
PreCompact |
上下文将被压缩前 | 把重要信息备份到文件,防止压缩丢失 |
matcher 字段我简单解释一下:在 PreToolUse 和 PostToolUse 场景中,它用来匹配工具名,支持正则。比如上面配置里的 Write|Edit 就只会拦截文件写入类的工具;如果留空,则所有工具调用都会触发,这会明显拖慢速度,不建议默认这么干。
3.3 回调脚本的输入输出协议:这是最核心的知识点
Hook 脚本的工作方式非常直观:Claude Code 会把一件事的结构化信息通过 stdin 以 JSON 形式送给脚本;脚本经过判断,把结果通过 stdout 以 JSON 形式返回。任何不是标准 JSON 的 stdout 内容都可能让 Claude Code 行为异常,这点后面我会专门讲坑。
一次 PreToolUse 事件给到脚本的 JSON 大致长这样(不同版本字段会有增减,但思路一致):
json复制{
"session_id": "xxx",
"transcript_path": "/path/to/logs/xxx.jsonl",
"cwd": "/home/me/myproject",
"tool_name": "Write",
"tool_input": {
"file_path": "/home/me/myproject/src/utils/token.go",
"content": "package utils..."
}
}
你不需要关心 session_id 这些元信息,要看的核心是 tool_name 和 tool_input。以 Write 为例,tool_input.file_path 就是模型正准备写的目标文件,你可以在这个环节用脚本判断路径是否合法。
如果你想拦截,那么 stdout 里返回类似下面的 JSON 即可:
json复制{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "该文件不在允许修改的目录内,已自动拦截。"
}
}
Claude Code 收到后,这次写文件操作会被直接拒绝,并且拒绝原因会出现在对话流中,模型看到后会自动调整思路。这就是“闸门”的完整工作逻辑。
3.4 先用只读采样验证你的 Hook“看得到什么”
很多人第一次写 Hook 脚本就直奔拦截,结果脚本不工作,又不知道为什么。我强烈建议先别急着做判断逻辑,先用一个只读脚本把真实事件打印到日志文件里,观察一次完整的调用长什么样。
我在 .claude/hooks/debug_hook.py 里放过这样的脚本:
python复制import json
import sys
from datetime import datetime
def main():
try:
payload = json.load(sys.stdin)
except Exception:
return
tool_input = payload.get("tool_input", {})
log_line = {
"time": datetime.now().isoformat(),
"tool": payload.get("tool_name"),
"input": tool_input
}
with open("/tmp/claude_hook_debug.log", "a", encoding="utf-8") as f:
f.write(json.dumps(log_line, ensure_ascii=False, indent=2) + "\n")
if __name__ == "__main__":
main()
然后让 Claude Code 干一件简单的活,比如“帮我在 docs 目录写一个 README”。操作完之后,你去查看 /tmp/claude_hook_debug.log,就能看到模型到底打算怎么调用工具、路径是什么、参数是什么。这份真实记录比任何文档都更能指导你写后面的判断规则。
踩过一次坑后,我现在每写一个 hook,都会先采样、再定制逻辑、最后上拦截,很少直接写一步到位。
4. 实战:一条命令加一套钩子,把“提交代码”变成防呆流程
理论讲太多容易飘,我拿一个我自己跑了很长时间的场景来拆解:让 Claude Code 在一个仓库里改完代码后,自动生成符合规范的提交,但强制它不能碰非授权文件,且必须真实跑过测试后才能提交。
4.1 场景设计:我要这套机制防住什么
Claude Code 最大的风险不是“写不出代码”,而是“太敢写”。它会在你指定的 src 目录正常改代码,但如果没人约束,它也可能顺手改掉配置文件、测试样本甚至文档。
另一个风险是“虚假自信”。模型在写完一个函数后经常直接说“已通过测试”,实际上根本没有执行测试命令。对这种行为,提示词约束效果差,必须上 PostToolUse 的钩子把真实结果喂回上下文。
还有一类是重复劳动。每次提交前,我都希望它按照传统规范写标题和正文,且正文能解释改动原因。这类规则放进 /do-commit 命令。
整体设计分成三层:命令层负责“按规范办事”,PreToolUse 钩子负责“不让它越界”,PostToolUse 钩子负责“把真实执行结果钉在上下文里”。
4.2 第一层:实现 do-commit 命令
这个命令约定用户在执行前可补充一段目标说明,例如 /do-commit 修复登录页在移动端的布局问题。命令主体内容:
markdown复制---
description: 按仓库规范完成提交,必须先跑测试再提交
---
请基于当前分支的改动完成一次 git 提交。
执行步骤:
1. 使用 `git status` 查看当前改动,确认没有未预期的文件被修改。
2. 检查是否出现了被钩子拦截的越权文件;如果存在,忽略这些文件的改动,不要提交它们。
3. 运行 `go test ./...`,必须真实看到执行结果;如果测试失败,停下并修复,不要强行提交。
4. 在确认测试通过后,使用 `git diff --cached` 查看将要提交的内容。
5. 生成提交信息,标题采用 Conventional Commits 风格,正文必须说明“为什么这样改”,而不是罗列文件名。
6. 执行提交。
硬性要求:
- 不要使用 `git commit --no-verify`。
- 不要改动与本次目标无关的文件。
- 不要在未运行测试的情况下声称测试通过。
注意第 3 条特别重要:很多模型喜欢走“我推断测试能过”的捷径,所以我要求它必须执行命令并观察真实输出。这时候 PostToolUse 钩子会成为兜底,确保它也骗不了系统。
4.3 第二层:PreToolUse 钩子守卫目标文件的合法范围
我定义了一个简单的路径白名单:只有 src/ 目录下的文件可以被写,docs/ 目录下的文档只允许新增不允许修改已有内容(这个例子我故意只用 src,让判断更清晰),其他任何目录的写操作都直接拦截。
脚本 .claude/hooks/guard_path.py:
python复制import json
import sys
ALLOWED_PREFIXES = ("/src/", ".\\src\\")
def main():
try:
payload = json.load(sys.stdin)
except Exception:
return
tool_input = payload.get("tool_input", {})
file_path = tool_input.get("file_path", "")
normalized = file_path.replace("\\", "/")
allowed = any(normalized.startswith(p) for p in ALLOWED_PREFIXES)
if not allowed:
result = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": f"目标路径不在允许范围内:{file_path}。只允许修改 src 目录下的文件。"
}
}
print(json.dumps(result))
return
# 放行:不输出或者输出空 JSON
print("")
if __name__ == "__main__":
main()
这里有个细节:如果判断是放行,stdout 什么都不输出即可,不需要构造 allow 结构。一旦模型尝试去改根目录下的 go.mod,这个钩子就会把它的动作弹回去。不仅这次动作做不了,模型看到 permissionDecisionReason 后,通常还会主动向用户解释它遇到了什么限制。
4.4 第三层:PostToolUse 钩子把测试结果“钉”回上下文
PreToolUse 挡住了越界,但还挡不住模型“假装测试过”。我在 PostToolUse 事件里监听 Bash 工具的调用,当它执行的命令包含测试关键词时,脚本就把真实退出码和最后一段输出抓出来,作为额外上下文喂回去。
简化版本 .claude/hooks/collect_test_result.py:
python复制import json
import sys
def main():
try:
payload = json.load(sys.stdin)
except Exception:
return
# PostToolUse 里拿到的 tool_input 往往比较精简
tool_input = payload.get("tool_input", {})
command = tool_input.get("command", "") if isinstance(tool_input, dict) else ""
if "test" not in command and "pytest" not in command and "go test" not in command:
return
# 只处理真实执行过的测试类命令
stdout_output = payload.get("tool_response", "")
if not isinstance(stdout_output, str):
stdout_output = json.dumps(stdout_output, ensure_ascii=False)
last_lines = stdout_output.strip().splitlines()[-5:]
summary = "\n".join(last_lines)
result = {
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": (
f"[钩子提示] 检测到测试命令执行完成,退出码见 transcript,末尾输出如下:\n{summary}\n"
"如果测试执行失败,你必须先修复问题,绝不能假装测试通过。"
)
}
}
print(json.dumps(result))
if __name__ == "__main__":
main()
这段脚本的可取之处在于:它不是把命令的全部输出一股脑丢回去,那会把上下文撑爆。它只摘取最后几行,让模型知道测试的最终状态,同时用一句强提示迫使模型面对结果。退出状态码如果没有显著失败,Claude Code 会把这次结果当成上下文,模型后续决策就会基于真实的测试反馈,而不是自己的凭空想象。
4.5 跑完整个流程后,日志里发生了什么
我按 git checkout -b fix-mobile-login 创建一个测试分支,然后故意留了一个未完成的改动,对 Claude Code 说“继续完成这个修复并用 /do-commit 提交”。实际运行中:
- 模型先执行
git status、git diff等只读命令,没有触发 PreToolUse 拦截; - 它尝试写
src/components/LoginPage.tsx,路径在src/下,放行; - 它中途试图顺手更新根目录的
README.md,被 PreToolUse 钩子拦截,然后模型在对话里复述了被拦截的原因; - 它执行
npm test,collect_test_result.py钩子把测试成功的最后几行注入上下文; - 模型随后按
/do-commit的规范生成 commit message 并提交。
整个过程中我只在最开始给了一句指令,剩下全部由这套配置接管。对比之前在自然语言模式下我全程盯着它的路径和测试表现,这个流程省心太多了。
5. 排错指北:Commands 和 Hooks 一起用最容易翻车的四类问题
配置一时爽,排错火葬场。这套系统组合使用时,我先后遇到过不少问题,整理下来有四个典型场景,几乎每个新人在第一次接 hooks 时都会遇到。
5.1 配置了命令,输入斜杠后却看不到
一个常见原因是目录放错了。命令必须在 .claude/commands/ 或 ~/.claude/commands/ 下,而且文件名后缀必须是 .md。很多新手没在意文件后缀,保存成了 .md.txt,自然识别不了。
另一个原因是写完新文件后没有重启会话。命令列表通常是在会话启动时建立的索引,如果你在某次会话运行中新增了命令文件,可能不会立刻出现在 / 列表里。重启一个会话再看,一般就能解决。
还有一个细节是文件格式。Markdown 里 YAML frontmatter 必须严格位于文档最上方,前面不能有空白字符或中文注释。我见过有人为了便于阅读,在 frontmatter 前面写了一段说明,结果整条命令都无法识别。
5.2 Hook 脚本没生效:我把它当成“静默失败”处理
Hook 脚本如果执行报错,Claude Code 不一定会在会话里给你弹出很大的异常提示。常见表现是:钩子没有任何反应,仿佛配置不存在。这时候要去排查三个点。
第一,检查执行命令中的路径。配置里写的是 python .claude/hooks/guard_path.py,这个相对路径是相对于当前工作目录的。如果你在一个子目录里启动了 Claude Code,路径就会失效。我更建议写成绝对路径或通过环境变量的方式拼接,比如把仓库根目录写进脚本内部。
第二,在 Linux/macOS 下如果用的是 shell 脚本,记得确认执行权限。bash XXX.sh 通常不需要可执行位,但如果直接写 .claude/hooks/xxx.sh,文件必须要 chmod +x。
第三,给脚本加一行“把运行结果写到固定日志文件”的兜底。一旦发现问题,先看日志文件有没有新增内容,或者用 shell 手动把一份样例 JSON 喂给脚本,先独立验证脚本本身能跑通(举个例子:cat /tmp/event.json | python .claude/hooks/guard_path.py)。这样能把问题快速界定在“脚本逻辑”还是“Claude Code 没调用”上。
5.3 stdout 被普通日志污染,Claude Code 行为变得诡异
这个坑我印象太深了。有一段时间我给 PostToolUse 挂了一个钩子,脚本是用 Python 写的,里面习惯性地写了 print("hook running finished"),想确认自己跑过。结果 Claude Code 的上下文里混进来一行普通文本,JSON 解析失败,会话行为变得非常奇怪,包括模型突然认为自己可以执行一些它没有权限执行的事。
Hook 脚本对 stdout 非常严格。如果你想在脚本里加日志方便调试,请写到 stderr 或文件里,不要 print 到 stdout。Claude Code 会完整读取 stdout 并尝试把它当成协议 JSON,任何多余字符都可能造成解析问题。
如果不幸粘上了这种问题,最快的排查方式是打开 transcript 日志,查看那次工具调用的原始输出。不要在会话里继续对话期望它自己恢复。
5.4 上下文被 Hooks 塞爆,Token 账单变得很难看
这是我配置 PostToolUse 之初最大的失误。当时为了向模型提供“足够多的上下文”,我直接把每个 Bash 命令的完整 stdout 回写到 context 里。跑一次全量测试产生的几千行日志会让上下文占用率飙升,不仅耗 token,还会导致模型更早开始处理信息压缩,影响它对本轮任务的理解。
后来我吸取的教训是:钩子给模型提供的信息应该是“结论”而非“全量日志”。想清楚模型此刻需要知道什么:它需要知道测试是通过还是失败、失败了大概哪个模块、结束语是什么,而不是完整的测试输出。所以在脚本里做裁剪、摘要、最后几行提取,这些看似简单的工作,实际上决定了这套系统是否能在复杂任务里长久运转。
另外一点是不要让多个不相关的 hook 同时往上下文里注入内容。我会给每个 PostToolUse 钩子加一个关键词过滤条件,只处理自己关心的事件。减少注入,不是浪费,而是在保护模型的注意力。
6. 让这套机制在团队里活起来:我的沉淀建议
6.1 先把 .claude 目录纳入版本管理
很多人配置完这些东西后,第一反应是把它加进 .gitignore,理由是它是本机工具配置,不应该进仓库。我的看法相反:.claude/commands/ 和 .claude/settings.json 恰恰是应该跟着仓库走的东西,前提是脚本里没有写入个人私密 token。
把 .claude 放进版本控制,等于把原来散落在各人脑中的“使用 Claude Code 的约定”变成团队资产。新同事加入后不需要问“你们平时怎么让它提代码规范的”,直接克隆仓库就能用。命令文件本身也是可 review 的,代码评审时可以顺便评审规则,这是很舒服的协作方式。
6.2 从一两个命令起步,别急着把全家桶搬进去
我遇到过朋友看到 Commands 和支持列表后非常兴奋,一个晚上写了 20 条命令、5 个 hook,第二天实际用起来的不到 3 个。跟任何自动化系统一样,一开始设计得太庞大,后面维护成本会摧毁这个习惯。
更实际的路径是:先用自然语言让 Claude Code 干活,当你在同一件事上重复第二遍时,把指令沉淀成命令;当某一次模型真的做了你不想让它做的事时,把那个场景写成一个 PreToolUse 钩子。这样积累出来的配置,每一行都有真实痛点支撑,不会变成摆设。
6.3 一份可以照抄的仓库目录模板
我把目前团队里效果最好的结构整理在这里,给想直接复制的读者作参考:
text复制my-project/
├── .claude/
│ ├── commands/
│ │ ├── code/
│ │ │ ├── review.md
│ │ │ ├── fix.md
│ │ │ └── test.md
│ │ ├── commit.md
│ │ └── release.md
│ ├── hooks/
│ │ ├── guard_path.py
│ │ ├── collect_test_result.py
│ │ └── check_workspace.sh
│ ├── settings.json
│ └── settings.local.json
├── CLAUDE.md
└── ...
settings.json 管理团队统一配置,settings.local.json 是本机私有配置,里面可以放个人习惯,不进版本库。CLAUDE.md 用来写仓库的长期规则,比如技术栈说明、目录规范、禁止事项,Commands 和 hooks 只保留“动作”和“拦截”这两件事。
6.4 最后再分享一个让检查流程更丝滑的小技巧
在 Command 文件里,我通常会在最后加一行:如果执行过程中发现任何与当前任务无关的改动,请停下来向用户说明,不要自作主张处理。。起初我以为这是废话,后来发现这条提示可以砍掉很多模型“顺便帮忙”造成的意外改动,配合 PreToolUse 钩子的白名单,双管齐下之后的越权操作少了很多。
我想强调的是,Commands 和 Hooks 的终极目标不是把模型锁死,而是把自由限制在可控范围里。模型在边界内依然可以天马行空地写代码、调试、重构,边界则由你通过配置来主导。这种“自由 + 护栏”的组合,也是我觉得 Claude Code 比单纯聊天窗口更像真实协作者的原因。如果你手头正好有重复劳动任务,不妨今天就挑一条最常说的自然语言指令,把它变成你仓库里的第一个 Command。
