Unnamed Skill
Complete guide for writing Claude Code and SDK hooks with security-first design.Triggers: hook creation, hook writing, PreToolUse, PostToolUse, UserPromptSubmit,tool validation, logging hooks, context injection, workflow automationUse when: creating new hooks for tool validation, logging operations for audit,injecting context before prompts, enforcing project-specific workflows,preventing dangerous operations in productionDO NOT use when: logic belongs in core skill - use Skills instead.DO NOT use when: complex multi-step workflows needed - use Agents instead.DO NOT use when: behavior better suited for custom tool.Use this skill BEFORE writing any hook. Check even if unsure.
$ 安裝
git clone https://github.com/athola/claude-night-market /tmp/claude-night-market && cp -r /tmp/claude-night-market/plugins/abstract/skills/hook-authoring ~/.claude/skills/claude-night-market// tip: Run this command in your terminal to install the skill
name: hook-authoring description: |
Triggers: validation, sdk, automation, hook, authoring Complete guide for writing Claude Code and SDK hooks with security-first design.
Triggers: hook creation, hook writing, PreToolUse, PostToolUse, UserPromptSubmit, tool validation, logging hooks, context injection, workflow automation
Use when: creating new hooks for tool validation, logging operations for audit, injecting context before prompts, enforcing project-specific workflows, preventing dangerous operations in production
DO NOT use when: logic belongs in core skill - use Skills instead. DO NOT use when: complex multi-step workflows needed - use Agents instead. DO NOT use when: behavior better suited for custom tool.
Use this skill BEFORE writing any hook. Check even if unsure. version: 1.0.0 category: hook-development tags: [hooks, sdk, security, performance, automation, validation] dependencies: [] estimated_tokens: 1200 complexity: intermediate provides: patterns: [hook-authoring, security-patterns, performance-optimization] infrastructure: [hook-validation, testing-framework] usage_patterns:
- writing-hooks
- hook-validation
- security-patterns
- performance-optimization
- sdk-integration
Table of Contents
- Overview
- Key Capabilities
- Quick Start
- Your First Hook (JSON - Claude Code)
- Your First Hook (Python - Claude Agent SDK)
- Hook Event Types
- Claude Code vs SDK
- JSON Hooks (Claude Code)
- Python SDK Hooks
- Security Essentials
- Critical Security Rules
- Example: Secure Logging Hook
- Performance Guidelines
- Performance Best Practices
- Example: Efficient Hook
- Scope Selection
- Decision Framework
- Scope Comparison
- Common Patterns
- Validation Hook
- Logging Hook
- Context Injection Hook
- Testing Hooks
- Unit Testing
- Module References
- Tools
- Related Skills
- Next Steps
- References
Hook Authoring Guide
Overview
Hooks are event interceptors that allow you to extend Claude Code and Claude Agent SDK behavior by executing custom logic at specific points in the agent lifecycle. They enable validation before tool use, logging after actions, context injection, workflow automation, and security enforcement.
This skill teaches you how to write effective, secure, and performant hooks for both declarative JSON (Claude Code) and programmatic Python (Claude Agent SDK) use cases.
Key Capabilities
- PreToolUse: Validate, filter, or transform tool inputs before execution
- PostToolUse: Log, analyze, or modify tool outputs after execution
- UserPromptSubmit: Inject context or filter user messages before processing
- Stop/SubagentStop: Cleanup, final reporting, or result aggregation
- PreCompact: State preservation before context window compaction
Quick Start
Your First Hook (JSON - Claude Code)
Create a simple logging hook in .claude/settings.json:
{
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "echo \"$(date): Executed $CLAUDE_TOOL_NAME\" >> ~/.claude/audit.log"
}]
}
]
}
Note: Use string matchers ("Bash") not object matchers ({"toolName": "Bash"}).
Verification: Run the command with --help flag to verify availability.
This logs every Bash command execution with a timestamp.
Your First Hook (Python - Claude Agent SDK)
Create a validation hook using the SDK:
from claude_agent_sdk import AgentHooks
class ValidationHooks(AgentHooks):
async def on_pre_tool_use(self, tool_name: str, tool_input: dict) -> dict | None:
"""Validate tool inputs before execution."""
if tool_name == "Bash":
command = tool_input.get("command", "")
if "rm -rf /" in command:
raise ValueError("Dangerous command blocked by hook")
# Return None to proceed unchanged, or modified dict to transform
return None
Verification: Run the command with --help flag to verify availability.
Hook Event Types
Quick reference for all supported hook events:
| Event | Trigger Point | Parameters | Common Use Cases |
|---|---|---|---|
| PreToolUse | Before tool execution | tool_name, tool_input | Validation, filtering, input transformation |
| PostToolUse | After tool execution | tool_name, tool_input, tool_output | Logging, metrics, output transformation |
| UserPromptSubmit | User sends message | message | Context injection, content filtering |
| PermissionRequest | Permission dialog shown | tool_name, tool_input | Auto-approve/deny with custom logic |
| Notification | Claude Code sends notification | message | Custom notification handling |
| Stop | Agent completes | reason, result | Final cleanup, summary reports |
| SubagentStop | Subagent completes | subagent_id, result | Result processing, aggregation |
| PreCompact | Before context compact | context_size | State preservation, checkpointing |
| SessionStart | Session starts/resumes | session_id, source, agent_type | Initialization, context loading |
| SessionEnd | Session terminates | session_id | Cleanup, final logging |
SessionStart Input Schema (Claude Code 2.1.2+)
The SessionStart hook receives JSON input via stdin with these fields:
{
"session_id": "abc123",
"source": "startup", // "startup" | "resume" | "clear" | "compact"
"agent_type": "my-agent" // Populated if --agent flag used
}
agent_type field: When Claude Code is launched with --agent my-agent, this field contains the agent name, enabling agent-specific initialization:
# Python example: Agent-aware SessionStart hook
input_data = json.loads(sys.stdin.read())
agent_type = input_data.get("agent_type", "")
if agent_type in ["code-reviewer", "quick-query"]:
# Skip heavy context injection for lightweight agents
print(json.dumps({"hookSpecificOutput": {"additionalContext": "Minimal context"}}))
else:
# Full initialization for implementation agents
print(json.dumps({"hookSpecificOutput": {"additionalContext": full_context}}))
# Bash example: Agent-aware SessionStart hook
HOOK_INPUT=$(cat)
AGENT_TYPE=$(echo "$HOOK_INPUT" | jq -r '.agent_type // empty')
case "$AGENT_TYPE" in
code-reviewer|quick-query)
echo '{"hookSpecificOutput": {"additionalContext": "Minimal context"}}'
;;
*)
echo '{"hookSpecificOutput": {"additionalContext": "Full context"}}'
;;
esac
Hooks in Frontmatter (Claude Code 2.1.0+)
New in 2.1.0: Define hooks directly in skill, command, or agent frontmatter. These hooks are scoped to the component's lifecycle.
Skill/Command/Agent Frontmatter Hooks
---
name: validated-skill
description: Skill with lifecycle hooks
hooks:
PreToolUse:
- matcher: "Bash"
command: "./validate-command.sh"
once: true # NEW: Run only once per session
- matcher: "Write|Edit"
command: "./pre-edit-check.sh"
PostToolUse:
- matcher: "Write|Edit"
command: "./format-on-save.sh"
Stop:
- command: "./cleanup-and-report.sh"
---
The once: true Configuration
New in 2.1.0: Use once: true to execute a hook only once per session, ideal for:
- One-time setup/initialization
- Resource allocation that shouldn't repeat
- Session-level configuration
hooks:
PreToolUse:
- matcher: "Bash"
command: "./setup-environment.sh"
once: true # Runs only on first Bash call
SessionStart:
- command: "./initialize-session.sh"
once: true # Runs only once at session start
Frontmatter vs Settings Hooks
| Aspect | Frontmatter Hooks | Settings Hooks |
|---|---|---|
| Scope | Component lifecycle | Global/project |
| Location | In skill/agent/command | settings.json |
| Persistence | Active only when component runs | Always active |
| Use case | Component-specific validation | Cross-cutting concerns |
PreToolUse updatedInput (2.1.0 Fix)
PreToolUse hooks can now return updatedInput when returning ask permission decision, enabling hooks to act as middleware while still requesting user consent:
{
"decision": "ask",
"updatedInput": {
"command": "modified-command --safe-flag"
}
}
Claude Code vs SDK
JSON Hooks (Claude Code)
Declarative configuration in .claude/settings.json, project .claude/settings.json, or plugin hooks/hooks.json:
{
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [{
"type": "command",
"command": "echo 'WARNING: Editing production file' >&2"
}]
}
]
}
Important: Use string matchers (regex patterns), not object matchers. The object format {"toolName": "Edit"} is deprecated.
Matcher patterns:
"Edit"- Match single tool"Read|Write|Edit"- Match multiple tools (regex OR)".*"- Match all tools
Verification: Run the command with --help flag to verify availability.
Pros: Simple, no code required, easy to version control Cons: Limited logic capabilities, shell command only
Python SDK Hooks
Programmatic callbacks using AgentHooks base class:
from claude_agent_sdk import AgentHooks
class MyHooks(AgentHooks):
async def on_pre_tool_use(self, tool_name: str, tool_input: dict) -> dict | None:
# Complex validation logic
if self._is_dangerous(tool_input):
raise ValueError("Operation blocked")
return None # or return modified input
Verification: Run the command with --help flag to verify availability.
Pros: Full Python capabilities, complex logic, state management Cons: Requires Python, more complex setup
Security Essentials
Critical Security Rules
- Input Validation: Always validate tool inputs before processing
- No Secret Logging: Never log API keys, tokens, passwords, or credentials
- Sandbox Awareness: Respect sandbox boundaries, don't escape
- Fail-Safe Defaults: Return None on error instead of blocking the agent
- Rate Limiting: Prevent hook abuse from malicious or buggy code
- Injection Prevention: Sanitize all logged content to prevent log injection
Example: Secure Logging Hook
import re
from claude_agent_sdk import AgentHooks
class SecureLoggingHooks(AgentHooks):
# Patterns that might contain secrets
SECRET_PATTERNS = [
r'api[_-]?key',
r'password',
r'token',
r'secret',
r'credential',
r'auth',
]
def _sanitize_output(self, text: str) -> str:
"""Remove potential secrets from log output."""
for pattern in self.SECRET_PATTERNS:
text = re.sub(
rf'({pattern}["\s:=]+)([^\s,}}]+)',
r'\1***REDACTED***',
text,
flags=re.IGNORECASE
)
return text
async def on_post_tool_use(
self, tool_name: str, tool_input: dict, tool_output: str
) -> str | None:
"""Log tool use with sanitization."""
safe_output = self._sanitize_output(tool_output)
# Log safe_output...
return None # Don't modify output
Verification: Run the command with --help flag to verify availability.
See modules/security-patterns.md for detailed security guidance.
Performance Guidelines
Performance Best Practices
- Non-Blocking: Use
async/awaitproperly, don't block the event loop - Timeout Handling: Hook timeout is 10 minutes (increased from 60s in 2.1.3). For most hooks, aim for < 30s; use extended time only for CI/CD integration, complex validation, or external API calls
- Efficient Logging: Batch writes, use async I/O
- Memory Management: Don't accumulate unbounded state
- Fail Fast: Quick validation, early returns, avoid expensive operations
Example: Efficient Hook
import asyncio
from claude_agent_sdk import AgentHooks
class EfficientHooks(AgentHooks):
def __init__(self):
self._log_queue = asyncio.Queue()
self._log_task = None
async def on_pre_tool_use(self, tool_name: str, tool_input: dict) -> dict | None:
# Quick validation only
if not self._is_valid_input(tool_input):
raise ValueError("Invalid input")
return None
async def on_post_tool_use(
self, tool_name: str, tool_input: dict, tool_output: str
) -> str | None:
# Queue log entry without blocking
await self._log_queue.put({
'tool': tool_name,
'timestamp': time.time()
})
return None
def _is_valid_input(self, tool_input: dict) -> bool:
"""Fast validation check."""
# Simple checks only, < 10ms
return len(str(tool_input)) < 1_000_000
Verification: Run the command with --help flag to verify availability.
See modules/performance-guidelines.md for detailed optimization techniques.
Scope Selection
Choose the right location for your hooks based on audience and purpose.
Important: Auto-Loading Behavior
hooks/hooks.jsonis automatically loaded when a plugin is enabled. Do NOT add"hooks": "./hooks/hooks.json"toplugin.json- this causes duplicate load errors. Only use thehooksfield for additional hook files beyond the standard location.
Decision Framework
**Verification:** Run the command with `--help` flag to verify availability.
Is this hook part of a plugin's core functionality?
├─ YES → Plugin hooks (hooks/hooks.json in plugin)
└─ NO ↓
Should all team members on this project have this hook?
├─ YES → Project hooks (.claude/settings.json)
└─ NO ↓
Should this hook apply to all my Claude sessions?
├─ YES → Global hooks (~/.claude/settings.json)
└─ NO → Reconsider if you need a hook at all
Verification: Run the command with --help flag to verify availability.
Scope Comparison
| Scope | Location | Audience | Committed? | Example Use Case |
|---|---|---|---|---|
| Plugin | hooks/hooks.json | Plugin users | Yes (with plugin) | YAML validation in YAML plugin |
| Project | .claude/settings.json | Team members | Yes (in repo) | Block production config edits |
| Global | ~/.claude/settings.json | Only you | Never | Personal audit logging |
See modules/scope-selection.md for detailed scope decision guidance.
Common Patterns
Validation Hook
Block dangerous operations before execution:
async def on_pre_tool_use(self, tool_name: str, tool_input: dict) -> dict | None:
if tool_name == "Bash":
command = tool_input.get("command", "")
# Block dangerous patterns
if any(pattern in command for pattern in ["rm -rf /", ":(){ :|:& };:"]):
raise ValueError(f"Dangerous command blocked: {command}")
# Block production access
if "production" in command and not self._has_approval():
raise ValueError("Production access requires approval")
return None
Verification: Run the command with --help flag to verify availability.
Logging Hook
Audit all tool operations:
async def on_post_tool_use(
self, tool_name: str, tool_input: dict, tool_output: str
) -> str | None:
await self._log_entry({
'timestamp': datetime.now().isoformat(),
'tool': tool_name,
'input_size': len(str(tool_input)),
'output_size': len(tool_output),
'success': True
})
return None
Verification: Run the command with --help flag to verify availability.
Context Injection Hook
Add relevant context before user prompts:
async def on_user_prompt_submit(self, message: str) -> str | None:
# Inject project-specific context
context = await self._load_project_context()
enhanced_message = f"{context}\n\n{message}"
return enhanced_message
Verification: Run the command with --help flag to verify availability.
Testing Hooks
Unit Testing
import pytest
from my_hooks import ValidationHooks
@pytest.mark.asyncio
async def test_dangerous_command_blocked():
hooks = ValidationHooks()
with pytest.raises(ValueError, match="Dangerous command"):
await hooks.on_pre_tool_use("Bash", {"command": "rm -rf /"})
@pytest.mark.asyncio
async def test_safe_command_allowed():
hooks = ValidationHooks()
result = await hooks.on_pre_tool_use("Bash", {"command": "ls -la"})
assert result is None # Allows execution
Verification: Run pytest -v from to verify.
See modules/testing-hooks.md for detailed testing strategies.
Module References
For detailed guidance on specific topics:
- Hook Types:
modules/hook-types.md- Detailed event signatures and parameters - SDK Callbacks:
modules/sdk-callbacks.md- Python SDK implementation patterns - Security Patterns:
modules/security-patterns.md- detailed security guidance - Performance Guidelines:
modules/performance-guidelines.md- Optimization techniques - Scope Selection:
modules/scope-selection.md- Choosing plugin/project/global - Testing Hooks:
modules/testing-hooks.md- Testing strategies and fixtures
Tools
- hook_validator.py: Validate hook structure and syntax (in
scripts/)
Related Skills
- hook-scope-guide: Decision framework for hook placement (existing)
- modular-skills: Design patterns for skill architecture
- skills-eval: Quality assessment and improvement framework
Next Steps
- Choose your hook type (JSON vs SDK) based on complexity needs
- Select the appropriate scope (plugin/project/global)
- Implement following security and performance best practices
- Test thoroughly with unit and integration tests
- Validate using
hook_validator.pybefore deployment
Environment Variables (Claude Code 2.1.2+)
FORCE_AUTOUPDATE_PLUGINS
Forces plugin auto-update even when the main Claude Code auto-updater is disabled.
Use cases:
- CI/CD pipelines that need latest plugin versions
- Development environments testing plugin updates
- Controlled update rollouts in enterprise settings
# Enable forced plugin updates
export FORCE_AUTOUPDATE_PLUGINS=1
claude
# Or inline
FORCE_AUTOUPDATE_PLUGINS=1 claude --agent my-agent
Note: This only affects plugin updates, not Claude Code core updates.
References
Troubleshooting
Common Issues
Hook not firing Verify hook pattern matches the event. Check hook logs for errors
Syntax errors Validate JSON/Python syntax before deployment
Permission denied Check hook file permissions and ownership
Repository
