如果你最近开始认真用Claude Code做开发,大概率已经遇到过这种场景:想让Claude按团队规范审查代码,每次都要在对话框里重复一大段要求;想让它动手改文件之前先跑一遍安全检查,却发现它总是埋头往前冲,拦都拦不住。Commands和Hooks就是为这两个痛点设计的。Commands是自定义斜杠命令,本质是把高频使用的提示词固化成 /xxx 这样的快捷入口,相当于把资深工程师的审查标准、提交规范做成团队可复用的“模板库”;Hooks则是挂在生命周期事件上的自动化脚本,在Claude每次读取文件、编辑文件、回复用户的前后,自动执行你预先写好的命令。这篇我会把两个系统的设计思路讲透,再给完整配置和可直接抄的实操案例,最后聊聊我踩过的坑和排查方法。适合已经装好Claude Code、想进一步提升日常效率的开发者参考。
1. Commands与Hooks到底解决什么问题
1.1 Commands:把高频提示词固化成斜杠命令
用过终端的人都知道alias的好处——把一长串命令缩成一个词,效率提升立竿见影。Claude Code的Commands就是这个思路,只不过它压缩的不是shell命令,而是你反复输入给Claude的提示词。
什么叫高频提示词?我举几个真实场景:
- 每次想让Claude审查代码时,你都要输入“请以资深工程师视角审查当前分支改动的代码,重点检查安全性、可维护性、是否遵守项目规范,发现问题按严重程度列出,并给出可执行建议”。
- 每次要写提交信息时,你都要输入“根据当前git diff生成符合Conventional Commits规范的提交信息,语言用中文,控制在50字以内”。
- 每次让Claude解释一段复杂逻辑时,你都要补一句“不要只罗列代码,要说明设计意图、调用链和潜在风险”。
这些话单独输入一次不觉得累,但一天重复十几次就变得非常烦躁,而且每次说法不一样,Claude的输出质量也不稳定。Commands就是把这些固定表述抽出来,做成 /review、/commit、/explain 这样的斜杠命令。团队里每个人敲同一个命令,拿到同一套标准,输出自然更可控。
更关键的是,Commands不只是“一段固定文字”。它可以接收参数,比如 /review 只检查src目录下的改动;可以限制Claude能用的工具,比如审查命令只允许读文件不允许改文件;还可以指定使用哪个agent子代理来执行。这些能力让命令从“快捷输入”升级成“带约束的工作流入口”。
1.2 Hooks:给工具调用装一层自动化的“门卫”
如果说Commands是主动发起的指令,那Hooks就是被动触发的“条件反射”。你不需要告诉Claude“你改文件之前要先跑lint”,而是配置一个Hook,让Claude每次准备调用Edit或Write工具时,系统自动先执行一遍lint脚本,不过就打断。
这个概念有点像是给Claude Code装了一个门卫——它想进哪个门(调用哪个工具),门卫按规则检查一下你的通行证(执行Hook脚本),通过了才放行。这解决的是AI编程助手的天然短板:不可控。模型再聪明,也可能在改代码时引入格式问题、忘记跑测试、或者不小心动了不该动的文件。Hooks把这些检查变成机制,而不是依赖模型“自觉”。
我用Hooks最多的几个场景:
- 编辑文件前先备份或检查目标文件是否在允许修改的白名单内;
- 改动代码后自动跑lint或单测,失败就中断Claude的下一步操作;
- Claude每次回复前,自动把当前分支的未提交变更追加到上下文里,避免它“失忆”;
- 把Claude每一次工具调用记录下来,用于审计和复盘。
Hooks本质上是事件驱动的外部脚本调用,它不改变Claude本身的能力,而是改变了Claude做事的“环境”和“流程”。这也是很多团队敢把Claude Code放进生产流的核心原因——有了Hooks,你才能对AI的行为设置硬性边界。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Commands系统配置与实战
2.1 命令文件放哪里
Commands的配置非常简单,本质上就是一堆Markdown文件,文件名就是命令名。注意文件名就是触发词本身。
存放位置有两个级别:
- 项目级:放在项目根目录下
.claude/commands/文件夹里,比如.claude/commands/review.md。这个目录会提交到git仓库,跟着项目走,适合团队共享规范。 - 用户级:放在用户目录下
~/.claude/commands/。放这里的命令对所有项目生效,适合个人高频短语,比如你习惯让Claude用某种风格写代码,放在用户级就不用每个项目都复制一遍。
两个位置会合并生效,同名命令以项目级优先。我实际用下来的经验是:跟业务规范有关的(如提交格式、审查标准)放项目级,跟着仓库走;跟个人习惯有关的(如“说话简洁点”“不要用emacs快捷键”)放用户级。
有些新版本还支持子命令,比如命令文件命名为 review-security.md,就可以用 /review-security 触发;如果命名为 review/create.md,还能实现二级目录形式的子命令。团队命令一多,这个分层能力会很有用。
2.2 一个标准命令文件的完整写法
命令文件是Markdown格式,但头部带有一段YAML frontmatter,用来声明元信息。我写一个完整的例子:
markdown复制---
description: 按团队规范审查本次改动
argument-hint: [可选] 指定审查范围
allowed-tools: Read, Grep, Glob
agent: code
---
请以资深代码审查员的身份,审查当前分支相对于主分支的改动。
重点检查:
1. 是否引入安全漏洞(SQL注入、硬编码密钥、路径穿越等)
2. 是否遵循项目的错误处理规范
3. 是否包含不必要的破坏性变更
4. 变更是否附带相应测试
如果发现任何问题,按严重程度排序输出,每个问题给出:
- 问题描述
- 影响范围
- 可执行的修改建议
本次需要关注的额外范围:$ARGUMENTS
这段配置里几个关键字段:
description:命令的简短说明,会在斜杠命令列表里展示。argument-hint:提示用户这个命令需要传什么参数,比如“指定审查范围”。allowed-tools:这个命令允许Claude使用的工具白名单,用逗号分隔。我给审查命令只开放了只读工具,明确禁止它改代码——审查就该只审不改,防止Claude顺手“修”出问题。agent:指定这个命令跑在哪个agent上,code是偏编程的,不写则用默认。
正文部分就是一段完整的提示词。这里有个非常重要的原则:正文里除了模板化的要求,一定要留出插入参数的位置,用 $ARGUMENTS 表示用户输入的全部参数。没有这个变量,用户敲 /review src/utils 时,src/utils 就会被忽略,命令就变成了一个死板的固定模板。
2.3 参数与内置变量
除了 $ARGUMENTS,Commands还支持按位置取参数。在调用时用 $1、$2 分别获取第一、第二个参数。比如定义一个 commit.md 命令:
markdown复制---
description: 生成符合规范的提交信息
argument-hint: <类型> <简要描述>
---
请根据当前git diff生成一条提交信息。
要求:
- 类型为 $1
- 描述围绕 $2 展开
- 使用Conventional Commits格式
- 语言用中文
用户输入 /commit feat 用户登录模块,那么 $1 就是 feat,$2 就是 用户登录模块,Claude会据此生成一条具体而规范的提交信息。这种用法在多参数场景下尤其好用,比让用户自由输入再让Claude自己解析要稳定得多。
另外还有几个环境变量也会经常出现在命令模板里,比如 $CLAUDE_PROJECT_DIR 代表当前项目根目录的绝对路径。如果命令涉及文件操作,用它来拼路径比写相对路径更可靠,尤其是在工作目录不固定的场景下。
2.4 我每天都在用的几个命令模板
分享几个我比较常用的命令,你可以直接复制到自己项目里改。
代码审查命令(上面已经写过,这里不再重复)
提交信息命令:
markdown复制---
description: 生成符合Conventional Commits规范的提交信息
argument-hint: <类型> <可选:简述>
allowed-tools: Bash, Read
---
读取当前git status和git diff,生成一条提交信息。
规则:
1. 格式:类型(影响范围): 简述
2. 类型必须是 feat/fix/docs/refactor/test/chore 之一
3. 简述不超过50字,用中文
4. 如果用户给了额外描述,约束为:$ARGUMENTS
5. 直接输出提交信息正文,不要输出解释
代码解释命令:
markdown复制---
description: 深入解释指定代码的设计意图
argument-hint: <目标文件或函数>
allowed-tools: Read, Grep, Glob
---
请深入解释 $ARGUMENTS 这段代码。
不要只列出代码做了什么,要讲清楚:
1. 这段代码要解决什么问题,为什么这么设计
2. 调用链上下游是什么
3. 哪些地方容易踩坑
4. 如果我来重构,你有何建议
输出控制在400字以内,用中文,先给结论再展开。
这几个命令解决的是我日常最高频的动作。特别推荐把“提交信息”做成命令,因为每次手动描述git diff很费劲,而且团队规范一旦定下来,用命令固化就人人一致了。
3. Hooks系统配置与实战
3.1 触发时机:先看懂事件类型
Hooks的配置核心是“事件”。Claude Code在运行过程中会触发一系列生命周期事件,你可以在这些事件上挂脚本。我挑最常用、也最值得用的几个来说:
PreToolUse:Claude调用某个工具之前触发。这是最常用的“拦截点”,适合做安全检查、白名单校验、数据备份。如果脚本以非0退出码结束,会阻止工具调用。PostToolUse:Claude调用某个工具之后触发。适合自动跑测试、格式化代码、收集执行结果。UserPromptSubmit:用户提交消息之后、Claude开始处理之前触发。适合把当前上下文、分支信息注入到对话里,或者检查用户输入是否包含敏感词。Notification:Claude需要用户审批或等待输入时触发。适合做提醒通知。Stop:Claude完成一次回复之后触发。适合做会后检查,比如自动跑一遍全量测试。
每个事件还可以配合matcher来精确指定匹配哪些工具。比如 PreToolUse 的 matcher 可以设成 Read、Edit、Write、Bash,甚至用 * 匹配所有工具。这意味着你可以只对“编辑文件”做检查,而不影响其他操作。
从功能角度看,这就像给工具调用做了一层AOP切面编程。用过Spring的同学应该秒懂——在方法执行前后织入额外逻辑,只不过这里的“方法”换成了Claude的工具调用。
3.2 配置文件与基础结构
Hooks的配置主要有两个地方:
- 项目级:项目根目录
.claude/settings.json中的hooks字段。 - 用户级:
~/.claude/settings.json中的hooks字段。
项目级会跟着仓库同步,适合团队统一的安全检查;用户级只对当前用户生效,适合个人偏好。
一个典型的配置结构长这样:
json复制{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node ~/.claude/scripts/after-edit.mjs",
"timeout": 30
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/scripts/check-prompt.sh",
"timeout": 10
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "npm run check",
"timeout": 120
}
]
}
]
}
}
结构看起来不复杂,几个关键点:
matcher:匹配哪些工具,多个工具用|分隔,支持通配符。type:目前核心是command,表示执行一条命令。command:要执行的完整命令。timeout:超时时间,单位秒。超时后会中断这个Hook。
注意在 UserPromptSubmit 事件下,matcher 通常留空字符串就行,因为它是按事件本身触发的,不匹配某个特定工具。
3.3 三个可以直接抄的Hook脚本
场景一:编辑文件前自动备份
这个脚本解决核心改动不可回滚的问题。每次Claude准备用Edit或Write工具时,先把原始文件复制到 .claude/backups/ 目录:
bash复制#!/usr/bin/env bash
# ~/.claude/scripts/backup-before-edit.sh
# 通过stdin接收事件JSON,从中解析出目标文件路径
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')
if [[ -n "$file_path" && -f "$file_path" ]]; then
backup_dir=".claude/backups/$(date +%Y%m%d%H%M%S)"
mkdir -p "$backup_dir"
if [[ "$file_path" == /* ]]; then
cp "$file_path" "$backup_dir/" 2>/dev/null
else
mkdir -p "$backup_dir/$(dirname "$file_path")"
cp "$file_path" "$backup_dir/$file_path" 2>/dev/null
fi
fi
exit 0
配置里挂在 PreToolUse 事件上:
json复制{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/scripts/backup-before-edit.sh",
"timeout": 10
}
]
}
]
}
}
这里我用了 jq 来解析JSON,如果你机器上没装jq,可以直接用 node 脚本或者 python 处理。
场景二:写代码后自动跑eslint
每次Claude改完文件,马上跑一次eslint,有错就中断,让Claude继续修:
json复制{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx eslint . --quiet",
"timeout": 60
}
]
}
]
}
}
这个配置的巧妙之处在于:PostToolUse 事件里如果命令以非0退出码结束,Claude会拿到这个失败信号,通常会自动尝试修复问题。这等于给Claude加了一层“自我纠错”的闭环——它改完,lint发现错误,系统反馈给它,它继续改,直到通过。
场景三:每次回复前注入当前git变更
Claude在做多轮对话时,经常忘记当前的代码状态。我在 UserPromptSubmit 事件里挂一个脚本,把当前分支的变更摘要自动注入下一轮上下文:
bash复制#!/usr/bin/env bash
# ~/.claude/scripts/git-context.sh
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
files=$(git diff --name-only 2>/dev/null | head -20)
echo "=== 当前上下文 ==="
echo "分支: $branch"
echo "变更文件:"
echo "$files"
echo "===================="
exit 0
这里有个小技巧:在这个事件里,Hook脚本的stdout会作为额外上下文追加给Claude,所以脚本输出什么,Claude就能“看到”什么。就这样,每次用户发消息时,Claude都能先看到当前的git状态,不会出现改了文件却不知道自己在哪个分支的情况。
3.4 stdout、超时与退出码:Hook的几个关键细节
这三个细节决定了你的Hook是“好用”还是“坑人”。
stdout:同一条Hook命令的stdout会被Claude读取。在 UserPromptSubmit 和 PostToolUse 场景下,输出会成为上下文的一部分;在 PreToolUse 场景下,输出会作为工具调用的额外信息。如果你不想让输出干扰对话,记得把无关信息写到文件而不是打到stdout。
退出码:这是Hook的“决策信号”。在 PreToolUse 里非0会阻止工具调用;在 PostToolUse 里非0会中断流程并把问题反馈给Claude;在 Stop 里非0会向用户展示错误并阻止“完成”。理解这个语义,你就可以设计出精确的控制流。
timeout:每个Hook都有超时限制,默认几十秒,具体超时行为是:超过时间后Hook被强制终止,并按失败处理。注意有些耗时操作(比如全量测试)要调大timeout,我之前就踩过测试跑了超过默认超时被kill的坑,后面第5部分细说。
4. Commands和Hooks的组合玩法
4.1 用Hook给Command补上“安全兜底”
Commands解决了“让Claude按标准干活”,但标准之外还有意外。Hooks可以给命令加一道兜底。比如我团队里有一个 /commit 命令,用来生成提交信息。理论上Claude只会生成文本,但保不齐它哪天手滑调用了 git commit 自己提交了。这时候挂一个 PreToolUse 拦截:
json复制{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/scripts/check-git-commit.sh",
"timeout": 5
}
]
}
]
}
}
脚本检查Bash命令里有没有 git commit,一旦发现就拒绝执行,提示用户走 git commit 命令或者让Claude只生成提交信息文本。这样即使用户用了 /commit,也不会出现Claude擅自杀入git流程的意外。
4.2 把团队工作流沉淀成项目规范
Commands和Hooks组合起来,可以做一条完整的“开发提交流水线”:
- 开发者发出
/task 实现xxx功能; UserPromptSubmitHook把当前分支、变更文件、相关issue链接自动注入上下文;- Claude开始工作,修改文件前由
PreToolUseHook触发备份; - Claude每次改完文件,
PostToolUseHook跑一遍单测和lint,挂了就强制让Claude继续修复; - 开发者让Claude跑
/review,allowed-tools限定只能读不能写,避免“审查者”和“实施者”身份混淆; - 最后
/commit生成规范提交信息。
这条流水线不需要任何人盯着,每个环节都有“机制”而不是“希望”。我特别建议团队刚引入Claude Code时,先用两条Hook(备份+lint)和两条Command(code review+commit message)搭个最小闭环,跑顺了再逐步增加。一上来搞太多规则,反而会让Claude频繁被打断,体验很差。
4.3 别让Hook变成性能黑洞
Hook虽然好用,但它是有代价的。每执行一次,都要起一个外部进程,如果脚本写得臃肿,Claude的响应速度会肉眼可见地变慢。
我总结了几条经验:
- 能合并就合并:同一事件的多个检查,尽量写进同一个脚本,不要写五个Hook各自执行一次。
- 加缓存:像目录扫描、依赖检查这类耗时操作,结果半小时内基本不变,直接缓存到临时文件,不要每次重算。
- 精确定位matcher:
PostToolUse匹配Edit|Write就好,不要懒省事用*匹配所有工具,否则连读文件都会触发,白白增加开销。 - 命令要幂等:Hook可能被重复触发,你的脚本必须保证多次执行结果一致,不会产生“备份了两次”“重复注入”这类副作用。
5. 常见问题与排查技巧
5.1 高频问题速查表
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 斜杠命令敲了没反应 | 文件名、路径、权限有问题 | 检查 .claude/commands/ 下的文件名是否对应,文件是否有可读权限 |
命令的 $ARGUMENTS 不生效 |
命令正文里变量名写错,或参数没传 | 确认调用格式是 /命令 参数,变量名用 $ARGUMENTS 全大写 |
| 命令没有出现在提示列表里 | frontmatter解析失败 | 检查YAML格式,尤其是冒号后面是否有空格 |
| Hook完全不触发 | matcher写错或事件名错误 | 确认事件名大小写是否准确(如 PostToolUse),matcher是否匹配目标工具 |
| Hook触发了但没效果 | 脚本退出码为0但逻辑没执行 | 手动跑一遍脚本命令,看有没有报错 |
| PostToolUse跑测试失败但流程没停 | 退出码没传出来 | 检查脚本最后一行是否把测试结果作为退出码return |
| 回复速度明显变慢 | Hook过多或命令太重 | 减少Hook数量,给脚本加缓存,扩大timeout |
5.2 从日志定位一次Hook失效
有一次我配置了 PreToolUse 的备份Hook,发现备份目录里空无一物。排查思路是这样的:
先确认Hook到底有没有触发。Claude Code在 ~/.claude/logs/ 下有运行日志,我查了当天的日志,确认 PreToolUse 确实被调用了,说明事件和matcher没问题。
再看脚本本身。手动执行:
bash复制echo '{"tool_input":{"file_path":"src/index.ts"}}' | bash ~/.claude/scripts/backup-before-edit.sh
结果发现脚本里 cat 读stdin后,管道里的JSON被消费掉了,后面 jq 解析时输入已经为空,自然取不到文件路径。问题不在Claude也不在配置,是我的脚本stdin处理写错了。
这个案例说明一个排查原则:先确认层级,再怀疑配置,最后怀疑脚本。顺序是:事件是否触发 -> matcher是否匹配 -> 命令是否可执行 -> 脚本逻辑是否正确。按这个顺序来,基本十分钟内能定位问题。
5.3 易错点与习惯建议
最后说几个我反复踩的坑。
坑一:把耗时命令挂在 PreToolUse 上。 我曾经在编辑文件前触发一次全量构建,结果每次改代码都要等几十秒,Claude的各种操作变得极其拖沓。后来改成只做快速检查(文件白名单、语法高亮校验),花时间的检查放到 PostToolUse 或者 Stop 阶段。
坑二:脚本里依赖的全局命令在Hook环境里不存在。 Claude Code执行Hook时的PATH可能和你终端里的不一样。比如你终端里有nvm管理的node版本,但Hook执行时用的是系统node,版本对不上导致脚本报错。尽量在命令里写绝对路径,或者用 bash -lc 加载用户shell环境。
坑三:让Hook输出大量内容到stdout。 特别是 UserPromptSubmit 事件,脚本输出会全部注入上下文。别往里面打几千行文件内容,既浪费token又干扰Claude判断。想记录日志就写入文件,别输出到stdout。
我的习惯是:每个Hook脚本开头先写一行:
bash复制echo "[hook] $(date '+%H:%M:%S') started" >> ~/.claude/logs/hook-$(basename "$0").log
把触发时间和参数简要记录到日志文件里。这样万一出问题,回头看日志就能还原现场,而不是对着黑盒发呆。
Commands和Hooks用熟了之后,你会明显感觉到Claude Code从一个“你问它答的聊天机器人”,变成了一个“能帮你守住流程下限的工程助手”。我个人最大的体会是,Hooks带来的不是效率提升那么简单,而是安全感——你敢于把更多权限交给AI去操作,因为你已经在关键路口设好了检查点。如果你刚开始接触这两个系统,先别急着配置一大堆东西,从一条备份Hook和一个 review 命令起步,跑一周感受一下,再按项目实际痛点逐步加码。
