Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
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; inject context (2.1.9+)
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
TeammateIdle/TaskCompleted: Multi-agent coordination and orchestration (2.1.33+)
PreCompact: State preservation before context window compaction
New in 2.1.9: PreToolUse hooks can now return additionalContext to inject information before a tool executes. This enables patterns like cache hints, security warnings, or relevant context injection.
Quick Start
Your First Hook (JSON - Claude Code)
Create a simple logging hook in .claude/settings.json:
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-skilldescription:Skillwithlifecyclehookshooks: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 callSessionStart:-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:
The hook POSTs the standard hook input as JSON and expects a standard hook response JSON body.
When to use HTTP hooks over command hooks:
Enterprise environments where shell execution is restricted
Centralized hook logic shared across teams via a web service
Sandboxed or containerized setups without local script access
Integration with external validation/logging services
Pros: No local scripts needed, centralized logic, works in sandboxed environments
Cons: Network latency, requires running HTTP service, external dependency
Python SDK Hooks
Programmatic callbacks using AgentHooks base class:
Verification: Run the command with --help flag to verify availability.
Pros: Full Python capabilities, complex logic, state management
Cons: Requires Python, more complex setup
Bash Permission Matching Notes
Environment Variable Wrappers (2.1.38+)
Permission rules now correctly match commands prefixed with environment variable assignments. Before 2.1.38, NODE_ENV=production npm test would not match a rule for Bash(npm *).
# These now all match `Bash(npm *)`:
npm test
NODE_ENV=production npm test
FORCE_COLOR=1 CI=true npm test
When writing PreToolUse hooks that inspect bash commands, be aware that the permission system strips env var prefixes for matching, but your hook receives the full command string including prefixes.
Heredoc Delimiter Security (2.1.38+)
Claude Code now validates heredoc delimiters to prevent command smuggling. The recommended pattern <<'EOF' (single-quoted) remains the safest approach. Always use single-quoted delimiters in heredoc patterns to prevent variable expansion.
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. Note: .claude/skills/ is read-only in sandbox mode (2.1.38+)
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
classSecureLoggingHooks(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 inself.SECRET_PATTERNS:
text = re.sub(
rf'({pattern}["\s:=]+)([^\s,}}]+)',
r'\1***REDACTED***',
text,
flags=re.IGNORECASE
)
return text
asyncdefon_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...returnNone# Don't modify output
Verification: Run the command with --help flag to verify availability.
See modules/testing-hooks.md for detailed security guidance.
Performance Guidelines
Performance Best Practices
Non-Blocking: Use async/await properly, 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
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.json is automatically loaded when a plugin is enabled.
Do NOT add "hooks": "./hooks/hooks.json" to plugin.json - this causes duplicate load errors.
Only use the hooks field 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:
asyncdefon_pre_tool_use(self, tool_name: str, tool_input: dict) -> dict | None:
if tool_name == "Bash":
command = tool_input.get("command", "")
# Block dangerous patternsifany(pattern in command for pattern in ["rm -rf /", ":(){ :|:& };:"]):
raise ValueError(f"Dangerous command blocked: {command}")
# Block production accessif"production"in command andnotself._has_approval():
raise ValueError("Production access requires approval")
returnNone
Verification: Run the command with --help flag to verify availability.
Inject context before a tool executes using additionalContext:
#!/usr/bin/env python3"""PreToolUse hook that injects context before WebFetch."""import json
import sys
defmain():
payload = json.load(sys.stdin)
tool_name = payload.get("tool_name", "")
if tool_name == "WebFetch":
url = payload.get("tool_input", {}).get("url", "")
# Check cache or knowledge base
cached = lookup_knowledge_base(url)
if cached:
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": f"Relevant cached info: {cached}"
}
}))
sys.exit(0)
if __name__ == "__main__":
main()
This pattern is useful for: cache hints before web requests, security warnings before risky operations, and injecting relevant project context before file operations.
Testing Hooks
Unit Testing
import pytest
from my_hooks import ValidationHooks
@pytest.mark.asyncioasyncdeftest_dangerous_command_blocked():
hooks = ValidationHooks()
with pytest.raises(ValueError, match="Dangerous command"):
await hooks.on_pre_tool_use("Bash", {"command": "rm -rf /"})
@pytest.mark.asyncioasyncdeftest_safe_command_allowed():
hooks = ValidationHooks()
result = await hooks.on_pre_tool_use("Bash", {"command": "ls -la"})
assert result isNone# 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
Hooks communicate decisions to Claude Code via exit codes:
Exit Code
Meaning
stdout
stderr
0
Success/allow
Shown to Claude as system context
Ignored
2
Block/deny
Ignored
Shown to user as explanation (2.1.39+ fix)
Other
Error
Ignored
Shown to user as error message
Blocking with Exit Code 2 (2.1.39+)
Use exit code 2 to block an action and display a message to the user:
#!/bin/bash# Example: Block force pushes with user-facing messagecommand=$(echo"$1" | jq -r '.tool_input.command // empty')
ifecho"$command" | grep -q 'push.*--force'; thenecho"Force push blocked: use --force-with-lease instead" >&2
exit 2
fiexit 0
Important: Before Claude Code 2.1.39, stderr from exit code 2 was silently swallowed (#10964). Users would see a generic "hook error" instead of the custom message. This is now fixed: stderr is properly displayed to the user.
Plugin hooks: Before 2.1.39, plugin-installed hooks had a separate code path that also failed to show stderr for exit code 2 (#10412). Both plugin and project hooks now work correctly.
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
Hook blocking message not shown (pre-2.1.39)
If using exit code 2 to block with a user-facing message and the message isn't appearing, upgrade to Claude Code 2.1.39+. In older versions, use exit 0 with stdout as a workaround.
Exit Criteria
The authored hook file exists at a valid scope location (hooks/hooks.json,
.claude/settings.json, or ~/.claude/settings.json) with correct JSON or Python syntax.
The hook fires on the target event: a test invocation of the matching tool call triggers
the hook command or callback without error.
The hook contains no secret logging: no field names matching api[_-]?key, password,
token, secret, or credential appear in log output paths.
Blocking hooks exit with code 2 and write the user-facing explanation to stderr (not stdout).
If abstract:validate-hook is available, it exits 0 on the authored hook file.