customaize-agent%3Acreate-hook by neolabhq/context-engineering-kit
npx skills add https://github.com/neolabhq/context-engineering-kit --skill customaize-agent:create-hook包含钩子
此技能使用 Claude 钩子,这些钩子可以自动执行代码以响应事件。安装前请仔细审查。
分析项目,建议实用的钩子,并使用适当的测试创建它们。
自动检测项目工具链并建议相关钩子:
当检测到 TypeScript (tsconfig.json) 时:
当检测到 Prettier (.prettierrc, prettier.config.js) 时:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
.eslintrc.*) 时:当 package.json 包含脚本时:
test 脚本 → "提交前运行测试"build 脚本 → "提交前验证构建"当检测到 git 仓库时:
决策树:
Project has TypeScript? → Suggest type checking hooks
Project has formatter? → Suggest formatting hooks
Project has tests? → Suggest test validation hooks
Security sensitive? → Suggest security hooks
+ Scan for additional patterns and suggest custom hooks based on:
- Custom scripts in package.json
- Unique file patterns or extensions
- Development workflow indicators
- Project-specific tooling configurations
首先询问:"这个钩子应该做什么?" 并提供你分析得出的相关建议。
然后根据用户的描述理解上下文,只询问你不确定的细节:
触发时机:何时应该运行?
PreToolUse:文件操作之前(可以阻止)PostToolUse:文件操作之后(反馈/修复)UserPromptSubmit:处理请求之前工具匹配器:哪些工具应该触发它?(Write, Edit, Bash, * 等)
作用域:global, project, 或 project-local
响应方式:
阻止行为(如果相关):"当发现问题时,是否应该停止操作?"
Claude 集成(关键):"Claude Code 是否应该自动查看并修复此钩子检测到的问题?"
additionalContext 进行错误通信suppressOutput: true 进行静默操作上下文污染:"成功的操作是否应该静默以避免噪音?"
文件过滤:"此钩子应该处理哪些文件类型?"
你应该:
~/.claude/hooks/ 或 .claude/hooks/$CLAUDE_PROJECT_DIR 引用项目根目录关键实现标准:
additionalContext/systemMessage 与 Claude 通信suppressOutput: true⚠️ 关键:输入/输出格式
这是大多数钩子实现失败的地方。请特别注意:
关键:测试成功路径和失败路径:
成功路径测试:
失败路径测试: 2. 测试预期的失败场景 - 创建钩子应该失败/警告的条件
验证步骤: 3. 验证预期行为:检查是否按预期阻止/警告/提供上下文
示例测试流程:
如果出现问题,你应该:
chmod +x)#!/usr/bin/env node
// Read stdin JSON, check .ts/.tsx files only
// Run: npx tsc --noEmit --pretty
// Output: JSON with additionalContext for errors
#!/usr/bin/env node
// Read stdin JSON, check supported file types
// Run: npx prettier --write [file]
// Output: JSON with suppressOutput: true
#!/bin/bash
# Read stdin JSON, check for secrets/keys
# Block if dangerous patterns found
# Exit 2 to block, 0 to continue
常见模式:
JSON.parse(process.stdin.read()){continue: true, suppressOutput: true}{continue: true, additionalContext: "error details"}exit(2)按用例分类的钩子类型:
钩子执行最佳实践:
✅ 钩子成功创建时:
结果:用户获得一个有效的钩子,通过智能自动化和质量检查增强其开发工作流程。
文档索引
获取完整的文档索引:https://code.claude.com/docs/llms.txt 在进一步探索之前,使用此文件发现所有可用页面。
当 Claude Code 编辑文件、完成任务或需要输入时,自动运行 shell 命令。格式化代码、发送通知、验证命令并强制执行项目规则。
钩子是用户定义的 shell 命令,在 Claude Code 生命周期的特定点执行。它们提供了对 Claude Code 行为的确定性控制,确保某些操作总是发生,而不是依赖 LLM 选择运行它们。使用钩子来强制执行项目规则、自动化重复任务,并将 Claude Code 与现有工具集成。
对于需要判断而非确定性规则的决策,你也可以使用基于提示的钩子或基于代理的钩子,这些钩子使用 Claude 模型来评估条件。
创建钩子的最快方法是通过 Claude Code 中的 /hooks 交互式菜单。本教程创建一个桌面通知钩子,这样每当 Claude 等待你的输入时,你都会收到警报,而无需监视终端。
<Tabs>
<Tab title="macOS">
使用 [`osascript`](https://ss64.com/mac/osascript.html) 通过 AppleScript 触发原生 macOS 通知:
```
osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'
```
</Tab>
<Tab title="Linux">
使用 `notify-send`,该命令预装在大多数带有通知守护进程的 Linux 桌面上:
```
notify-send 'Claude Code' 'Claude Code needs your attention'
```
</Tab>
<Tab title="Windows (PowerShell)">
使用 PowerShell 通过 .NET 的 Windows Forms 显示原生消息框:
```
powershell.exe -Command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')"
```
</Tab>
</Tabs>
钩子让你可以在 Claude Code 生命周期的关键点运行代码:编辑后格式化文件、执行前阻止命令、Claude 需要输入时发送通知、会话开始时注入上下文等。有关钩子事件的完整列表,请参阅 钩子参考。
每个示例都包含一个可立即使用的配置块,你可以将其添加到设置文件中。最常见的模式:
每当 Claude 完成工作并需要你的输入时,获取桌面通知,这样你无需检查终端即可切换到其他任务。
此钩子使用 Notification 事件,该事件在 Claude 等待输入或许可时触发。下面的每个选项卡使用平台的本地通知命令。将其添加到 ~/.claude/settings.json,或使用上面的交互式教程通过 /hooks 配置它:
自动在 Claude 编辑的每个文件上运行 Prettier,这样格式化保持一致,无需手动干预。
此钩子使用带有 Edit|Write 匹配器的 PostToolUse 事件,因此它仅在文件编辑工具后运行。该命令使用 jq 提取编辑的文件路径并将其传递给 Prettier。将其添加到项目根目录的 .claude/settings.json 中:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
防止 Claude 修改敏感文件,如 .env、package-lock.json 或 .git/ 中的任何内容。Claude 会收到解释编辑被阻止原因的反馈,以便调整其方法。
此示例使用钩子调用的单独脚本文件。该脚本根据受保护模式列表检查目标文件路径,并以代码 2 退出以阻止编辑。
```bash theme={null}
#!/bin/bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0
```
```bash theme={null}
chmod +x .claude/hooks/protect-files.sh
```
```json theme={null}
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
```
当 Claude 的上下文窗口填满时,压缩会总结对话以释放空间。这可能会丢失重要细节。使用带有 compact 匹配器的 SessionStart 钩子,在每次压缩后重新注入关键上下文。
你的命令写入 stdout 的任何文本都会添加到 Claude 的上下文中。此示例提醒 Claude 项目约定和最近的工作。将其添加到项目根目录的 .claude/settings.json 中:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
}
]
}
]
}
}
你可以将 echo 替换为任何产生动态输出的命令,例如 git log --oneline -5 以显示最近的提交。对于在每次会话开始时注入上下文,请考虑改用 CLAUDE.md。对于环境变量,请参阅参考中的 CLAUDE_ENV_FILE。
钩子事件在 Claude Code 生命周期的特定点触发。当事件触发时,所有匹配的钩子并行运行,并且相同的钩子命令会自动去重。下表显示了每个事件及其触发时机:
| 事件 | 触发时机 |
|---|---|
SessionStart | 会话开始或恢复时 |
UserPromptSubmit | 你提交提示时,在 Claude 处理之前 |
PreToolUse | 工具调用执行之前。可以阻止它 |
PermissionRequest | 权限对话框出现时 |
PostToolUse | 工具调用成功后 |
PostToolUseFailure | 工具调用失败后 |
Notification | Claude Code 发送通知时 |
SubagentStart | 子代理生成时 |
SubagentStop | 子代理完成时 |
Stop | Claude 完成响应时 |
PreCompact | 上下文压缩之前 |
SessionEnd | 会话终止时 |
每个钩子都有一个 type,决定其运行方式。大多数钩子使用 "type": "command",它运行一个 shell 命令。另外两个选项使用 Claude 模型来做决策:"type": "prompt" 用于单轮评估,"type": "agent" 用于具有工具访问权限的多轮验证。有关详细信息,请参阅基于提示的钩子和基于代理的钩子。
钩子通过 stdin、stdout、stderr 和退出代码与 Claude Code 通信。当事件触发时,Claude Code 将特定于事件的 JSON 数据传递给你的脚本的 stdin。你的脚本读取该数据,执行其工作,并通过退出代码告诉 Claude Code 下一步该做什么。
每个事件都包含 session_id 和 cwd 等公共字段,但每种事件类型添加不同的数据。例如,当 Claude 运行 Bash 命令时,PreToolUse 钩子在 stdin 上收到类似这样的内容:
{
"session_id": "abc123", // 此会话的唯一 ID
"cwd": "/Users/sarah/myproject", // 事件触发时的工作目录
"hook_event_name": "PreToolUse", // 哪个事件触发了此钩子
"tool_name": "Bash", // Claude 即将使用的工具
"tool_input": { // Claude 传递给工具的参数
"command": "npm test" // 对于 Bash,这是 shell 命令
}
}
你的脚本可以解析该 JSON 并对任何这些字段进行操作。UserPromptSubmit 钩子获取 prompt 文本,SessionStart 钩子获取 source(启动、恢复、压缩)等。有关共享字段,请参阅参考中的 通用输入字段,以及每个事件部分中特定于事件的模式。
你的脚本通过写入 stdout 或 stderr 并以特定代码退出来告诉 Claude Code 下一步该做什么。例如,一个想要阻止命令的 PreToolUse 钩子:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q "drop table"; then
echo "Blocked: dropping tables is not allowed" >&2 # stderr 成为 Claude 的反馈
exit 2 # 退出 2 = 阻止操作
fi
exit 0 # 退出 0 = 允许继续
退出代码决定接下来发生什么:
UserPromptSubmit 和 SessionStart 钩子,你写入 stdout 的任何内容都会添加到 Claude 的上下文中。Ctrl+O 切换详细模式以在转录中查看这些消息。退出代码给你两个选项:允许或阻止。要获得更多控制,请退出 0 并将 JSON 对象打印到 stdout。
例如,PreToolUse 钩子可以拒绝工具调用并告诉 Claude 原因,或者将其升级给用户批准:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}
Claude Code 读取 permissionDecision 并取消工具调用,然后将 permissionDecisionReason 作为反馈反馈给 Claude。这三个选项特定于 PreToolUse:
"allow":继续而不显示权限提示"deny":取消工具调用并将原因发送给 Claude"ask":正常向用户显示权限提示其他事件使用不同的决策模式。例如,PostToolUse 和 Stop 钩子使用顶层的 decision: "block" 字段,而 PermissionRequest 使用 hookSpecificOutput.decision.behavior。有关按事件的完整分类,请参阅参考中的 摘要表。
对于 UserPromptSubmit 钩子,请改用 additionalContext 将文本注入 Claude 的上下文。基于提示的钩子 (type: "prompt") 处理输出的方式不同:请参阅基于提示的钩子。
没有匹配器时,钩子会在其事件的每次发生时触发。匹配器让你可以缩小范围。例如,如果你只想在文件编辑后运行格式化程序(而不是每次工具调用后),请向 PostToolUse 钩子添加匹配器:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "prettier --write ..." }
]
}
]
}
}
"Edit|Write" 匹配器是一个匹配工具名称的正则表达式模式。该钩子仅在 Claude 使用 Edit 或 Write 工具时触发,而不是在使用 Bash、Read 或任何其他工具时触发。
每种事件类型在特定字段上匹配。匹配器支持精确字符串和正则表达式模式:
| 事件 | 匹配器过滤的内容 | 示例匹配器值 |
|---|---|---|
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest | 工具名称 | Bash, `Edit |
SessionStart | 会话如何启动 | startup, resume, clear, compact |
SessionEnd | 会话结束的原因 | clear, logout, prompt_input_exit, other |
Notification | 通知类型 | permission_prompt, idle_prompt, auth_success, elicitation_dialog |
SubagentStart | 代理类型 | Bash, Explore, Plan, 或自定义代理名称 |
PreCompact | 触发压缩的原因 | manual, auto |
UserPromptSubmit, Stop | 不支持匹配器 | 始终在每次发生时触发 |
SubagentStop | 代理类型 | 与 SubagentStart 相同的值 |
更多示例显示不同事件类型上的匹配器:
```json theme={null}
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
}
]
}
]
}
}
```
下面的命令使用 `jq` 从钩子的 JSON 输入中提取工具名称,并将其写入 stderr,在那里它出现在详细模式 (`Ctrl+O`) 中:
```json theme={null}
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__github__.*",
"hooks": [
{
"type": "command",
"command": "echo \"GitHub tool called: $(jq -r '.tool_name')\" >&2"
}
]
}
]
}
}
```
```json theme={null}
{
"hooks": {
"SessionEnd": [
{
"matcher": "clear",
"hooks": [
{
"type": "command",
"command": "rm -f /tmp/claude-scratch-*.txt"
}
]
}
]
}
}
```
有关完整匹配器语法,请参阅 钩子参考。
你添加钩子的位置决定了其作用域:
你也可以使用 Claude Code 中的 /hooks 菜单 以交互方式添加、删除和查看钩子。要一次性禁用所有钩子,请使用 /hooks 菜单底部的切换开关,或在设置文件中设置 "disableAllHooks": true。
通过 /hooks 菜单添加的钩子立即生效。如果你在 Claude Code 运行时直接编辑设置文件,更改不会生效,直到你在 /hooks 菜单中查看它们或重新启动会话。
对于需要判断而非确定性规则的决策,请使用 type: "prompt" 钩子。Claude Code 不是运行 shell 命令,而是将你的提示和钩子的输入数据发送给 Claude 模型(默认为 Haiku)来做决策。如果需要更多能力,你可以使用 model 字段指定不同的模型。
模型的唯一工作是返回一个是/否决策作为 JSON:
"ok": true:操作继续"ok": false:操作被阻止。模型的 "reason" 会反馈给 Claude,以便调整。此示例使用 Stop 钩子询问模型所有请求的任务是否已完成。如果模型返回 "ok": false,Claude 会继续工作并使用 reason 作为其下一条指令:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
}
]
}
]
}
}
有关完整配置选项,请参阅参考中的 基于提示的钩子。
当验证需要检查文件或运行命令时,请使用 type: "agent" 钩子。与进行单次 LLM 调用的提示钩子不同,代理钩子会生成一个子代理,该子代理可以在返回决策之前读取文件、搜索代码和使用其他工具来验证条件。
代理钩子使用与提示钩子相同的 "ok" / "reason" 响应格式,但默认超时时间更长,为 60 秒,最多 50 个工具使用轮次。
此示例验证测试通过后才允许 Claude 停止:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
"timeout": 120
}
]
}
]
}
}
当钩子输入数据足以做出决策时,请使用提示钩子。当你需要根据代码库的实际状态验证某些内容时,请使用代理钩子。
有关完整配置选项,请参阅参考中的 基于代理的钩子。
timeout 字段(以秒为单位)按钩子配置。PostToolUse 钩子无法撤消操作,因为工具已经执行。PermissionRequest 钩子在 非交互模式 (-p) 中不会触发。使用 PreToolUse 钩子进行自动权限决策。Stop 钩子在 Claude 完成响应时触发,而不仅仅是在任务完成时。它们不会在用户中断时触发。钩子已配置但从未执行。
/hooks 并确认钩子出现在正确的事件下PreToolUse 在工具执行前触发,PostToolUse 在工具执行后触发)-p) 中使用 PermissionRequest 钩子,请改用 PreToolUse你在转录中看到类似 "PreToolUse hook error: ..." 的消息。
你的脚本意外地以非零代码退出。通过管道传输示例 JSON 手动测试:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $? # 检查退出代码
如果看到 "command not found",请使用绝对路径或 $CLAUDE_PROJECT_DIR 引用脚本
如果看到 "jq: command not found",请安装 jq 或使用 Python/Node.js 进行 JSON 解析
如果脚本根本没有运行,请使其可执行:chmod +x ./my-hook.sh
/hooks 显示未配置钩子你编辑了设置文件,但钩子未出现在菜单中。
/hooks 以重新加载。通过 /hooks 菜单添加的钩子立即生效,但手动文件编辑需要重新加载。.claude/settings.json,全局钩子为 ~/.claude/settings.jsonClaude 无限循环地继续工作,而不是停止。
你的 Stop 钩子脚本需要检查是否已经触发了延续。从 JSON 输入中解析 stop_hook_active 字段,如果为 true 则提前退出:
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # 允许 Claude 停止
fi
# ... 钩子逻辑的其余部分
即使你的钩子脚本输出有效的 JSON,Claude Code 也会显示 JSON 解析错误。
当 Claude Code 运行钩子时,它会生成一个 shell,该 shell 会加载你的配置文件 (~/.zshrc 或 ~/.bashrc)。如果你的配置文件包含无条件的 echo 语句,该输出会附加到你的钩子的 JSON 之前:
Shell ready on arm64
{"decision": "block", "reason": "Not allowed"}
Claude Code 尝试将其解析为 JSON 并失败。要解决此问题,请将 shell 配置文件中的 echo 语句包装起来,使其仅在交互式 shell 中运行:
# 在 ~/.zshrc 或 ~/.bashrc 中
if [[ $- == *i* ]]; then
echo "Shell ready"
fi
$- 变量包含 shell 标志,i 表示交互式。钩子在非交互式 shell 中运行,因此 echo 被跳过。
使用 Ctrl+O 切换详细模式以在转录中查看钩子输出,或运行 claude --debug 以获取完整的执行详细信息,包括哪些钩子匹配及其退出代码。
文档索引
获取完整的文档索引:https://code.claude.com/docs/llms.txt 在进一步探索之前,使用此文件发现所有可用页面。
Claude Code 钩子事件、配置模式、JSON 输入/输出格式、退出代码、异步钩子、提示钩子和 MCP 工具钩子的参考。
钩子是用户定义的 shell 命令或 LLM 提示,在 Claude Code 生命周期的特定点自动执行。使用此参考来查找事件模式、配置选项、JSON 输入/输出格式以及高级功能,如异步钩子和 MCP 工具钩子。如果你是第一次设置钩子,请从 指南 开始。
钩子在 Claude Code 会话期间的特定点触发。当事件触发且匹配器匹配时,Claude Code 会将有关事件的 JSON 上下文传递给你的钩子处理程序。对于命令钩子,这通过 stdin 到达。然后你的处理程序可以检查输入、采取行动,并可选地返回一个决策。有些事件每个会话触发一次,而其他事件则在代理循环内重复触发:
下表总结了每个事件的触发时机。钩子事件部分记录了每个事件的完整输入模式和控制选项。
| 事件 | 触发时机 |
|---|---|
SessionStart | 会话开始或恢复时 |
UserPromptSubmit | 用户提交提示时,在 Claude 处理之前 |
PreToolUse | 工具调用执行之前。可以阻止它 |
PermissionRequest | 权限对话框出现时 |
PostToolUse | 工具调用成功后 |
PostToolUseFailure | 工具调用失败后 |
Notification | Claude Code 发送通知时 |
SubagentStart | 子代理生成时 |
SubagentStop | 子代理完成时 |
Stop | Claude 完成响应时 |
PreCompact | 上下文压缩之前 |
SessionEnd | 会话终止时 |
要了解这些部分如何组合在一起,请考虑这个阻止破坏性 shell 命令的 PreToolUse 钩子。该钩子在每次 Bash 工具调用之前运行 block-rm.sh:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-rm.sh"
}
]
}
]
}
}
该脚本从 stdin 读取 JSON 输入,提取命令,如果包含 rm -rf 则返回 permissionDecision 为 "deny":
#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Destructive command blocked by hook"
}
}'
else
exit 0 # 允许命令
fi
现在假设 Claude Code 决定运行 Bash "rm -rf /tmp/build"。以下是发生的情况:
Contains Hooks
This skill uses Claude hooks which can execute code automatically in response to events. Review carefully before installing.
Analyze the project, suggest practical hooks, and create them with proper testing.
Automatically detect the project tooling and suggest relevant hooks:
When TypeScript is detected (tsconfig.json):
When Prettier is detected (.prettierrc, prettier.config.js):
When ESLint is detected (.eslintrc.*):
When package.json has scripts:
test script → "Run tests before commits"build script → "Validate build before commits"When a git repository is detected:
Decision Tree:
Project has TypeScript? → Suggest type checking hooks
Project has formatter? → Suggest formatting hooks
Project has tests? → Suggest test validation hooks
Security sensitive? → Suggest security hooks
+ Scan for additional patterns and suggest custom hooks based on:
- Custom scripts in package.json
- Unique file patterns or extensions
- Development workflow indicators
- Project-specific tooling configurations
Start by asking: "What should this hook do?" and offer relevant suggestions from your analysis.
Then understand the context from the user's description and only ask about details you're unsure about :
Trigger timing : When should it run?
PreToolUse: Before file operations (can block)PostToolUse: After file operations (feedback/fixes)UserPromptSubmit: Before processing requestsTool matcher : Which tools should trigger it? (Write, Edit, Bash, * etc)
Scope : global, project, or
You should:
~/.claude/hooks/ or .claude/hooks/ based on scope$CLAUDE_PROJECT_DIR to reference project rootKey Implementation Standards:
additionalContext/systemMessage for Claude communicationsuppressOutput: true for successful operations⚠️ CRITICAL: Input/Output Format
This is where most hook implementations fail. Pay extra attention to:
CRITICAL: Test both happy and sad paths:
Happy Path Testing:
Sad Path Testing: 2. Test expected failure scenario - Create conditions where hook should fail/warn
Verification Steps: 3. Verify expected behavior : Check if it blocks/warns/provides context as intended
Example Testing Process:
If Issues Occur, you should:
chmod +x)#!/usr/bin/env node
// Read stdin JSON, check .ts/.tsx files only
// Run: npx tsc --noEmit --pretty
// Output: JSON with additionalContext for errors
#!/usr/bin/env node
// Read stdin JSON, check supported file types
// Run: npx prettier --write [file]
// Output: JSON with suppressOutput: true
#!/bin/bash
# Read stdin JSON, check for secrets/keys
# Block if dangerous patterns found
# Exit 2 to block, 0 to continue
Complete templates available at:https://docs.claude.com/en/docs/claude-code/hooks#examples
📖 Official Docs : https://docs.claude.com/en/docs/claude-code/hooks.md
Common Patterns:
JSON.parse(process.stdin.read()){continue: true, suppressOutput: true}{continue: true, additionalContext: "error details"}exit(2) in PreToolUse hooksHook Types by Use Case:
Hook Execution Best Practices:
✅ Hook created successfully when:
Result : The user gets a working hook that enhances their development workflow with intelligent automation and quality checks.
Documentation Index
Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt Use this file to discover all available pages before exploring further.
Run shell commands automatically when Claude Code edits files, finishes tasks, or needs input. Format code, send notifications, validate commands, and enforce project rules.
Hooks are user-defined shell commands that execute at specific points in Claude Code's lifecycle. They provide deterministic control over Claude Code's behavior, ensuring certain actions always happen rather than relying on the LLM to choose to run them. Use hooks to enforce project rules, automate repetitive tasks, and integrate Claude Code with your existing tools.
For decisions that require judgment rather than deterministic rules, you can also use prompt-based hooks or agent-based hooks that use a Claude model to evaluate conditions.
For other ways to extend Claude Code, see skills for giving Claude additional instructions and executable commands, subagents for running tasks in isolated contexts, and plugins for packaging extensions to share across projects.
The fastest way to create a hook is through the /hooks interactive menu in Claude Code. This walkthrough creates a desktop notification hook, so you get alerted whenever Claude is waiting for your input instead of watching the terminal.
<Tabs>
<Tab title="macOS">
Uses [`osascript`](https://ss64.com/mac/osascript.html) to trigger a native macOS notification through AppleScript:
```
osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'
```
</Tab>
<Tab title="Linux">
Uses `notify-send`, which is pre-installed on most Linux desktops with a notification daemon:
```
notify-send 'Claude Code' 'Claude Code needs your attention'
```
</Tab>
<Tab title="Windows (PowerShell)">
Uses PowerShell to show a native message box through .NET's Windows Forms:
```
powershell.exe -Command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')"
```
</Tab>
</Tabs>
Hooks let you run code at key points in Claude Code's lifecycle: format files after edits, block commands before they execute, send notifications when Claude needs input, inject context at session start, and more. For the full list of hook events, see the Hooks reference.
Each example includes a ready-to-use configuration block that you add to a settings file. The most common patterns:
Get a desktop notification whenever Claude finishes working and needs your input, so you can switch to other tasks without checking the terminal.
This hook uses the Notification event, which fires when Claude is waiting for input or permission. Each tab below uses the platform's native notification command. Add this to ~/.claude/settings.json, or use the interactive walkthrough above to configure it with /hooks:
Automatically run Prettier on every file Claude edits, so formatting stays consistent without manual intervention.
This hook uses the PostToolUse event with an Edit|Write matcher, so it runs only after file-editing tools. The command extracts the edited file path with jq and passes it to Prettier. Add this to .claude/settings.json in your project root:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
Prevent Claude from modifying sensitive files like .env, package-lock.json, or anything in .git/. Claude receives feedback explaining why the edit was blocked, so it can adjust its approach.
This example uses a separate script file that the hook calls. The script checks the target file path against a list of protected patterns and exits with code 2 to block the edit.
```bash theme={null}
#!/bin/bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0
```
```bash theme={null}
chmod +x .claude/hooks/protect-files.sh
```
```json theme={null}
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
```
When Claude's context window fills up, compaction summarizes the conversation to free space. This can lose important details. Use a SessionStart hook with a compact matcher to re-inject critical context after every compaction.
Any text your command writes to stdout is added to Claude's context. This example reminds Claude of project conventions and recent work. Add this to .claude/settings.json in your project root:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
}
]
}
]
}
}
You can replace the echo with any command that produces dynamic output, like git log --oneline -5 to show recent commits. For injecting context on every session start, consider using CLAUDE.md instead. For environment variables, see CLAUDE_ENV_FILE in the reference.
Hook events fire at specific lifecycle points in Claude Code. When an event fires, all matching hooks run in parallel, and identical hook commands are automatically deduplicated. The table below shows each event and when it triggers:
| Event | When it fires |
|---|---|
SessionStart | When a session begins or resumes |
UserPromptSubmit | When you submit a prompt, before Claude processes it |
PreToolUse | Before a tool call executes. Can block it |
PermissionRequest | When a permission dialog appears |
PostToolUse | After a tool call succeeds |
PostToolUseFailure |
Each hook has a type that determines how it runs. Most hooks use "type": "command", which runs a shell command. Two other options use a Claude model to make decisions: "type": "prompt" for single-turn evaluation and "type": "agent" for multi-turn verification with tool access. See Prompt-based hooks and Agent-based hooks for details.
Hooks communicate with Claude Code through stdin, stdout, stderr, and exit codes. When an event fires, Claude Code passes event-specific data as JSON to your script's stdin. Your script reads that data, does its work, and tells Claude Code what to do next via the exit code.
Every event includes common fields like session_id and cwd, but each event type adds different data. For example, when Claude runs a Bash command, a PreToolUse hook receives something like this on stdin:
{
"session_id": "abc123", // unique ID for this session
"cwd": "/Users/sarah/myproject", // working directory when the event fired
"hook_event_name": "PreToolUse", // which event triggered this hook
"tool_name": "Bash", // the tool Claude is about to use
"tool_input": { // the arguments Claude passed to the tool
"command": "npm test" // for Bash, this is the shell command
}
}
Your script can parse that JSON and act on any of those fields. UserPromptSubmit hooks get the prompt text instead, SessionStart hooks get the source (startup, resume, compact), and so on. See Common input fields in the reference for shared fields, and each event's section for event-specific schemas.
Your script tells Claude Code what to do next by writing to stdout or stderr and exiting with a specific code. For example, a PreToolUse hook that wants to block a command:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q "drop table"; then
echo "Blocked: dropping tables is not allowed" >&2 # stderr becomes Claude's feedback
exit 2 # exit 2 = block the action
fi
exit 0 # exit 0 = let it proceed
The exit code determines what happens next:
UserPromptSubmit and SessionStart hooks, anything you write to stdout is added to Claude's context.Ctrl+O to see these messages in the transcript.Exit codes give you two options: allow or block. For more control, exit 0 and print a JSON object to stdout instead.
For example, a PreToolUse hook can deny a tool call and tell Claude why, or escalate it to the user for approval:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}
Claude Code reads permissionDecision and cancels the tool call, then feeds permissionDecisionReason back to Claude as feedback. These three options are specific to PreToolUse:
"allow": proceed without showing a permission prompt"deny": cancel the tool call and send the reason to Claude"ask": show the permission prompt to the user as normalOther events use different decision patterns. For example, PostToolUse and Stop hooks use a top-level decision: "block" field, while PermissionRequest uses hookSpecificOutput.decision.behavior. See the summary table in the reference for a full breakdown by event.
For UserPromptSubmit hooks, use additionalContext instead to inject text into Claude's context. Prompt-based hooks (type: "prompt") handle output differently: see Prompt-based hooks.
Without a matcher, a hook fires on every occurrence of its event. Matchers let you narrow that down. For example, if you want to run a formatter only after file edits (not after every tool call), add a matcher to your PostToolUse hook:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "prettier --write ..." }
]
}
]
}
}
The "Edit|Write" matcher is a regex pattern that matches the tool name. The hook only fires when Claude uses the Edit or Write tool, not when it uses Bash, Read, or any other tool.
Each event type matches on a specific field. Matchers support exact strings and regex patterns:
| Event | What the matcher filters | Example matcher values |
|---|---|---|
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest | tool name | Bash, `Edit |
SessionStart | how the session started | startup, resume, , |
A few more examples showing matchers on different event types:
```json theme={null}
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
}
]
}
]
}
}
```
The command below extracts the tool name from the hook's JSON input with `jq` and writes it to stderr, where it shows up in verbose mode (`Ctrl+O`):
```json theme={null}
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__github__.*",
"hooks": [
{
"type": "command",
"command": "echo \"GitHub tool called: $(jq -r '.tool_name')\" >&2"
}
]
}
]
}
}
```
```json theme={null}
{
"hooks": {
"SessionEnd": [
{
"matcher": "clear",
"hooks": [
{
"type": "command",
"command": "rm -f /tmp/claude-scratch-*.txt"
}
]
}
]
}
}
```
For full matcher syntax, see the Hooks reference.
Where you add a hook determines its scope:
| Location | Scope | Shareable |
|---|---|---|
~/.claude/settings.json | All your projects | No, local to your machine |
.claude/settings.json | Single project | Yes, can be committed to the repo |
.claude/settings.local.json | Single project | No, gitignored |
| Managed policy settings | Organization-wide | Yes, admin-controlled |
Plugin hooks/hooks.json | When plugin is enabled | Yes, bundled with the plugin |
You can also use the /hooks menu in Claude Code to add, delete, and view hooks interactively. To disable all hooks at once, use the toggle at the bottom of the /hooks menu or set "disableAllHooks": true in your settings file.
Hooks added through the /hooks menu take effect immediately. If you edit settings files directly while Claude Code is running, the changes won't take effect until you review them in the /hooks menu or restart your session.
For decisions that require judgment rather than deterministic rules, use type: "prompt" hooks. Instead of running a shell command, Claude Code sends your prompt and the hook's input data to a Claude model (Haiku by default) to make the decision. You can specify a different model with the model field if you need more capability.
The model's only job is to return a yes/no decision as JSON:
"ok": true: the action proceeds"ok": false: the action is blocked. The model's "reason" is fed back to Claude so it can adjust.This example uses a Stop hook to ask the model whether all requested tasks are complete. If the model returns "ok": false, Claude keeps working and uses the reason as its next instruction:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
}
]
}
]
}
}
For full configuration options, see Prompt-based hooks in the reference.
When verification requires inspecting files or running commands, use type: "agent" hooks. Unlike prompt hooks which make a single LLM call, agent hooks spawn a subagent that can read files, search code, and use other tools to verify conditions before returning a decision.
Agent hooks use the same "ok" / "reason" response format as prompt hooks, but with a longer default timeout of 60 seconds and up to 50 tool-use turns.
This example verifies that tests pass before allowing Claude to stop:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
"timeout": 120
}
]
}
]
}
}
Use prompt hooks when the hook input data alone is enough to make a decision. Use agent hooks when you need to verify something against the actual state of the codebase.
For full configuration options, see Agent-based hooks in the reference.
timeout field (in seconds).PostToolUse hooks cannot undo actions since the tool has already executed.PermissionRequest hooks do not fire in non-interactive mode (-p). Use PreToolUse hooks for automated permission decisions.Stop hooks fire whenever Claude finishes responding, not only at task completion. They do not fire on user interrupts.The hook is configured but never executes.
/hooks and confirm the hook appears under the correct eventPreToolUse fires before tool execution, PostToolUse fires after)PermissionRequest hooks in non-interactive mode (-p), switch to PreToolUse insteadYou see a message like "PreToolUse hook error: ..." in the transcript.
Your script exited with a non-zero code unexpectedly. Test it manually by piping sample JSON:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $? # Check the exit code
If you see "command not found", use absolute paths or $CLAUDE_PROJECT_DIR to reference scripts
If you see "jq: command not found", install jq or use Python/Node.js for JSON parsing
If the script isn't running at all, make it executable: chmod +x ./my-hook.sh
/hooks shows no hooks configuredYou edited a settings file but the hooks don't appear in the menu.
/hooks to reload. Hooks added through the /hooks menu take effect immediately, but manual file edits require a reload..claude/settings.json for project hooks, ~/.claude/settings.json for global hooksClaude keeps working in an infinite loop instead of stopping.
Your Stop hook script needs to check whether it already triggered a continuation. Parse the stop_hook_active field from the JSON input and exit early if it's true:
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Allow Claude to stop
fi
# ... rest of your hook logic
Claude Code shows a JSON parsing error even though your hook script outputs valid JSON.
When Claude Code runs a hook, it spawns a shell that sources your profile (~/.zshrc or ~/.bashrc). If your profile contains unconditional echo statements, that output gets prepended to your hook's JSON:
Shell ready on arm64
{"decision": "block", "reason": "Not allowed"}
Claude Code tries to parse this as JSON and fails. To fix this, wrap echo statements in your shell profile so they only run in interactive shells:
# In ~/.zshrc or ~/.bashrc
if [[ $- == *i* ]]; then
echo "Shell ready"
fi
The $- variable contains shell flags, and i means interactive. Hooks run in non-interactive shells, so the echo is skipped.
Toggle verbose mode with Ctrl+O to see hook output in the transcript, or run claude --debug for full execution details including which hooks matched and their exit codes.
Documentation Index
Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt Use this file to discover all available pages before exploring further.
Reference for Claude Code hook events, configuration schema, JSON input/output formats, exit codes, async hooks, prompt hooks, and MCP tool hooks.
Hooks are user-defined shell commands or LLM prompts that execute automatically at specific points in Claude Code's lifecycle. Use this reference to look up event schemas, configuration options, JSON input/output formats, and advanced features like async hooks and MCP tool hooks. If you're setting up hooks for the first time, start with the guide instead.
Hooks fire at specific points during a Claude Code session. When an event fires and a matcher matches, Claude Code passes JSON context about the event to your hook handler. For command hooks, this arrives on stdin. Your handler can then inspect the input, take action, and optionally return a decision. Some events fire once per session, while others fire repeatedly inside the agentic loop:
The table below summarizes when each event fires. The Hook events section documents the full input schema and decision control options for each one.
| Event | When it fires |
|---|---|
SessionStart | When a session begins or resumes |
UserPromptSubmit | When you submit a prompt, before Claude processes it |
PreToolUse | Before a tool call executes. Can block it |
PermissionRequest | When a permission dialog appears |
PostToolUse | After a tool call succeeds |
PostToolUseFailure |
To see how these pieces fit together, consider this PreToolUse hook that blocks destructive shell commands. The hook runs block-rm.sh before every Bash tool call:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-rm.sh"
}
]
}
]
}
}
The script reads the JSON input from stdin, extracts the command, and returns a permissionDecision of "deny" if it contains rm -rf:
#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Destructive command blocked by hook"
}
}'
else
exit 0 # allow the command
fi
Now suppose Claude Code decides to run Bash "rm -rf /tmp/build". Here's what happens:
```json theme={null}
{ "tool_name": "Bash", "tool_input": { "command": "rm -rf /tmp/build" }, ... }
```
```json theme={null}
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive command blocked by hook"
}
}
```
If the command had been safe (like `npm test`), the script would hit `exit 0` instead, which tells Claude Code to allow the tool call with no further action.
The Configuration section below documents the full schema, and each hook event section documents what input your command receives and what output it can return.
Hooks are defined in JSON settings files. The configuration has three levels of nesting:
PreToolUse or StopSee How a hook resolves above for a complete walkthrough with an annotated example.
Where you define a hook determines its scope:
| Location | Scope | Shareable |
|---|---|---|
~/.claude/settings.json | All your projects | No, local to your machine |
.claude/settings.json | Single project | Yes, can be committed to the repo |
.claude/settings.local.json | Single project | No, gitignored |
| Managed policy settings | Organization-wide | Yes, admin-controlled |
Plugin hooks/hooks.json | When plugin is enabled | Yes, bundled with the plugin |
For details on settings file resolution, see settings. Enterprise administrators can use allowManagedHooksOnly to block user, project, and plugin hooks. See Hook configuration.
The matcher field is a regex string that filters when hooks fire. Use "*", "", or omit matcher entirely to match all occurrences. Each event type matches on a different field:
| Event | What the matcher filters | Example matcher values |
|---|---|---|
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest | tool name | Bash, `Edit |
SessionStart | how the session started | startup, resume, , |
The matcher is a regex, so Edit|Write matches either tool and Notebook.* matches any tool starting with Notebook. The matcher runs against a field from the JSON input that Claude Code sends to your hook on stdin. For tool events, that field is tool_name. Each hook event section lists the full set of matcher values and the input schema for that event.
This example runs a linting script only when Claude writes or edits a file:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "/path/to/lint-check.sh"
}
]
}
]
}
}
UserPromptSubmit and Stop don't support matchers and always fire on every occurrence. If you add a matcher field to these events, it is silently ignored.
MCP server tools appear as regular tools in tool events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest), so you can match them the same way you match any other tool name.
MCP tools follow the naming pattern mcp__<server>__<tool>, for example:
mcp__memory__create_entities: Memory server's create entities toolmcp__filesystem__read_file: Filesystem server's read file toolmcp__github__search_repositories: GitHub server's search toolUse regex patterns to target specific MCP tools or groups of tools:
mcp__memory__.* matches all tools from the memory servermcp__.*__write.* matches any tool containing "write" from any serverThis example logs all memory server operations and validates write operations from any MCP server:
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__memory__.*",
"hooks": [
{
"type": "command",
"command": "echo 'Memory operation initiated' >> ~/mcp-operations.log"
}
]
},
{
"matcher": "mcp__.*__write.*",
"hooks": [
{
"type": "command",
"command": "/home/user/scripts/validate-mcp-write.py"
}
]
}
]
}
}
Each object in the inner hooks array is a hook handler: the shell command, LLM prompt, or agent that runs when the matcher matches. There are three types:
type: "command"): run a shell command. Your script receives the event's JSON input on stdin and communicates results back through exit codes and stdout.type: "prompt"): send a prompt to a Claude model for single-turn evaluation. The model returns a yes/no decision as JSON. See Prompt-based hooks.type: "agent"): spawn a subagent that can use tools like Read, Grep, and Glob to verify conditions before returning a decision. See Agent-based hooks.These fields apply to all hook types:
| Field | Required | Description |
|---|---|---|
type | yes | "command", "prompt", or "agent" |
timeout | no | Seconds before canceling. Defaults: 600 for command, 30 for prompt, 60 for agent |
statusMessage | no | Custom spinner message displayed while the hook runs |
once |
In addition to the common fields, command hooks accept these fields:
| Field | Required | Description |
|---|---|---|
command | yes | Shell command to execute |
async | no | If true, runs in the background without blocking. See Run hooks in the background |
In addition to the common fields, prompt and agent hooks accept these fields:
| Field | Required | Description |
|---|---|---|
prompt | yes | Prompt text to send to the model. Use $ARGUMENTS as a placeholder for the hook input JSON |
model | no | Model to use for evaluation. Defaults to a fast model |
All matching hooks run in parallel, and identical handlers are deduplicated automatically. Handlers run in the current directory with Claude Code's environment. The $CLAUDE_CODE_REMOTE environment variable is set to "true" in remote web environments and not set in the local CLI.
Use environment variables to reference hook scripts relative to the project or plugin root, regardless of the working directory when the hook runs:
$CLAUDE_PROJECT_DIR: the project root. Wrap in quotes to handle paths with spaces.
${CLAUDE_PLUGIN_ROOT}: the plugin's root directory, for scripts bundled with a plugin.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"
}
]
}
]
}
}
This example runs a formatting script bundled with the plugin:
{
"description": "Automatic code formatting",
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
"timeout": 30
}
]
}
]
}
}
See the plugin components reference for details on creating plugin hooks.
In addition to settings files and plugins, hooks can be defined directly in skills and subagents using frontmatter. These hooks are scoped to the component's lifecycle and only run when that component is active.
All hook events are supported. For subagents, Stop hooks are automatically converted to SubagentStop since that is the event that fires when a subagent completes.
Hooks use the same configuration format as settings-based hooks but are scoped to the component's lifetime and cleaned up when it finishes.
This skill defines a PreToolUse hook that runs a security validation script before each Bash command:
---
name: secure-operations
description: Perform operations with security checks
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/security-check.sh"
---
Agents use the same format in their YAML frontmatter.
/hooks menuType /hooks in Claude Code to open the interactive hooks manager, where you can view, add, and delete hooks without editing settings files directly. For a step-by-step walkthrough, see Set up your first hook in the guide.
Each hook in the menu is labeled with a bracket prefix indicating its source:
[User]: from ~/.claude/settings.json[Project]: from .claude/settings.json[Local]: from .claude/settings.local.json[Plugin]: from a plugin's hooks/hooks.json, read-onlyTo remove a hook, delete its entry from the settings JSON file, or use the /hooks menu and select the hook to delete it.
To temporarily disable all hooks without removing them, set "disableAllHooks": true in your settings file or use the toggle in the /hooks menu. There is no way to disable an individual hook while keeping it in the configuration.
Direct edits to hooks in settings files don't take effect immediately. Claude Code captures a snapshot of hooks at startup and uses it throughout the session. This prevents malicious or accidental hook modifications from taking effect mid-session without your review. If hooks are modified externally, Claude Code warns you and requires review in the /hooks menu before changes apply.
Hooks receive JSON data via stdin and communicate results through exit codes, stdout, and stderr. This section covers fields and behavior common to all events. Each event's section under Hook events includes its specific input schema and decision control options.
All hook events receive these fields via stdin as JSON, in addition to event-specific fields documented in each hook event section:
| Field | Description |
|---|---|
session_id | Current session identifier |
transcript_path | Path to conversation JSON |
cwd | Current working directory when the hook is invoked |
permission_mode | Current permission mode: "default", "plan", "acceptEdits", , or |
For example, a PreToolUse hook for a Bash command receives this on stdin:
{
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}
The tool_name and tool_input fields are event-specific. Each hook event section documents the additional fields for that event.
The exit code from your hook command tells Claude Code whether the action should proceed, be blocked, or be ignored.
Exit 0 means success. Claude Code parses stdout for JSON output fields. JSON output is only processed on exit 0. For most events, stdout is only shown in verbose mode (Ctrl+O). The exceptions are UserPromptSubmit and SessionStart, where stdout is added as context that Claude can see and act on.
Exit 2 means a blocking error. Claude Code ignores stdout and any JSON in it. Instead, stderr text is fed back to Claude as an error message. The effect depends on the event: PreToolUse blocks the tool call, UserPromptSubmit rejects the prompt, and so on. See exit code 2 behavior for the full list.
Any other exit code is a non-blocking error. stderr is shown in verbose mode (Ctrl+O) and execution continues.
For example, a hook command script that blocks dangerous Bash commands:
#!/bin/bash
# Reads JSON input from stdin, checks the command
command=$(jq -r '.tool_input.command' < /dev/stdin)
if [[ "$command" == rm* ]]; then
echo "Blocked: rm commands are not allowed" >&2
exit 2 # Blocking error: tool call is prevented
fi
exit 0 # Success: tool call proceeds
Exit code 2 is the way a hook signals "stop, don't do this." The effect depends on the event, because some events represent actions that can be blocked (like a tool call that hasn't happened yet) and others represent things that already happened or can't be prevented.
| Hook event | Can block? | What happens on exit 2 |
|---|---|---|
PreToolUse | Yes | Blocks the tool call |
PermissionRequest | Yes | Denies the permission |
UserPromptSubmit | Yes | Blocks prompt processing and erases the prompt |
Stop | Yes | Prevents Claude from stopping, continues the conversation |
SubagentStop | Yes |
Exit codes let you allow or block, but JSON output gives you finer-grained control. Instead of exiting with code 2 to block, exit 0 and print a JSON object to stdout. Claude Code reads specific fields from that JSON to control behavior, including decision control for blocking, allowing, or escalating to the user.
Your hook's stdout must contain only the JSON object. If your shell profile prints text on startup, it can interfere with JSON parsing. See JSON validation failed in the troubleshooting guide.
The JSON object supports three kinds of fields:
continue work across all events. These are listed in the table below.decision and reason are used by some events to block or provide feedback.hookSpecificOutput is a nested object for events that need richer control. It requires a hookEventName field set to the event name.| Field | Default | Description |
|---|---|---|
continue | true | If false, Claude stops processing entirely after the hook runs. Takes precedence over any event-specific decision fields |
stopReason | none | Message shown to the user when continue is false. Not shown to Claude |
suppressOutput |
To stop Claude entirely regardless of event type:
{ "continue": false, "stopReason": "Build failed, fix errors before continuing" }
Not every event supports blocking or controlling behavior through JSON. The events that do each use a different set of fields to express that decision. Use this table as a quick reference before writing a hook:
| Events | Decision pattern | Key fields |
|---|---|---|
| UserPromptSubmit, PostToolUse, PostToolUseFailure, Stop, SubagentStop | Top-level decision | decision: "block", reason |
| PreToolUse | hookSpecificOutput | permissionDecision (allow/deny/ask), permissionDecisionReason |
| PermissionRequest | hookSpecificOutput |
Here are examples of each pattern in action:
```json theme={null}
{
"decision": "block",
"reason": "Test suite must pass before proceeding"
}
```
```json theme={null}
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database writes are not allowed"
}
}
```
```json theme={null}
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedInput": {
"command": "npm run lint"
}
}
}
}
```
For extended examples including Bash command validation, prompt filtering, and auto-approval scripts, see What you can automate in the guide and the Bash command validator reference implementation.
Each event corresponds to a point in Claude Code's lifecycle where hooks can run. The sections below are ordered to match the lifecycle: from session setup through the agentic loop to session end. Each section describes when the event fires, what matchers it supports, the JSON input it receives, and how to control behavior through output.
Runs when Claude Code starts a new session or resumes an existing session. Useful for loading development context like existing issues or recent changes to your codebase, or setting up environment variables. For static context that does not require a script, use CLAUDE.md instead.
SessionStart runs on every session, so keep these hooks fast.
The matcher value corresponds to how the session was initiated:
| Matcher | When it fires |
|---|---|
startup | New session |
resume | --resume, --continue, or /resume |
clear | /clear |
compact | Auto or manual compaction |
In addition to the common input fields, SessionStart hooks receive source, model, and optionally agent_type. The source field indicates how the session started: "startup" for new sessions, "resume" for resumed sessions, "clear" after /clear, or "compact" after compaction. The model field contains the model identifier. If you start Claude Code with claude --agent <name>, an field contains the agent name.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "SessionStart",
"source": "startup",
"model": "claude-sonnet-4-5-20250929"
}
Any text your hook script prints to stdout is added as context for Claude. In addition to the JSON output fields available to all hooks, you can return these event-specific fields:
| Field | Description |
|---|---|
additionalContext | String added to Claude's context. Multiple hooks' values are concatenated |
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "My additional context here"
}
}
SessionStart hooks have access to the CLAUDE_ENV_FILE environment variable, which provides a file path where you can persist environment variables for subsequent Bash commands.
To set individual environment variables, write export statements to CLAUDE_ENV_FILE. Use append (>>) to preserve variables set by other hooks:
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE"
fi
exit 0
To capture all environment changes from setup commands, compare the exported variables before and after:
#!/bin/bash
ENV_BEFORE=$(export -p | sort)
# Run your setup commands that modify the environment
source ~/.nvm/nvm.sh
nvm use 20
if [ -n "$CLAUDE_ENV_FILE" ]; then
ENV_AFTER=$(export -p | sort)
comm -13 <(echo "$ENV_BEFORE") <(echo "$ENV_AFTER") >> "$CLAUDE_ENV_FILE"
fi
exit 0
Any variables written to this file will be available in all subsequent Bash commands that Claude Code executes during the session.
Runs when the user submits a prompt, before Claude processes it. This allows you to add additional context based on the prompt/conversation, validate prompts, or block certain types of prompts.
In addition to the common input fields, UserPromptSubmit hooks receive the prompt field containing the text the user submitted.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "UserPromptSubmit",
"prompt": "Write a function to calculate the factorial of a number"
}
UserPromptSubmit hooks can control whether a user prompt is processed and add context. All JSON output fields are available.
There are two ways to add context to the conversation on exit code 0:
additionalContext: use the JSON format below for more control. The additionalContext field is added as contextPlain stdout is shown as hook output in the transcript. The additionalContext field is added more discretely.
To block a prompt, return a JSON object with decision set to "block":
| Field | Description |
|---|---|
decision | "block" prevents the prompt from being processed and erases it from context. Omit to allow the prompt to proceed |
reason | Shown to the user when decision is "block". Not added to context |
additionalContext | String added to Claude's context |
{
"decision": "block",
"reason": "Explanation for decision",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "My additional context here"
}
}
Runs after Claude creates tool parameters and before processing the tool call. Matches on tool name: Bash, Edit, Write, Read, Glob, Grep, Task, WebFetch, WebSearch, and any MCP tool names.
Use PreToolUse decision control to allow, deny, or ask for permission to use the tool.
In addition to the common input fields, PreToolUse hooks receive tool_name, tool_input, and tool_use_id. The tool_input fields depend on the tool:
Executes shell commands.
| Field | Type | Example | Description |
|---|---|---|---|
command | string | "npm test" | The shell command to execute |
description | string | "Run test suite" | Optional description of what the command does |
timeout | number | 120000 |
Creates or overwrites a file.
| Field | Type | Example | Description |
|---|---|---|---|
file_path | string | "/path/to/file.txt" | Absolute path to the file to write |
content | string | "file content" | Content to write to the file |
Replaces a string in an existing file.
| Field | Type | Example | Description |
|---|---|---|---|
file_path | string | "/path/to/file.txt" | Absolute path to the file to edit |
old_string | string | "original text" | Text to find and replace |
new_string | string | "replacement text" |
Reads file contents.
| Field | Type | Example | Description |
|---|---|---|---|
file_path | string | "/path/to/file.txt" | Absolute path to the file to read |
offset | number | 10 | Optional line number to start reading from |
limit | number | 50 |
Finds files matching a glob pattern.
| Field | Type | Example | Description |
|---|---|---|---|
pattern | string | "**/*.ts" | Glob pattern to match files against |
path | string | "/path/to/dir" | Optional directory to search in. Defaults to current working directory |
Searches file contents with regular expressions.
| Field | Type | Example | Description |
|---|---|---|---|
pattern | string | "TODO.*fix" | Regular expression pattern to search for |
path | string | "/path/to/dir" | Optional file or directory to search in |
glob | string | "*.ts" |
Fetches and processes web content.
| Field | Type | Example | Description |
|---|---|---|---|
url | string | "https://example.com/api" | URL to fetch content from |
prompt | string | "Extract the API endpoints" | Prompt to run on the fetched content |
Searches the web.
| Field | Type | Example | Description |
|---|---|---|---|
query | string | "react hooks best practices" | Search query |
allowed_domains | array | ["docs.example.com"] | Optional: only include results from these domains |
blocked_domains | array | ["spam.example.com"] |
Spawns a subagent.
| Field | Type | Example | Description |
|---|---|---|---|
prompt | string | "Find all API endpoints" | The task for the agent to perform |
description | string | "Find API endpoints" | Short description of the task |
subagent_type | string | "Explore" |
PreToolUse hooks can control whether a tool call proceeds. Unlike other hooks that use a top-level decision field, PreToolUse returns its decision inside a hookSpecificOutput object. This gives it richer control: three outcomes (allow, deny, or ask) plus the ability to modify tool input before execution.
| Field | Description |
|---|---|
permissionDecision | "allow" bypasses the permission system, "deny" prevents the tool call, "ask" prompts the user to confirm |
permissionDecisionReason | For "allow" and "ask", shown to the user but not Claude. For "deny", shown to Claude |
updatedInput |
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "My reason here",
"updatedInput": {
"field_to_modify": "new value"
},
"additionalContext": "Current environment: production. Proceed with caution."
}
}
Runs when the user is shown a permission dialog. Use PermissionRequest decision control to allow or deny on behalf of the user.
Matches on tool name, same values as PreToolUse.
PermissionRequest hooks receive tool_name and tool_input fields like PreToolUse hooks, but without tool_use_id. An optional permission_suggestions array contains the "always allow" options the user would normally see in the permission dialog. The difference is when the hook fires: PermissionRequest hooks run when a permission dialog is about to be shown to the user, while PreToolUse hooks run before tool execution regardless of permission status.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "PermissionRequest",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf node_modules",
"description": "Remove node_modules directory"
},
"permission_suggestions": [
{ "type": "toolAlwaysAllow", "tool": "Bash" }
]
}
PermissionRequest hooks can allow or deny permission requests. In addition to the JSON output fields available to all hooks, your hook script can return a decision object with these event-specific fields:
| Field | Description |
|---|---|
behavior | "allow" grants the permission, "deny" denies it |
updatedInput | For "allow" only: modifies the tool's input parameters before execution |
updatedPermissions | For "allow" only: applies permission rule updates, equivalent to the user selecting an "always allow" option |
message |
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedInput": {
"command": "npm run lint"
}
}
}
}
Runs immediately after a tool completes successfully.
Matches on tool name, same values as PreToolUse.
PostToolUse hooks fire after a tool has already executed successfully. The input includes both tool_input, the arguments sent to the tool, and tool_response, the result it returned. The exact schema for both depends on the tool.
{
"session_id": "abc123",
"transcript_path": "
agent-browser 浏览器自动化工具 - Vercel Labs 命令行网页操作与测试
147,400 周安装
project-localResponse approach :
Blocking behavior (if relevant): "Should this stop operations when issues are found?"
Claude integration (CRITICAL): "Should Claude Code automatically see and fix issues this hook detects?"
additionalContext for error communicationsuppressOutput: true for silent operationContext pollution : "Should successful operations be silent to avoid noise?"
File filtering : "What file types should this hook process?"
| After a tool call fails |
Notification | When Claude Code sends a notification |
SubagentStart | When a subagent is spawned |
SubagentStop | When a subagent finishes |
Stop | When Claude finishes responding |
PreCompact | Before context compaction |
SessionEnd | When a session terminates |
clearcompactSessionEnd | why the session ended | clear, logout, prompt_input_exit, other |
Notification | notification type | permission_prompt, idle_prompt, auth_success, elicitation_dialog |
SubagentStart | agent type | Bash, Explore, Plan, or custom agent names |
PreCompact | what triggered compaction | manual, auto |
UserPromptSubmit, Stop | no matcher support | always fires on every occurrence |
SubagentStop | agent type | same values as SubagentStart |
| Skill or agent frontmatter | While the skill or agent is active | Yes, defined in the component file |
| After a tool call fails |
Notification | When Claude Code sends a notification |
SubagentStart | When a subagent is spawned |
SubagentStop | When a subagent finishes |
Stop | When Claude finishes responding |
PreCompact | Before context compaction |
SessionEnd | When a session terminates |
| Skill or agent frontmatter | While the component is active | Yes, defined in the component file |
clearcompactSessionEnd | why the session ended | clear, logout, prompt_input_exit, bypass_permissions_disabled, other |
Notification | notification type | permission_prompt, idle_prompt, auth_success, elicitation_dialog |
SubagentStart | agent type | Bash, Explore, Plan, or custom agent names |
PreCompact | what triggered compaction | manual, auto |
SubagentStop | agent type | same values as SubagentStart |
UserPromptSubmit, Stop | no matcher support | always fires on every occurrence |
| no |
If true, runs only once per session then is removed. Skills only, not agents. See Hooks in skills and agents |
"dontAsk""bypassPermissions"hook_event_name | Name of the event that fired |
| Prevents the subagent from stopping |
PostToolUse | No | Shows stderr to Claude (tool already ran) |
PostToolUseFailure | No | Shows stderr to Claude (tool already failed) |
Notification | No | Shows stderr to user only |
SubagentStart | No | Shows stderr to user only |
SessionStart | No | Shows stderr to user only |
SessionEnd | No | Shows stderr to user only |
PreCompact | No | Shows stderr to user only |
falseIf true, hides stdout from verbose mode output |
systemMessage | none | Warning message shown to the user |
decision.behavior (allow/deny) |
agent_type| Optional timeout in milliseconds |
run_in_background | boolean | false | Whether to run the command in background |
| Replacement text |
replace_all | boolean | false | Whether to replace all occurrences |
| Optional number of lines to read |
| Optional glob pattern to filter files |
output_mode | string | "content" | "content", "files_with_matches", or "count". Defaults to "files_with_matches" |
-i | boolean | true | Case insensitive search |
multiline | boolean | false | Enable multiline matching |
| Optional: exclude results from these domains |
| Type of specialized agent to use |
model | string | "sonnet" | Optional model alias to override the default |
Modifies the tool's input parameters before execution. Combine with "allow" to auto-approve, or "ask" to show the modified input to the user |
additionalContext | String added to Claude's context before the tool executes |
For "deny" only: tells Claude why the permission was denied |
interrupt | For "deny" only: if true, stops Claude |