hooks-create
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCreate Hook: Turn an Idea Into a Working Claude Code Hook
创建钩子:将想法转化为可运行的Claude Code钩子
What the user wants the hook to do: $ARGUMENTS
If that is filled in, treat it as the behavior spec and start from it. Don't re-ask what they already told you;
only ask to pin down the gaps (the exact paths / commands / patterns, and whether it must block). If it is
blank, start by asking what the hook should guarantee or do (Workflow step 1).
用户希望钩子实现的功能:$ARGUMENTS
如果该字段已填写,则将其视为行为规范并以此为起点。不要重复询问用户已说明的内容;仅询问需要明确的细节(如具体路径/命令/模式,以及是否必须执行阻止操作)。如果该字段为空,则先询问用户钩子应保障或实现的功能(工作流步骤1)。
What a hook is (30-second intro)
什么是钩子(30秒入门)
A hook is deterministic code that fires automatically on a Claude Code lifecycle event — before a tool
runs, after it runs, when the agent tries to finish, when a session starts, and so on. Unlike a skill or a
subagent, the agent does not choose to invoke a hook — it fires whether the model "remembers" or not. That is
the whole point: a rule asks the agent to behave; a hook guarantees it, at the tooling layer the model
can't talk its way around.
The user brings the idea ("the agent should never read my "); this skill writes the code and wires
it in. They don't need to know Python.
.envIt's all composition — a hook is just a small script the harness runs at a defined moment, configured in
. You're adding one more deterministic guarantee to the AI Layer.
.claude/settings.json钩子是在Claude Code生命周期事件触发时自动运行的确定性代码——包括工具运行前、运行后、Agent尝试结束时、会话启动时等场景。与skill或子Agent不同,钩子无需Agent主动调用——无论模型是否“记得”,它都会触发。这正是钩子的核心价值:规则是“要求”Agent遵守;而钩子则在工具层强制保障规则,模型无法绕过。
用户只需提出想法(如“Agent绝不能读取我的文件”);此技能负责编写代码并完成配置。用户无需掌握Python知识。
.env一切皆组合——钩子只是在特定时刻由执行框架运行的小型脚本,配置在中。你正在为AI层添加一项确定性保障。
.claude/settings.jsonThe one thing to get right: which event, and can it block?
关键要点:选择正确的事件及是否可阻止
The behavior the user wants maps to one lifecycle event. Pick by when it should fire and whether it must
stop something:
| The user wants to… | Event | Can it block? |
|---|---|---|
| Stop the agent from doing something (read a secret, edit a protected path, run a destructive command) | PreToolUse ⭐ | Yes — block the tool before it runs |
| React after an action (auto-format an edited file, log a command, inject context) | PostToolUse | No — the tool already ran; observe / format / inject only |
| Guarantee work isn't "done" until a check passes (don't stop until tests/lint/types are green) | Stop (or SubagentStop) | Yes — block the stop and send the agent back to work |
| Gate or scan the user's prompt before the model sees it | UserPromptSubmit | Yes — block the prompt; can also inject context |
| Load context every time a session starts | SessionStart | No — inject context only |
| Get notified when the agent needs you / finishes | Notification (or Stop) | No — side-effect only (desktop/Slack/sound) |
| Snapshot state before context compaction | PreCompact | No |
Pre = guarantee/gate. Post = react/log. If the user's goal is "make sure X never happens" or "don't finish until Y," it's a blocking hook (PreToolUse / Stop / UserPromptSubmit). If it's "do Z when W happens," it's an observe/react hook (PostToolUse / SessionStart / Notification).
用户所需的行为对应一种生命周期事件。根据触发时机和是否需要阻止操作来选择:
| 用户希望…… | 事件 | 是否可阻止? |
|---|---|---|
| 阻止Agent执行某些操作(读取机密文件、编辑受保护路径、运行破坏性命令) | PreToolUse ⭐ | 是 —— 在工具运行前阻止 |
| 在操作后执行响应(自动格式化编辑后的文件、记录命令、注入上下文) | PostToolUse | 否 —— 工具已运行;仅可执行观察/格式化/注入操作 |
| 确保任务在检查通过前不“结束”(直到测试/代码检查/类型校验通过才停止) | Stop(或SubagentStop) | 是 —— 阻止停止操作并让Agent继续工作 |
| 在模型看到用户提示前进行校验或扫描 | UserPromptSubmit | 是 —— 阻止提示;也可注入上下文 |
| 每次会话启动时加载上下文 | SessionStart | 否 —— 仅可注入上下文 |
| 在Agent需要你或完成任务时收到通知 | Notification(或Stop) | 否 —— 仅产生副作用(桌面/Slack/声音通知) |
| 在上下文压缩前快照状态 | PreCompact | 否 |
Pre类事件=保障/校验。Post类事件=响应/记录。 如果用户的目标是“确保X绝不会发生”或“直到Y完成才结束”,则使用可阻止型钩子(PreToolUse / Stop / UserPromptSubmit)。如果目标是“当W发生时执行Z”,则使用观察/响应型钩子(PostToolUse / SessionStart / Notification)。
Required reading (do this first)
必读步骤(首先完成)
The hook event list and the exact stdin/stdout contract evolve — don't rely on a snapshot. Before writing,
fetch the current docs and confirm the event name, its input fields, and its control protocol:
- Hooks reference: https://code.claude.com/docs/en/hooks
- Hooks guide (examples): https://code.claude.com/docs/en/hooks-guide
Use on these and verify against what you're about to write. If the fetch fails, proceed from the
canonical events in the table above and say so in your report so the user can double-check.
WebFetch钩子事件列表和标准输入输出协议会不断演进——不要依赖快照。编写前,获取最新文档并确认事件名称、输入字段和控制协议:
使用获取上述文档并与你即将编写的内容进行验证。如果获取失败,可基于上表中的标准事件继续操作,并在报告中说明,以便用户自行核对。
WebFetchThe execution protocol (how a hook talks to Claude Code)
执行协议(钩子与Claude Code的交互方式)
- Input: Claude Code passes a JSON object on stdin — always includes ,
session_id,cwd; event-specific fields likehook_event_name+tool_name(tool events),tool_input(UserPromptSubmit),prompt(SessionStart).source - Output / control:
- — allow / success. For
exit 0andUserPromptSubmit, anything printed to stdout is injected into the agent's context.SessionStart - — block. The action is prevented and whatever you print to stderr is fed back to the agent as the reason, so it adapts. (Only blocking-capable events honor this — see the table.)
exit 2 - any other exit code — non-blocking error; shown to the user, execution continues.
- Advanced (optional): instead of exit codes, print a JSON object on stdout — e.g. (Stop), or
{"decision":"block", "reason":"…"}. Prefer the simple exit-code form unless the user needs to modify input/output or inject context with{"hookSpecificOutput":{"hookEventName":"PreToolUse", "permissionDecision":"deny","permissionDecisionReason":"…"}}. Confirm field names against the fetched docs.additionalContext
- 输入:Claude Code通过**标准输入(stdin)**传递JSON对象——始终包含、
session_id、cwd;以及事件特定字段,如hook_event_name+tool_name(工具事件)、tool_input(UserPromptSubmit)、prompt(SessionStart)。source - 输出/控制:
- —— 允许操作/执行成功。对于
exit 0和UserPromptSubmit,输出到**标准输出(stdout)**的内容会被注入到Agent的上下文中。SessionStart - —— 阻止操作。该操作会被阻止,输出到**标准错误(stderr)**的内容会作为原因反馈给Agent,使其做出调整。(仅可阻止型事件支持此功能——见上表。)
exit 2 - 其他任何退出码 —— 非阻止型错误;会显示给用户,但执行会继续。
- 高级(可选):可替代退出码,在stdout输出JSON对象——例如(Stop事件),或
{"decision":"block","reason":"…"}。除非用户需要修改输入/输出或通过{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"…"}}注入上下文,否则优先使用简单的退出码形式。请根据获取的文档确认字段名称。additionalContext
Workflow
工作流
1. Understand the idea (start from $ARGUMENTS
; ask only to fill gaps)
$ARGUMENTS1. 理解需求(从$ARGUMENTS
开始;仅询问缺失信息)
$ARGUMENTSStart from what the user already described in (the user may not be technical). Pin down two things in
plain language, asking only for what is missing:
$ARGUMENTS- What should happen or be prevented, and when (before/after an action, at finish, at session start)?
- How precisely should it match? ("any file", "the
.envfolder", "migrations/", "my test command exits non-zero"). Get the concrete file paths / commands / patterns — the guarantee is only as good as what it matches, so don't guess. If the ask is vague, propose a concrete interpretation and confirm.rm -rf
从用户在中描述的内容开始(用户可能不懂技术)。用通俗语言明确两点,仅询问缺失的信息:
$ARGUMENTS- 具体要实现或阻止什么,以及触发时机(操作前/后、结束时、会话启动时)?
- 匹配规则的精确程度?(如“所有文件”、“
.env文件夹”、“migrations/命令”、“我的测试命令返回非零值”)。获取具体的文件路径/命令/模式——保障效果取决于匹配规则的准确性,请勿猜测。如果需求模糊,可提出具体的解释方案并确认。rm -rf
2. Read the docs
2. 阅读文档
Fetch the hooks reference/guide (above) and confirm the target event's name, stdin fields, and control protocol.
获取钩子参考文档/指南(如上)并确认目标事件的名称、标准输入字段和控制协议。
3. Pick the event (and matcher)
3. 选择事件(及匹配器)
Use the table to choose the single event. Choose a matcher that scopes it tightly — for tool events, the
tool name(s) (e.g. , , ); empty/ means every occurrence. Don't fire on
everything if the goal is specific.
"Bash""Edit|Write""mcp__.*""*"使用上表选择单一事件。选择匹配器来精确限定范围——对于工具事件,可指定工具名称(如、、);空值/表示匹配所有情况。如果目标明确,请勿匹配所有事件。
"Bash""Edit|Write""mcp__.*""*"4. Write the hook script
4. 编写钩子脚本
- Default to a single-file Python script at
uv(matches the pack's tooling). Use another language only if the user asks..claude/hooks/<event_snake_case>.py - Read the JSON from stdin, do the check, and:
- to block: print a clear reason to and
stderr;sys.exit(2) - to allow: (optionally print context to stdout for the injecting events).
sys.exit(0)
- to block: print a clear reason to
- Fail open. Wrap the body so any unexpected error exits (a broken hook must never brick the user's session). The only intentional non-zero exit is the deliberate
0block.exit 2 - Keep it lean and readable — the user will want to tweak the matched paths/commands later.
- If a hook already exists for that event, extend it rather than overwrite (add your check; keep theirs).
- /
Stoponly: checkSubagentStopfirst andstop_hook_activewhen it is true. Without that guard the hook blocks the stop, the agent works, tries to stop again, is blocked again — a loop.sys.exit(0)
⚠️ Running a project command from a hook (read this before writing one)
The hook itself runs under, in an isolated ephemeral environment. Its interpreter is NOT the project's interpreter and does NOT have the project's dependencies. So:uv run
- NEVER rebuild the command with
or a baresys.executablelist. That runs the hook's throwaway python, which has no pytest, no project packages, nothing. The command fails with["python", "-m", "pytest", ...]every single time — so a "don't finish until tests pass" hook blocks on green as readily as on red, and reports a nonsense reason. It looks like it works. It does not.No module named …- DO run the user's command verbatim, as a shell string, in the project directory — with uv's ephemeral venv stripped from the environment.
alone is not enough:shell=Trueputs its throwaway interpreter first onuv runand setsPATH, so even a shell command resolvesVIRTUAL_ENVto the wrong one. Copy this helper as-is:pythonPut the command in a single named constant at the top of the file so the user can edit one obvious line.pythonTEST_COMMAND = "python -m pytest -q" # exactly what the user typed; the one line they'll edit def _project_env() -> dict: """os.environ minus uv's ephemeral venv, so the project's own tools resolve.""" env = os.environ.copy() venv = env.pop("VIRTUAL_ENV", None) if venv: drop = {os.path.join(venv, "Scripts"), os.path.join(venv, "bin")} env["PATH"] = os.pathsep.join( p for p in env.get("PATH", "").split(os.pathsep) if p not in drop ) return env result = subprocess.run( TEST_COMMAND, shell=True, capture_output=True, text=True, cwd=hook_input.get("cwd"), # the project root Claude Code passes in env=_project_env(), )- If the command must run from a subdirectory (a monorepo, or a project whose test config lives deeper — e.g.
), ask for that, and pass it:app/backend/. Getting this wrong produces a hook that always blocks, which the user will read as "hooks are broken."cwd=Path(hook_input["cwd"]) / "app/backend"
- 默认使用**单文件Python脚本**,路径为
uv(与工具包的工具链匹配)。仅在用户要求时使用其他语言。.claude/hooks/<event_snake_case>.py - 从stdin读取JSON,执行检查,并:
- 若要阻止操作:向stderr输出清晰的原因并执行;
sys.exit(2) - 若要允许操作:执行(对于注入上下文的事件,可选择向stdout输出上下文内容)。
sys.exit(0)
- 若要阻止操作:向stderr输出清晰的原因并执行
- 故障开放原则:将脚本主体包裹起来,确保任何意外错误都以exit 0退出(损坏的钩子绝不能导致用户会话崩溃)。唯一有意的非零退出是主动执行来阻止操作。
exit 2 - 保持脚本简洁易读——用户后续可能需要调整匹配的路径/命令。
- 如果该事件已存在钩子,扩展现有钩子而非覆盖(添加你的检查逻辑;保留原有逻辑)。
- 仅适用于/
Stop事件:首先检查SubagentStop,如果为true则执行stop_hook_active。若无此防护,钩子会阻止停止操作,Agent继续工作,再次尝试停止时又被阻止——形成循环。sys.exit(0)
⚠️ 从钩子中运行项目命令(编写前必读)
钩子在**的隔离临时环境**中运行。其解释器并非项目的解释器,也不包含项目的依赖。因此:uv run
- 绝不要用
或纯列表形式(如sys.executable)重构命令。这会使用钩子的临时Python环境,该环境没有pytest、项目包等任何依赖。命令每次都会因["python", "-m", "pytest", ...]失败——因此“直到测试通过才停止”的钩子无论测试是否通过都会阻止停止操作,并返回无意义的原因。表面看似正常,实际无法工作。No module named …- 正确做法是完全按照用户输入的命令字符串,在项目目录中运行——并从环境变量中移除uv的临时虚拟环境。仅使用
不够:shell=True会将其临时解释器放在uv run的最前面,并设置PATH,因此即使是shell命令也会解析到错误的Python环境。请直接复制以下辅助函数:VIRTUAL_ENV将命令放在文件顶部的单个命名常量中,方便用户修改这一行即可。pythonTEST_COMMAND = "python -m pytest -q" # 完全按照用户输入的内容;用户只需修改这一行 def _project_env() -> dict: """移除uv临时虚拟环境的os.environ,确保解析到项目自身的工具。""" env = os.environ.copy() venv = env.pop("VIRTUAL_ENV", None) if venv: drop = {os.path.join(venv, "Scripts"), os.path.join(venv, "bin")} env["PATH"] = os.pathsep.join( p for p in env.get("PATH", "").split(os.pathsep) if p not in drop ) return env result = subprocess.run( TEST_COMMAND, shell=True, capture_output=True, text=True, cwd=hook_input.get("cwd"), # Claude Code传递的项目根目录 env=_project_env(), )- 如果命令必须从子目录运行(如单体仓库,或测试配置位于深层目录的项目——例如
),请询问用户该路径,并传入:app/backend/。若此处出错,钩子会始终阻止操作,用户会认为“钩子功能损坏”。cwd=Path(hook_input["cwd"]) / "app/backend"
5. Wire it into settings.json
5. 配置到settings.json
Edit (create it if absent). Merge into any existing block — never clobber
other events or other hooks on the same event. Shape:
.claude/settings.jsonhooksjson
{
"hooks": {
"PreToolUse": [
{ "matcher": "Edit|Write|Read|Bash",
"hooks": [ { "type": "command", "command": "uv run .claude/hooks/pre_tool_use.py" } ] }
]
}
}编辑(若不存在则创建)。合并到现有块中——绝不要覆盖其他事件或同一事件的其他钩子。格式如下:
.claude/settings.jsonhooksjson
{
"hooks": {
"PreToolUse": [
{ "matcher": "Edit|Write|Read|Bash",
"hooks": [ { "type": "command", "command": "uv run .claude/hooks/pre_tool_use.py" } ] }
]
}
}6. Prove it yourself, then explain and warn
6. 自行验证,然后解释并提醒
Run the hook before you hand it over. Feed it a sample event on stdin and check the exit code — do not ship a
hook you have only read. A hook that always blocks, or never blocks, looks identical to a working one until it
fires at the wrong moment.
bash
undefined交付前先运行钩子。向stdin传入示例事件并检查退出码——不要交付仅通过阅读验证的钩子。始终阻止或始终允许的钩子,在错误时机触发前看起来与正常钩子无异。
bash
undefinedshould ALLOW (exit 0)
应该允许(exit 0)
echo '{"session_id":"t","cwd":"<project-root>","hook_event_name":"Stop","stop_hook_active":false}' | uv run .claude/hooks/stop.py; echo "exit=$?"
- For a **command-running hook** (tests/lint/types), this is mandatory and you must check **both** directions:
it exits 0 while the command passes, and exits 2 once it genuinely fails. If it exits 2 in both states, the
command is not resolving — re-read the warning in step 4 about `sys.executable`.
- For a **blocking guard**, feed it one payload that should be blocked and one that should pass.
- If a check comes back wrong, fix the script and re-run before reporting success.
Then:
- Tell the user **what you built**, in plain words: which event, what it guarantees, and the one line they'd
change to adjust it.
- Give them a **way to prove it** in the agent: for a blocking hook, an action that *should* be blocked ("ask me
to read the env file — watch it refuse"); for an observe hook, where the output lands (the log, the notification).
- Report what you verified, and say plainly if you could not verify something.
- **Security note (always say this):** a hook runs arbitrary code automatically, with your credentials, on every
matching event. Review hooks like you review CI config; only run hooks you trust. (Same caution as MCP
servers.)echo '{"session_id":"t","cwd":"<project-root>","hook_event_name":"Stop","stop_hook_active":false}' | uv run .claude/hooks/stop.py; echo "exit=$?"
- 对于**运行命令的钩子**(测试/代码检查/类型校验),此步骤是必须的,且必须检查**两种情况**:命令通过时返回0,命令真正失败时返回2。如果两种情况都返回2,说明命令解析错误——请重新阅读步骤4中关于`sys.executable`的警告。
- 对于**阻止型防护钩子**,传入一个应被阻止的负载和一个应被允许的负载进行测试。
- 如果检查结果不符合预期,修复脚本并重新运行,再报告成功。
然后:
- 用通俗语言告诉用户**你构建了什么**:使用的事件、实现的保障,以及用户可调整的关键代码行。
- 给用户**验证方法**:对于阻止型钩子,提供一个应被阻止的操作(如“让我读取env文件——看它是否拒绝”);对于观察型钩子,说明输出位置(日志、通知)。
- 报告你已验证的内容,并明确说明无法验证的部分。
- **安全提示(必须告知)**:钩子会在每次匹配事件时自动运行任意代码,并使用你的凭据。请像审查CI配置一样审查钩子;仅运行你信任的钩子。(与MCP服务器的注意事项相同。)Quality checks
质量检查
- ✅ The behavior maps to the right event, and a blocking goal uses a blocking-capable event (PreToolUse / Stop / UserPromptSubmit) — not PostToolUse.
- ✅ The matcher is scoped to what the user actually meant (not firing on everything by accident).
- ✅ The script fails open — any error exits 0; the only is the intended block, with a clear stderr reason.
exit 2 - ✅ Any project command runs verbatim via in the project
shell=True— never rebuilt withcwdor a baresys.executable(the hook's own interpreter has none of the project's dependencies).python - ✅ A /
Stophook honorsSubagentStopso it cannot loop.stop_hook_active - ✅ was merged, not overwritten; existing hooks still present.
settings.json - ✅ You ran the hook and confirmed it exits 0 when it should allow and 2 when it should block — not just read it.
- ✅ The user got a plain-English explanation + a test + the security note.
- ✅ 行为与正确的事件匹配,且需要阻止的目标使用了可阻止型事件(PreToolUse / Stop / UserPromptSubmit)——而非PostToolUse。
- ✅ 匹配器范围精确,符合用户实际需求(不会意外匹配所有事件)。
- ✅ 脚本遵循故障开放原则——任何错误都返回0;唯一的是有意的阻止操作,且带有清晰的stderr原因。
exit 2 - ✅ 所有项目命令都通过在项目
shell=True中按原命令运行——绝不要用cwd或纯sys.executable重构(钩子自身的解释器没有任何项目依赖)。python - ✅ /
Stop钩子遵守**SubagentStop**规则,避免循环。stop_hook_active - ✅ 是合并而非覆盖;现有钩子仍保留。
settings.json - ✅ 你已运行钩子并确认允许时返回0、阻止时返回2——而非仅通过阅读验证。
- ✅ 用户收到了通俗解释+测试方法+安全提示。
Notes
注意事项
- Hooks are the deterministic floor of the AI Layer — use them for the non-negotiables (secrets, protected paths, "don't finish until green"), not for things a rule or skill handles well enough.
- A blocking hook's coverage is only as good as its matcher — it guarantees the hook runs, but you decide
what it catches. Be honest with the user about the edges (e.g. a matcher won't catch a base64'd read).
.env - Keep hooks fast — they run on the matched event every time. Heavy work belongs in an async hook or a skill.
- 钩子是AI层的确定性底线——用于处理不可协商的需求(机密保护、路径防护、“直到通过才结束”),而非规则或skill可胜任的场景。
- 阻止型钩子的覆盖范围仅取决于匹配器——它能保障钩子运行,但你需要决定它能拦截哪些内容。请如实告知用户边缘情况(如匹配器无法捕获base64编码的读取操作)。
.env - 保持钩子快速——它会在每次匹配事件时运行。繁重的工作应放在异步钩子或skill中。",