一键导入
hook-developer
Complete Claude Code hooks reference - input/output schemas, registration, testing patterns
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Complete Claude Code hooks reference - input/output schemas, registration, testing patterns
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
How to use AI APIs like OpenAI, ChatGPT, Elevenlabs, etc. When a user asks you to make an app that requires an AI API, use this skill to understand how to use the API or how to respond to the user.
Debug Gemini/GPT vision API issues in puzzle image extraction
Constraint satisfaction patterns for the Pips puzzle solver
Create git commits with user approval and no Claude attribution
Create or update continuity ledger for state preservation across clears
Create handoff document for transferring work to another session
| name | hook-developer |
| description | Complete Claude Code hooks reference - input/output schemas, registration, testing patterns |
Complete reference for developing Claude Code hooks. Use this to write hooks with correct input/output schemas.
| Hook | Fires When | Can Block? | Primary Use |
|---|---|---|---|
| PreToolUse | Before tool executes | YES | Block/modify tool calls |
| PostToolUse | After tool completes | Partial | React to tool results |
| UserPromptSubmit | User sends prompt | YES | Validate/inject context |
| PermissionRequest | Permission dialog shows | YES | Auto-approve/deny |
| SessionStart | Session begins | NO | Load context |
| SessionEnd | Session ends | NO | Cleanup/save state |
| Stop | Agent finishes | YES | Force continuation |
| SubagentStop | Subagent finishes | YES | Force continuation |
| PreCompact | Before compaction | NO | Save state |
| Notification | Notification sent | NO | Custom alerts |
Purpose: Block or modify tool execution before it happens.
Input:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "default|plan|acceptEdits|bypassPermissions",
"hook_event_name": "PreToolUse",
"tool_name": "string",
"tool_input": {
"file_path": "string",
"command": "string"
},
"tool_use_id": "string"
}
Output (JSON):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "string",
"updatedInput": {}
},
"continue": true,
"stopReason": "string",
"systemMessage": "string",
"suppressOutput": true
}
Exit code 2: Blocks tool, stderr shown to Claude.
Common matchers: Bash, Edit|Write, Read, Task, mcp__.*
Purpose: React to tool execution results, provide feedback to Claude.
Input:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "PostToolUse",
"tool_name": "string",
"tool_input": {},
"tool_response": {
"filePath": "string",
"success": true,
"output": "string",
"exitCode": 0
},
"tool_use_id": "string"
}
CRITICAL: The response field is tool_response, NOT tool_result.
Output (JSON):
{
"decision": "block",
"reason": "string",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "string"
},
"continue": true,
"stopReason": "string",
"suppressOutput": true
}
Blocking: "decision": "block" with "reason" prompts Claude to address the issue.
Common matchers: Edit|Write, Bash
Purpose: Validate user prompts, inject context before Claude processes.
Input:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "UserPromptSubmit",
"prompt": "string"
}
Output (Plain text):
Any stdout text is added to context for Claude.
Output (JSON):
{
"decision": "block",
"reason": "string",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "string"
}
}
Blocking: "decision": "block" erases prompt, shows "reason" to user only (not Claude).
Exit code 2: Blocks prompt, shows stderr to user only.
Purpose: Automate permission dialog decisions.
Input:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "PermissionRequest",
"tool_name": "string",
"tool_input": {}
}
Output:
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow|deny",
"updatedInput": {},
"message": "string",
"interrupt": false
}
}
}
Purpose: Initialize session, load context, set environment variables.
Input:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "SessionStart",
"source": "startup|resume|clear|compact"
}
Environment variable: CLAUDE_ENV_FILE - write export VAR=value to persist env vars.
Output (Plain text or JSON):
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "string"
},
"suppressOutput": true
}
Plain text stdout is added as context.
Purpose: Cleanup, save state, log session.
Input:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "SessionEnd",
"reason": "clear|logout|prompt_input_exit|other"
}
Output: Cannot affect session (already ending). Use for cleanup only.
Purpose: Control when Claude stops, force continuation.
Input:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "Stop",
"stop_hook_active": false
}
CRITICAL: Check stop_hook_active: true to prevent infinite loops!
Output:
{
"decision": "block",
"reason": "string"
}
Blocking: "decision": "block" forces Claude to continue with "reason" as prompt.
Purpose: Control when subagents (Task tool) stop.
Input:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "SubagentStop",
"stop_hook_active": false
}
Output: Same as Stop.
Purpose: Save state before context compaction.
Input:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "PreCompact",
"trigger": "manual|auto",
"custom_instructions": "string"
}
Matchers: manual, auto
Output:
{
"continue": true,
"systemMessage": "string"
}
Purpose: Custom notification handling.
Input:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "Notification",
"message": "string",
"notification_type": "permission_prompt|idle_prompt|auth_success|elicitation_dialog"
}
Matchers: permission_prompt, idle_prompt, auth_success, elicitation_dialog, *
Output:
{
"continue": true,
"suppressOutput": true,
"systemMessage": "string"
}
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh",
"timeout": 60
}
]
}
]
}
}
| Pattern | Matches |
|---|---|
Bash | Exactly Bash tool |
Edit|Write | Edit OR Write |
Read.* | Regex: Read* |
mcp__.*__write.* | MCP write tools |
* | All tools |
Case-sensitive: Bash ≠ bash
startup|resume|clear|compact)manual|auto){
"hooks": {
"UserPromptSubmit": [
{
"hooks": [{ "type": "command", "command": "/path/to/hook.sh" }]
}
]
}
}
| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR | Absolute path to project root |
CLAUDE_CODE_REMOTE | "true" if remote, empty if local |
| Variable | Description |
|---|---|
CLAUDE_ENV_FILE | Path to write export VAR=value lines |
| Exit Code | Behavior | stdout | stderr |
|---|---|---|---|
| 0 | Success | JSON processed | Ignored |
| 2 | Blocking error | IGNORED | Error message |
| Other | Non-blocking error | Ignored | Verbose mode |
| Hook | Effect |
|---|---|
| PreToolUse | Blocks tool, stderr to Claude |
| PostToolUse | stderr to Claude (tool already ran) |
| UserPromptSubmit | Blocks prompt, stderr to user only |
| Stop | Blocks stop, stderr to Claude |
#!/bin/bash
set -e
cd "$CLAUDE_PROJECT_DIR/.claude/hooks"
cat | npx tsx src/my-hook.ts
Or for bundled:
#!/bin/bash
set -e
cd "$HOME/.claude/hooks"
cat | node dist/my-hook.mjs
import { readFileSync } from 'fs';
interface HookInput {
session_id: string;
hook_event_name: string;
tool_name?: string;
tool_input?: Record<string, unknown>;
tool_response?: Record<string, unknown>;
// ... other fields per hook type
}
function readStdin(): string {
return readFileSync(0, 'utf-8');
}
async function main() {
const input: HookInput = JSON.parse(readStdin());
// Process input
const output = {
decision: 'block', // or undefined to allow
reason: 'Why blocking'
};
console.log(JSON.stringify(output));
}
main().catch(console.error);
# PostToolUse (Write)
echo '{"tool_name":"Write","tool_input":{"file_path":"test.md"},"tool_response":{"success":true},"session_id":"test"}' | \
.claude/hooks/my-hook.sh
# PreToolUse (Bash)
echo '{"tool_name":"Bash","tool_input":{"command":"ls"},"session_id":"test"}' | \
.claude/hooks/my-hook.sh
# SessionStart
echo '{"hook_event_name":"SessionStart","source":"startup","session_id":"test"}' | \
.claude/hooks/session-start.sh
# SessionEnd
echo '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"test"}' | \
.claude/hooks/session-end.sh
# UserPromptSubmit
echo '{"prompt":"test prompt","session_id":"test"}' | \
.claude/hooks/prompt-submit.sh
cd .claude/hooks
npx esbuild src/my-hook.ts \
--bundle --platform=node --format=esm \
--outfile=dist/my-hook.mjs
#!/usr/bin/env python3
import json, sys
data = json.load(sys.stdin)
path = data.get('tool_input', {}).get('file_path', '')
BLOCKED = ['.env', 'secrets.json']
if any(b in path for b in BLOCKED):
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": f"Blocked: {path} is protected"
}
}))
else:
print('{}')
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" == *.ts ]] || [[ "$FILE" == *.tsx ]]; then
npx prettier --write "$FILE" 2>/dev/null
fi
echo '{}'
#!/bin/bash
echo "Git status:"
git status --short 2>/dev/null || echo "(not a git repo)"
echo ""
echo "Recent commits:"
git log --oneline -5 2>/dev/null || echo "(no commits)"
#!/usr/bin/env python3
import json, sys, subprocess
data = json.load(sys.stdin)
# Prevent infinite loops
if data.get('stop_hook_active'):
print('{}')
sys.exit(0)
# Check if tests pass
result = subprocess.run(['npm', 'test'], capture_output=True)
if result.returncode != 0:
print(json.dumps({
"decision": "block",
"reason": "Tests are failing. Please fix before stopping."
}))
else:
print('{}')
+x permission?tool_response not tool_result?stop_hook_active in Stop hooks?$CLAUDE_PROJECT_DIR for paths?tool_response not tool_resultdecision: "block" + reason for blockingecho '{}' | ./hook.sh before relying on itls .claude/cache/ before editing code/debug-hooks - Systematic debugging workflow.claude/rules/hooks.md - Hook development rules