hook-developer skill | PlugMCP
Skip to content Install The skill file stays in the publisher's repository.
Add to Cursor Add to Claude Code Add to CodexPaste the copied command in a terminal. It installs this SKILL.md into that agent.
npx skills add parcadei/Continuous-Claude-v3 --skill hook-developer -a cursor -y Copy Download skill .claude/skills/hook-developer/SKILL.md on GitHub Questions
How do I install hook-developer? Download .claude/skills/hook-developer/SKILL.md from the publisher, or copy the path and open the repository. PlugMCP lists the skill and does not host the file.
Where does hook-developer live? The publisher keeps it at https://github.com/parcadei/Continuous-Claude-v3.
Which agents can use hook-developer? Claude, Cursor, and Codex can read a SKILL.md you add from the publisher's repository. SKILL.md About FAQ
Complete Claude Code hooks reference - input/output schemas, registration, testing patterns
name: hook-developer
description: Complete Claude Code hooks reference - input/output schemas, registration, testing patterns
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
plugmcp Plug any tool into any agent. MCP servers for people who do not write servers. Gateway for volume, n8n for apps.
PlugMCP, 2026
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.
{
"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"
}
{
"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.
{
"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.
{
"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.
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "UserPromptSubmit",
"prompt": "string"
}
Any stdout text is added to context for Claude.
{
"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.
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "PermissionRequest",
"tool_name": "string",
"tool_input": {}
}
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow|deny",
"updatedInput": {},
"message": "string",
"interrupt": false
}
}
}
SessionStart Purpose: Initialize session, load context, set environment variables.
{
"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.
{
"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.
{
"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!
{
"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.
{
"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.
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "SubagentStop",
"stop_hook_active": false
}
PreCompact Purpose: Save state before context compaction.
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "PreCompact",
"trigger": "manual|auto",
"custom_instructions": "string"
}
{
"continue": true,
"systemMessage": "string"
}
Notification Purpose: Custom notification handling.
{
"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, *
{
"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 BashExactly Bash tool Edit|WriteEdit OR 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
}
{
"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_DIRAbsolute path to project root CLAUDE_CODE_REMOTE"true" if remote/web, empty if local CLI
SessionStart Only Variable Description CLAUDE_ENV_FILEPath to write export VAR=value lines
Plugin Hooks Only Variable Description CLAUDE_PLUGIN_ROOTAbsolute 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.ts
#!/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.sh
Rebuild 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
Key Learnings from Past Sessions
Field names matter - tool_response not tool_result
Output format - decision: "block" + reason for blocking
Exit code 2 - stderr goes to Claude/user, stdout IGNORED
Rebuild bundles - TypeScript source edits don't auto-apply
Test manually - echo '{}' | ./hook.sh before relying on it
Check outputs first - ls .claude/cache/ before editing code
Detached spawn hides errors - add logging to debug
See Also
/debug-hooks - Systematic debugging workflow
.claude/rules/hooks.md - Hook development rules
How do I install hook-developer? Download .claude/skills/hook-developer/SKILL.md from the publisher, or copy the path and open the repository. PlugMCP lists the skill and does not host the file.
Where does hook-developer live? The publisher keeps it at https://github.com/parcadei/Continuous-Claude-v3.
Which agents can use hook-developer? Claude, Cursor, and Codex can read a SKILL.md you add from the publisher's repository.