
Hook Developer
- 485 installs
- 3.9k repo stars
- Updated January 26, 2026
- parcadei/continuous-claude-v3
hook-developer is a Claude Code skill that provides a complete reference for writing Claude Code hooks with correct input/output schemas, registration, and testing patterns.
About
hook-developer from continuous-claude-v3 is the authoritative Claude Code hooks reference for developers extending agent behavior. It documents when each hook fires, whether it can block tool execution, and primary use cases in a quick-reference table covering PreToolUse and PostToolUse among others. Developers use hook-developer when creating hooks, debugging input/output formats, registering hooks in settings.json, or learning which hooks block versus inject context. The skill ensures custom developer workflows trigger automatically at the correct moment during long-running sessions.
- Automatically triggers developer hooks based on conversation context
- Enables continuous Claude sessions that self-extend with custom capabilities
- Reduces context-window waste by calling tools only when relevant
- Supports custom hook definitions for project-specific automation
Hook Developer by the numbers
- 485 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,801 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/parcadei/continuous-claude-v3 --skill hook-developerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 485 |
|---|---|
| repo stars | ★ 3.9k |
| Last updated | January 26, 2026 |
| Repository | parcadei/continuous-claude-v3 ↗ |
How do you write Claude Code hooks with correct schemas?
Let Claude automatically invoke custom developer tools and workflows at the right moment during long-running coding sessions.
Who is it for?
Developers extending Claude Code with custom hooks who need schema-accurate registration and testing guidance.
Skip if: Non-Claude agent platforms without hook APIs, or one-off scripts that do not integrate with Claude Code lifecycle events.
When should I use this skill?
Creating a new Claude Code hook, debugging hook I/O, or configuring hook registration in settings.json.
What you get
Registered Claude Code hooks, validated input/output schemas, and tested block-or-context-injection behavior.
- Hook implementation
- settings.json registration
- Hook test validation
Files
Hook Developer
Complete reference for developing Claude Code hooks. Use this to write hooks with correct input/output schemas.
When to Use
- Creating a new hook
- Debugging hook input/output format
- Understanding what fields are available
- Setting up hook registration in settings.json
- Learning what hooks can block vs inject context
Quick Reference
| 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, set env vars |
| SessionEnd | Session ends | NO | Cleanup/save state |
| Stop | Agent finishes | YES | Force continuation |
| SubagentStart | Subagent spawns | NO | Pattern coordination |
| SubagentStop | Subagent finishes | YES | Force continuation |
| PreCompact | Before compaction | NO | Save state |
| Notification | Notification sent | NO | Custom alerts |
Hook type options: type: "command" (bash) or type: "prompt" (LLM evaluation)
---
Hook Input/Output Schemas
PreToolUse
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__.*
---
PostToolUse
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
---
UserPromptSubmit
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.
---
PermissionRequest
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
}
}
}---
SessionStart
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.
---
SessionEnd
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.
---
Stop
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.
---
SubagentStart
Purpose: Run when a subagent (Task tool) is spawned.
Input:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "SubagentStart",
"agent_id": "string"
}Output: Context injection only (cannot block).
---
SubagentStop
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.
---
PreCompact
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"
}---
Notification
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"
}---
Registration in settings.json
Standard Structure
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh",
"timeout": 60
}
]
}
]
}
}Matcher Patterns
| Pattern | Matches |
|---|---|
Bash | Exactly Bash tool |
| `Edit\ | Write` |
Read.* | Regex: Read* |
mcp__.*__write.* | MCP write tools |
* | All tools |
Case-sensitive: Bash ≠ bash
Events Requiring Matchers
- PreToolUse - YES (required)
- PostToolUse - YES (required)
- PermissionRequest - YES (required)
- Notification - YES (optional)
- SessionStart - YES (
startup|resume|clear|compact) - PreCompact - YES (
manual|auto)
Events Without Matchers
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [{ "type": "command", "command": "/path/to/hook.sh" }]
}
]
}
}---
Hook Types
Command Hooks (type: "command")
Default type. Executes bash commands or scripts.
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh",
"timeout": 60
}Prompt-Based Hooks (type: "prompt")
Uses LLM (Haiku) for context-aware decisions. Best for Stop/SubagentStop.
{
"type": "prompt",
"prompt": "Evaluate if Claude should stop. Context: $ARGUMENTS. Check if all tasks are complete.",
"timeout": 30
}Response schema:
{
"decision": "approve" | "block",
"reason": "Explanation",
"continue": false,
"stopReason": "Message to user",
"systemMessage": "Warning"
}MCP Tool Naming
MCP tools use pattern mcp__<server>__<tool>:
| Pattern | Matches |
|---|---|
mcp__memory__.* | All memory server tools |
mcp__.*__write.* | All MCP write tools |
mcp__github__.* | All GitHub tools |
---
Environment Variables
Available to All Hooks
| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR | Absolute path to project root |
CLAUDE_CODE_REMOTE | "true" if remote/web, empty if local CLI |
SessionStart Only
| Variable | Description |
|---|---|
CLAUDE_ENV_FILE | Path to write export VAR=value lines |
Plugin Hooks Only
| Variable | Description |
|---|---|
CLAUDE_PLUGIN_ROOT | Absolute path to plugin directory |
---
Exit Codes
| Exit Code | Behavior | stdout | stderr |
|---|---|---|---|
| 0 | Success | JSON processed | Ignored |
| 2 | Blocking error | IGNORED | Error message |
| Other | Non-blocking error | Ignored | Verbose mode |
Exit Code 2 by Hook
| 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 |
---
Shell Wrapper Pattern
#!/bin/bash
set -e
cd "$CLAUDE_PROJECT_DIR/.claude/hooks"
cat | npx tsx src/my-hook.tsOr for bundled:
#!/bin/bash
set -e
cd "$HOME/.claude/hooks"
cat | node dist/my-hook.mjs---
TypeScript Handler Pattern
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);---
Testing Hooks
Manual Test Commands
# 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.shRebuild After TypeScript Edits
cd .claude/hooks
npx esbuild src/my-hook.ts \
--bundle --platform=node --format=esm \
--outfile=dist/my-hook.mjs---
Common Patterns
Block Dangerous Files (PreToolUse)
#!/usr/bin/env python3
import json, sys
data = json.load(sys.stdin)
path = data.get('tool_input', {}).get('file_path', '')
BLOCKED = ['.env', 'secrets.json', '.git/']
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('{}')Auto-Format Files (PostToolUse)
#!/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 '{}'Inject Git Context (UserPromptSubmit)
#!/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)"Force Test Verification (Stop)
#!/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('{}')---
Debugging Checklist
- [ ] Hook registered in settings.json?
- [ ] Shell script has
+xpermission? - [ ] Bundle rebuilt after TS changes?
- [ ] Using
tool_responsenottool_result? - [ ] Output is valid JSON (or plain text)?
- [ ] Checking
stop_hook_activein Stop hooks? - [ ] Using
$CLAUDE_PROJECT_DIRfor paths?
---
Key Learnings from Past Sessions
1. Field names matter - tool_response not tool_result 2. Output format - decision: "block" + reason for blocking 3. Exit code 2 - stderr goes to Claude/user, stdout IGNORED 4. Rebuild bundles - TypeScript source edits don't auto-apply 5. Test manually - echo '{}' | ./hook.sh before relying on it 6. Check outputs first - ls .claude/cache/ before editing code 7. Detached spawn hides errors - add logging to debug
See Also
/debug-hooks- Systematic debugging workflow.claude/rules/hooks.md- Hook development rules
Related skills
FAQ
Can Claude Code hooks block tool execution?
hook-developer notes PreToolUse fires before a tool executes and CAN block or modify tool calls. PostToolUse fires after execution. The skill's quick-reference table lists block capability per hook type.
Where are Claude Code hooks registered?
hook-developer covers hook registration in settings.json plus input/output schema requirements. Developers use it when creating hooks, debugging formats, or learning available hook fields.