{"id":"hook-developer","name":"hook-developer","summary":"Claudeコードフック完全参照 - 入出力スキーマ、レジストレーション、テストパターン","body":"# Hook Developer\n\nComplete reference for developing Claude Code hooks. Use this to write hooks with correct input/output schemas.\n\n## When to Use\n\n- Creating a new hook\n- Debugging hook input/output format\n- Understanding what fields are available\n- Setting up hook registration in settings.json\n- Learning what hooks can block vs inject context\n\n## Quick Reference\n\n| Hook | Fires When | Can Block? | Primary Use |\n|------|-----------|------------|-------------|\n| **PreToolUse** | Before tool executes | YES | Block/modify tool calls |\n| **PostToolUse** | After tool completes | Partial | React to tool results |\n| **UserPromptSubmit** | User sends prompt | YES | Validate/inject context |\n| **PermissionRequest** | Permission dialog shows | YES | Auto-approve/deny |\n| **SessionStart** | Session begins | NO | Load context, set env vars |\n| **SessionEnd** | Session ends | NO | Cleanup/save state |\n| **Stop** | Agent finishes | YES | Force continuation |\n| **SubagentStart** | Subagent spawns | NO | Pattern coordination |\n| **SubagentStop** | Subagent finishes | YES | Force continuation |\n| **PreCompact** | Before compaction | NO | Save state |\n| **Notification** | Notification sent | NO | Custom alerts |\n\n**Hook type options:** `type: \"command\"` (bash) or `type: \"prompt\"` (LLM evaluation)\n\n---\n\n## Hook Input/Output Schemas\n\n### PreToolUse\n\n**Purpose:** Block or modify tool execution before it happens.\n\n**Input:**\n```json\n{\n  \"session_id\": \"string\",\n  \"transcript_path\": \"string\",\n  \"cwd\": \"string\",\n  \"permission_mode\": \"default|plan|acceptEdits|bypassPermissions\",\n  \"hook_event_name\": \"PreToolUse\",\n  \"tool_name\": \"string\",\n  \"tool_input\": {\n    \"file_path\": \"string\",\n    \"command\": \"string\"\n  },\n  \"tool_use_id\": \"string\"\n}\n```\n\n**Output (JSON):**\n```json\n{\n  \"hookSpecificOutput\": {\n    \"hookEventName\": \"PreToolUse\",\n    \"permissionDecision\": \"allow|deny|ask\",\n    \"permissionDecisionReason\": \"string\",\n    \"updatedInput\": {}\n  },\n  \"continue\": true,\n  \"stopReason\": \"string\",\n  \"systemMessage\": \"string\",\n  \"suppressOutput\": true\n}\n```\n\n**Exit code 2:** Blocks tool, stderr shown to Claude.\n\n**Common matchers:** `Bash`, `Edit|Write`, `Read`, `Task`, `mcp__.*`\n\n---\n\n### PostToolUse\n\n**Purpose:** React to tool execution results, provide feedback to Claude.\n\n**Input:**\n```json\n{\n  \"session_id\": \"string\",\n  \"transcript_path\": \"string\",\n  \"cwd\": \"string\",\n  \"permission_mode\": \"string\",\n  \"hook_event_name\": \"PostToolUse\",\n  \"tool_name\": \"string\",\n  \"tool_input\": {},\n  \"tool_response\": {\n    \"filePath\": \"string\",\n    \"success\": true,\n    \"output\": \"string\",\n    \"exitCode\": 0\n  },\n  \"tool_use_id\": \"string\"\n}\n```\n\n**CRITICAL:** The response field is `tool_response`, NOT `tool_result`.\n\n**Output (JSON):**\n```json\n{\n  \"decision\": \"block\",\n  \"reason\": \"string\",\n  \"hookSpecificOutput\": {\n    \"hookEventName\": \"PostToolUse\",\n    \"additionalContext\": \"string\"\n  },\n  \"continue\": true,\n  \"stopReason\": \"string\",\n  \"suppressOutput\": true\n}\n```\n\n**Blocking:** `\"decision\": \"block\"` with `\"reason\"` prompts Claude to address the issue.\n\n**Common matchers:** `Edit|Write`, `Bash`\n\n---\n\n### UserPromptSubmit\n\n**Purpose:** Validate user prompts, inject context before Claude processes.\n\n**Input:**\n```json\n{\n  \"session_id\": \"string\",\n  \"transcript_path\": \"string\",\n  \"cwd\": \"string\",\n  \"permission_mode\": \"string\",\n  \"hook_event_name\": \"UserPromptSubmit\",\n  \"prompt\": \"string\"\n}\n```\n\n**Output (Plain text):**\n```\nAny stdout text is added to context for Claude.\n```\n\n**Output (JSON):**\n```json\n{\n  \"decision\": \"block\",\n  \"reason\": \"string\",\n  \"hookSpecificOutput\": {\n    \"hookEventName\": \"UserPromptSubmit\",\n    \"additionalContext\": \"string\"\n  }\n}\n```\n\n**Blocking:** `\"decision\": \"block\"` erases prompt, shows `\"reason\"` to user only (not Claude).\n\n**Exit code 2:** Blocks prompt, shows stderr to user only.\n\n---\n\n### PermissionRequest\n\n**Purpose:** Automate permission dialog decisions.\n\n**Input:**\n```json\n{\n  \"session_id\": \"string\",\n  \"transcript_path\": \"string\",\n  \"cwd\": \"string\",\n  \"permission_mode\": \"string\",\n  \"hook_event_name\": \"PermissionRequest\",\n  \"tool_name\": \"string\",\n  \"tool_input\": {}\n}\n```\n\n**Output:**\n```json\n{\n  \"hookSpecificOutput\": {\n    \"hookEventName\": \"PermissionRequest\",\n    \"decision\": {\n      \"behavior\": \"allow|deny\",\n      \"updatedInput\": {},\n      \"message\": \"string\",\n      \"interrupt\": false\n    }\n  }\n}\n```\n\n---\n\n### SessionStart\n\n**Purpose:** Initialize session, load context, set environment variables.\n\n**Input:**\n```json\n{\n  \"session_id\": \"string\",\n  \"transcript_path\": \"string\",\n  \"cwd\": \"string\",\n  \"permission_mode\": \"string\",\n  \"hook_event_name\": \"SessionStart\",\n  \"source\": \"startup|resume|clear|compact\"\n}\n```\n\n**Environment variable:** `CLAUDE_ENV_FILE` - write `export VAR=value` to persist env vars.\n\n**Output (Plain text or JSON):**\n```json\n{\n  \"hookSpecificOutput\": {\n    \"hookEventName\": \"SessionStart\",\n    \"additionalContext\": \"string\"\n  },\n  \"suppressOutput\": true\n}\n```\n\nPlain text stdout is added as context.\n\n---\n\n### SessionEnd\n\n**Purpose:** Cleanup, save state, log session.\n\n**Input:**\n```json\n{\n  \"session_id\": \"string\",\n  \"transcript_path\": \"string\",\n  \"cwd\": \"string\",\n  \"permission_mode\": \"string\",\n  \"hook_event_name\": \"SessionEnd\",\n  \"reason\": \"clear|logout|prompt_input_exit|other\"\n}\n```\n\n**Output:** Cannot affect session (already ending). Use for cleanup only.\n\n---\n\n### Stop\n\n**Purpose:** Control when Claude stops, force continuation.\n\n**Input:**\n```json\n{\n  \"session_id\": \"string\",\n  \"transcript_path\": \"string\",\n  \"cwd\": \"string\",\n  \"permission_mode\": \"string\",\n  \"hook_event_name\": \"Stop\",\n  \"stop_hook_active\": false\n}\n```\n\n**CRITICAL:** Check `stop_hook_active: true` to prevent infinite loops!\n\n**Output:**\n```json\n{\n  \"decision\": \"block\",\n  \"reason\": \"string\"\n}\n```\n\n**Blocking:** `\"decision\": \"block\"` forces Claude to continue with `\"reason\"` as prompt.\n\n---\n\n### SubagentStart\n\n**Purpose:** Run when a subagent (Task tool) is spawned.\n\n**Input:**\n```json\n{\n  \"session_id\": \"string\",\n  \"transcript_path\": \"string\",\n  \"cwd\": \"string\",\n  \"permission_mode\": \"string\",\n  \"hook_event_name\": \"SubagentStart\",\n  \"agent_id\": \"string\"\n}\n```\n\n**Output:** Context injection only (cannot block).\n\n---\n\n### SubagentStop\n\n**Purpose:** Control when subagents (Task tool) stop.\n\n**Input:**\n```json\n{\n  \"session_id\": \"string\",\n  \"transcript_path\": \"string\",\n  \"cwd\": \"string\",\n  \"permission_mode\": \"string\",\n  \"hook_event_name\": \"SubagentStop\",\n  \"stop_hook_active\": false\n}\n```\n\n**Output:** Same as Stop.\n\n---\n\n### PreCompact\n\n**Purpose:** Save state before context compaction.\n\n**Input:**\n```json\n{\n  \"session_id\": \"string\",\n  \"transcript_path\": \"string\",\n  \"cwd\": \"string\",\n  \"permission_mode\": \"string\",\n  \"hook_event_name\": \"PreCompact\",\n  \"trigger\": \"manual|auto\",\n  \"custom_instructions\": \"string\"\n}\n```\n\n**Matchers:** `manual`, `auto`\n\n**Output:**\n```json\n{\n  \"continue\": true,\n  \"systemMessage\": \"string\"\n}\n```\n\n---\n\n### Notification\n\n**Purpose:** Custom notification handling.\n\n**Input:**\n```json\n{\n  \"session_id\": \"string\",\n  \"transcript_path\": \"string\",\n  \"cwd\": \"string\",\n  \"permission_mode\": \"string\",\n  \"hook_event_name\": \"Notification\",\n  \"message\": \"string\",\n  \"notification_type\": \"permission_prompt|idle_prompt|auth_success|elicitation_dialog\"\n}\n```\n\n**Matchers:** `permission_prompt`, `idle_prompt`, `auth_success`, `elicitation_dialog`, `*`\n\n**Output:**\n```json\n{\n  \"continue\": true,\n  \"suppressOutput\": true,\n  \"systemMessage\": \"string\"\n}\n```\n\n---\n\n## Registration in settings.json\n\n### Standard Structure\n\n```json\n{\n  \"hooks\": {\n    \"EventName\": [\n      {\n        \"matcher\": \"ToolPattern\",\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh\",\n            \"timeout\": 60\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\n### Matcher Patterns\n\n| Pattern | Matches |\n|---------|---------|\n| `Bash` | Exactly Bash tool |\n| `Edit\\|Write` | Edit OR Write |\n| `Read.*` | Regex: Read* |\n| `mcp__.*__write.*` | MCP write tools |\n| `*` | All tools |\n\n**Case-sensitive:** `Bash` ≠ `bash`\n\n### Events Requiring Matchers\n\n- PreToolUse - YES (required)\n- PostToolUse - YES (required)\n- PermissionRequest - YES (required)\n- Notification - YES (optional)\n- SessionStart - YES (`startup|resume|clear|compact`)\n- PreCompact - YES (`manual|auto`)\n\n### Events Without Matchers\n\n```json\n{\n  \"hooks\": {\n    \"UserPromptSubmit\": [\n      {\n        \"hooks\": [{ \"type\": \"command\", \"command\": \"/path/to/hook.sh\" }]\n      }\n    ]\n  }\n}\n```\n\n---\n\n## Hook Types\n\n### Command Hooks (type: \"command\")\n\nDefault type. Executes bash commands or scripts.\n\n```json\n{\n  \"type\": \"command\",\n  \"command\": \"$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh\",\n  \"timeout\": 60\n}\n```\n\n### Prompt-Based Hooks (type: \"prompt\")\n\nUses LLM (Haiku) for context-aware decisions. Best for Stop/SubagentStop.\n\n```json\n{\n  \"type\": \"prompt\",\n  \"prompt\": \"Evaluate if Claude should stop. Context: $ARGUMENTS. Check if all tasks are complete.\",\n  \"timeout\": 30\n}\n```\n\n**Response schema:**\n```json\n{\n  \"decision\": \"approve\" | \"block\",\n  \"reason\": \"Explanation\",\n  \"continue\": false,\n  \"stopReason\": \"Message to user\",\n  \"systemMessage\": \"Warning\"\n}\n```\n\n## MCP Tool Naming\n\nMCP tools use pattern `mcp__<server>__<tool>`:\n\n| Pattern | Matches |\n|---------|---------|\n| `mcp__memory__.*` | All memory server tools |\n| `mcp__.*__write.*` | All MCP write tools |\n| `mcp__github__.*` | All GitHub tools |\n\n---\n\n## Environment Variables\n\n### Available to All Hooks\n\n| Variable | Description |\n|----------|-------------|\n| `CLAUDE_PROJECT_DIR` | Absolute path to project root |\n| `CLAUDE_CODE_REMOTE` | \"true\" if remote/web, empty if local CLI |\n\n### SessionStart Only\n\n| Variable | Description |\n|----------|-------------|\n| `CLAUDE_ENV_FILE` | Path to write `export VAR=value` lines |\n\n### Plugin Hooks Only\n\n| Variable | Description |\n|----------|-------------|\n| `CLAUDE_PLUGIN_ROOT` | Absolute path to plugin directory |\n\n---\n\n## Exit Codes\n\n| Exit Code | Behavior | stdout | stderr |\n|-----------|----------|--------|--------|\n| **0** | Success | JSON processed | Ignored |\n| **2** | Blocking error | IGNORED | Error message |\n| **Other** | Non-blocking error | Ignored | Verbose mode |\n\n### Exit Code 2 by Hook\n\n| Hook | Effect |\n|------|--------|\n| PreToolUse | Blocks tool, stderr to Claude |\n| PostToolUse | stderr to Claude (tool already ran) |\n| UserPromptSubmit | Blocks prompt, stderr to user only |\n| Stop | Blocks stop, stderr to Claude |\n\n---\n\n## Shell Wrapper Pattern\n\n```bash\n#!/bin/bash\nset -e\ncd \"$CLAUDE_PROJECT_DIR/.claude/hooks\"\ncat | npx tsx src/my-hook.ts\n```\n\nOr for bundled:\n\n```bash\n#!/bin/bash\nset -e\ncd \"$HOME/.claude/hooks\"\ncat | node dist/my-hook.mjs\n```\n\n---\n\n## TypeScript Handler Pattern\n\n```typescript\nimport { readFileSync } from 'fs';\n\ninterface HookInput {\n  session_id: string;\n  hook_event_name: string;\n  tool_name?: string;\n  tool_input?: Record<string, unknown>;\n  tool_response?: Record<string, unknown>;\n  // ... other fields per hook type\n}\n\nfunction readStdin(): string {\n  return readFileSync(0, 'utf-8');\n}\n\nasync function main() {\n  const input: HookInput = JSON.parse(readStdin());\n\n  // Process input\n\n  const output = {\n    decision: 'block',  // or undefined to allow\n    reason: 'Why blocking'\n  };\n\n  console.log(JSON.stringify(output));\n}\n\nmain().catch(console.error);\n```\n\n---\n\n## Testing Hooks\n\n### Manual Test Commands\n\n```bash\n# PostToolUse (Write)\necho '{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"test.md\"},\"tool_response\":{\"success\":true},\"session_id\":\"test\"}' | \\\n  .claude/hooks/my-hook.sh\n\n# PreToolUse (Bash)\necho '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"ls\"},\"session_id\":\"test\"}' | \\\n  .claude/hooks/my-hook.sh\n\n# SessionStart\necho '{\"hook_event_name\":\"SessionStart\",\"source\":\"startup\",\"session_id\":\"test\"}' | \\\n  .claude/hooks/session-start.sh\n\n# SessionEnd\necho '{\"hook_event_name\":\"SessionEnd\",\"reason\":\"clear\",\"session_id\":\"test\"}' | \\\n  .claude/hooks/session-end.sh\n\n# UserPromptSubmit\necho '{\"prompt\":\"test prompt\",\"session_id\":\"test\"}' | \\\n  .claude/hooks/prompt-submit.sh\n```\n\n### Rebuild After TypeScript Edits\n\n```bash\ncd .claude/hooks\nnpx esbuild src/my-hook.ts \\\n  --bundle --platform=node --format=esm \\\n  --outfile=dist/my-hook.mjs\n```\n\n---\n\n## Common Patterns\n\n### Block Dangerous Files (PreToolUse)\n\n```python\n#!/usr/bin/env python3\nimport json, sys\n\ndata = json.load(sys.stdin)\npath = data.get('tool_input', {}).get('file_path', '')\n\nBLOCKED = ['.env', 'secrets.json', '.git/']\nif any(b in path for b in BLOCKED):\n    print(json.dumps({\n        \"hookSpecificOutput\": {\n            \"hookEventName\": \"PreToolUse\",\n            \"permissionDecision\": \"deny\",\n            \"permissionDecisionReason\": f\"Blocked: {path} is protected\"\n        }\n    }))\nelse:\n    print('{}')\n```\n\n### Auto-Format Files (PostToolUse)\n\n```bash\n#!/bin/bash\nINPUT=$(cat)\nFILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty')\n\nif [[ \"$FILE\" == *.ts ]] || [[ \"$FILE\" == *.tsx ]]; then\n  npx prettier --write \"$FILE\" 2>/dev/null\nfi\n\necho '{}'\n```\n\n### Inject Git Context (UserPromptSubmit)\n\n```bash\n#!/bin/bash\necho \"Git status:\"\ngit status --short 2>/dev/null || echo \"(not a git repo)\"\necho \"\"\necho \"Recent commits:\"\ngit log --oneline -5 2>/dev/null || echo \"(no commits)\"\n```\n\n### Force Test Verification (Stop)\n\n```python\n#!/usr/bin/env python3\nimport json, sys, subprocess\n\ndata = json.load(sys.stdin)\n\n# Prevent infinite loops\nif data.get('stop_hook_active'):\n    print('{}')\n    sys.exit(0)\n\n# Check if tests pass\nresult = subprocess.run(['npm', 'test'], capture_output=True)\nif result.returncode != 0:\n    print(json.dumps({\n        \"decision\": \"block\",\n        \"reason\": \"Tests are failing. Please fix before stopping.\"\n    }))\nelse:\n    print('{}')\n```\n\n---\n\n## Debugging Checklist\n\n- [ ] Hook registered in settings.json?\n- [ ] Shell script has `+x` permission?\n- [ ] Bundle rebuilt after TS changes?\n- [ ] Using `tool_response` not `tool_result`?\n- [ ] Output is valid JSON (or plain text)?\n- [ ] Checking `stop_hook_active` in Stop hooks?\n- [ ] Using `$CLAUDE_PROJECT_DIR` for paths?\n\n---\n\n## Key Learnings from Past Sessions\n\n1. **Field names matter** - `tool_response` not `tool_result`\n2. **Output format** - `decision: \"block\"` + `reason` for blocking\n3. **Exit code 2** - stderr goes to Claude/user, stdout IGNORED\n4. **Rebuild bundles** - TypeScript source edits don't auto-apply\n5. **Test manually** - `echo '{}' | ./hook.sh` before relying on it\n6. **Check outputs first** - `ls .claude/cache/` before editing code\n7. **Detached spawn hides errors** - add logging to debug\n\n## See Also\n\n- `/debug-hooks` - Systematic debugging workflow\n- `.claude/rules/hooks.md` - Hook development rules","author":"@parcadei","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/parcadei/Continuous-Claude-v3/tree/main/.claude/skills/hook-developer","license":"MIT","category":"writing","lang":"en","tokens":3971,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[],"requires":{"mcp":["memory","github"],"tools":[]},"safety":{"flags":[{"code":"injection.disable-permissions","kind":"injection","where":"SKILL.md:45","excerpt":"bypassPermissions","message":"instructs the agent to disable permission checks","severity":"warn"}],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":[]}}