第 5 章:skill 进阶(hooks 与 slash-command)
给 skill 加上参数和工具权限,再用 hooks 与 slash-command 接成一套可重复的审查工作流。
小满 · 机关廊
光有手艺还不够。今天你给小满装上会自己触发的机关。
草稿章节。跑通格式用的第一版,正式索引前会再打磨。
本章目标
把第 4 章那个被动触发的单一 skill,升级成一套可重复的工作流,做三件事。第一,给 skill 加参数,让调用方能限定范围(审查某个文件,还是相对某个基准分支的整个 diff)。第二,限制它能用哪些工具,让审查只能读、永远不能写。第三,把它绑到一个可按名触发的 slash-command,和一个在确定时机自动触发的 hook 上。这么做的好处是:同一套审查流程,三条路都能跑到。手动用 /pr-reviewer,模型在相关时自动调起,以及在工作流固定节点经由 hook 确定性地触发。
这里有个关键区别要记住:skill 是一段流程,slash-command 是调起它的一种方式,hook 是一个不依赖模型做任何决定的确定性触发器。skill 是概率性的,要模型来选;hook 是有保证的,由 harness 来跑。两个你都需要。
前置准备
- 完成第 4 章:一个 agent 已能发现的、可用的
pr-reviewerskill。 - 你的 agent 支持 skill frontmatter 字段、slash-command 和 hooks。下面用到的字段名只是照文档形态举的例子,会随版本变,最终以官方文档为准。
- 一个可以安全测试自动触发、又不会刷屏真实 PR 的仓库。
- hook 那一步需要
jq来解析 hook 的 JSON 输入。
动手做
1. 给 skill 加参数
改 SKILL.md,让指令接受调用方给出的明确范围,而不是每次都审查全部。skill 支持参数替换:$ARGUMENTS 展开为命令名之后输入的全部内容,$1、$2(或具名参数)取出对应位置。声明一个 argument-hint,让自动补全告诉调用方该传什么。
---
name: pr-reviewer
description: >-
审查一个 pull request 或 diff 的正确性、测试、安全与清晰度。
当用户要求 review 一个 PR、审查改动、或检查 diff 时使用。
argument-hint: "[路径或基准分支]"
allowed-tools: Read, Grep, Bash(git diff *), Bash(gh pr diff *)
---
# PR 审查助手
从参数判断范围:
- 若 $1 看起来像路径,只审查该文件的改动。
- 若 $1 看起来像分支(如 main),审查相对它的 diff。
- 若 $ARGUMENTS 为空,审查当前 PR 的完整 diff。
## 待审 diff
!`git diff ${1:-HEAD}`
(然后对上面的 diff 跑第 4 章那份四维清单)
2. 把工具权限收到最小
审查只读,绝不能写、推送或合并。在 frontmatter 的 allowed-tools 字段里,只列出 skill 可以不经询问就使用的工具。这里有个容易踩的点:allowed-tools 是预批准所列工具,让 agent 不停下来问你,但它本身并不会移除其它工具。要真正封掉危险操作,得配合权限设置里的 deny 规则,或者在 disallowed-tools 里把它们列出来。
# SKILL.md frontmatter 里:只预批准读取类工具
allowed-tools: Read, Grep, Bash(git diff *), Bash(gh pr diff *)
// .claude/settings.json 里:全局硬禁写/合并(示意)
{
"permissions": {
"deny": [
"Bash(git push *)",
"Bash(git commit *)",
"Bash(gh pr merge *)"
]
}
}
这里收紧权限不是走形式。一个根本推不了代码的审查助手,你才敢让它自动跑而不用一直盯着。
3. 加一个 slash-command
因为文件夹名就是命令,你放在 .claude/skills/pr-reviewer/SKILL.md 的 skill 已经能用 /pr-reviewer 调起。队友一句话就能跑起完全相同的审查,把范围作为参数传入。若想让命令只能手动触发(模型永不自动触发),加上 disable-model-invocation: true。
/pr-reviewer # 审查当前整个 diff
/pr-reviewer src/auth.py # 只审查某个文件的改动
/pr-reviewer main # 审查相对 main 的 diff
4. 接一个 hook,做确定性的自动审查
hook 在某个确定的生命周期时机运行,与模型怎么决定无关。在 settings.json 里按你关心的事件配置它。一个常见形态:在 git 提交工具的 PreToolUse 上,跑一个脚本,在提交落地前触发审查。hook 通过退出码(exit 0 放行,exit 2 阻止)或 stdout 上的结构化 JSON 回传结果。
// .claude/settings.json(示意;事件/字段名以文档为准)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git commit *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/review-before-commit.sh"
}
]
}
]
}
}
#!/bin/bash
# review-before-commit.sh:审查发现阻塞项就拦下提交(示意)
DIFF=$(git diff --cached)
if [ -z "$DIFF" ]; then exit 0; fi # 没有暂存内容,没什么可审
# 对暂存的 diff 跑你的审查助手;假设它写出一份结论文件
# 若发现阻塞项,拒绝提交并告诉模型原因
if grep -q "REQUEST CHANGES" review-verdict.txt; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "审查发现阻塞项。提交前先修掉。"
}
}'
else
exit 0
fi
如果你是用 SDK 在进程内跑这个 agent,同样这道护栏可以直接用代码挂上去,不用走外部脚本。Python 用 HookMatcher 把回调注册到 PreToolUse 事件上,返回 permissionDecision: "deny" 来拦截;TypeScript 在 hooks.PreToolUse 里挂一个函数,返回 { decision: "block", continue: false }。
import os
import anyio
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, HookMatcher
async def review_before_commit(input_data, tool_use_id, context):
if input_data["tool_name"] != "Bash":
return {}
command = input_data["tool_input"].get("command", "")
if "git commit" not in command:
return {} # 只在 git commit 上出手,不一刀切拦所有 Bash
verdict = open("review-verdict.txt").read() if os.path.exists("review-verdict.txt") else ""
if "REQUEST CHANGES" in verdict:
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "审查发现阻塞项。提交前先修掉。",
}
}
return {} # 干净,放行提交
options = ClaudeAgentOptions(
allowed_tools=["Read", "Grep", "Bash"],
hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[review_before_commit])]},
)
async def main():
async with ClaudeSDKClient(options=options) as client:
await client.query("修一下退款的差一错误并提交。")
async for msg in client.receive_response():
print(msg)
anyio.run(main)
import { query } from "@anthropic-ai/claude-agent-sdk";
import type { HookJSONOutput } from "@anthropic-ai/claude-agent-sdk";
import { existsSync, readFileSync } from "fs";
const q = query({
prompt: "修一下退款的差一错误并提交。",
options: {
allowedTools: ["Read", "Grep", "Bash"],
hooks: {
PreToolUse: [
{
matcher: "Bash",
hooks: [
async (input: any): Promise<HookJSONOutput> => {
const command = input.tool_input?.command ?? "";
if (input.tool_name !== "Bash" || !command.includes("git commit")) {
return { continue: true }; // 只在 git commit 上出手
}
const verdict = existsSync("review-verdict.txt")
? readFileSync("review-verdict.txt", "utf8")
: "";
if (verdict.includes("REQUEST CHANGES")) {
return {
decision: "block",
stopReason: "审查发现阻塞项。提交前先修掉。",
continue: false,
};
}
return { continue: true }; // 干净,放行提交
},
],
},
],
},
},
});
for await (const message of q) {
console.log(message);
}
5. 让它可重复,并确认各条路都落到同一个 skill
这样组合起来,好处是只有一个事实来源。slash-command、模型的自动调起、hook 都应该驱动同一份 SKILL.md 流程,所以不管从哪条路起,审查结果都一样。拿一处真实改动,从每条路各走一遍,比对一下输出长什么样。
路 A:输入 /pr-reviewer main -> 同一份清单,范围是相对 main 的 diff
路 B:说"审查我的改动" -> 模型加载 skill,同一份清单
路 C:尝试 git commit -> hook 触发,同一份清单,可拦截
习得「条件反射」你没开口说审查,小满也会在你 git commit 那一刻被 hook 自动叫起来跑同一份审查,发现阻塞项就拦下提交,不用你每次记得手动喊它。
如何验证
- 调起
/pr-reviewer src/auth.py,确认审查只覆盖该文件的改动,证明参数限定了范围。 - 观察一次审查中的工具调用,确认只有读取类工具触发。尝试一次写入,确认 deny 规则拦下它。
- 暂存一个带故意阻塞项的改动并尝试提交。确认 hook 不用提示就触发,并以原因文本拦下提交。
- 暂存一个干净改动并提交。确认 hook 放行(exit 0),证明触发是被限定的,而非一刀切地全拦。
习得「确认收放有度」你能暂存一个带阻塞项的改动看 hook 拦下提交,再暂存一个干净改动看它放行,确认这个自动拦截只在该拦的时候拦,不会把每一次提交都拦下来。
小结
你的审查助手现在可以拼装了。参数限定范围,allowed-tools 加 deny 规则定好边界,文件夹名给你一个 slash-command,PreToolUse hook 确定性地把它自动跑起来。记住这个心智模型:skill 是那唯一的流程;slash-command 和模型调起是通向它的两条概率性入口;hook 是接在生命周期事件上、有保证会触发的那条。正是把这几样分开,它才成了一套工作流,而不是一次性的提示词。下一章升级到 subagent 与编排,会讲清楚多 agent 什么时候帮你、什么时候坑你。
常见坑
- 权限给太宽。 预批准所有工具,或者忘了
allowed-tools并不会移除工具,结果一个”审查助手”也能推送或合并。把最小权限的allowed-tools和显式 deny 规则配在一起用。 - hook 触发太频。 matcher 太松的 hook 会在每次工具调用时都跑一遍审查,烦死你。用
if条件把它限定到具体的工具和命令(Bash(git commit *)),而不是整个Bash。 - 参数藏着或没传。 如果 skill 假设了一个调用方从来不设的范围,结果就会跑偏。用
$ARGUMENTS/$1把范围写明,再用argument-hint标出来。 - 拦截的方式不对。 一个非零退出、但消息没什么用的 hook,只会把 agent 弄糊涂。用 exit 2,或者一个带清楚
permissionDecisionReason的结构化 deny 决定,让模型知道该修什么。
装上 hook,小满第一次在你没开口时自动做了件事,pre-commit 替你拦了一手,像宠物学会你一进门就叼来拖鞋。又惊喜,又有点失控。机关廊,亮了。
刚点亮 机关廊 · 地图已点亮 6 / 16
来源
- Anthropic Agent Skills 官方文档 · official
- Claude Code hooks 参考文档 · official