From Vengineer
Expert guidance for creating Claude Code hooks. Use when configuring hooks in settings files, creating hook scripts, or implementing hook-based workflows for tool interception, validation, or automation.
npx claudepluginhub dylanliiii/shipyard --plugin VengineerThis skill uses the workspace's default tool permissions.
This skill provides comprehensive guidance for creating and managing Claude Code hooks—powerful automation tools that intercept and respond to Claude Code events.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Automates semantic versioning and release workflow for Claude Code plugins: bumps versions in package.json, marketplace.json, plugin.json; verifies builds; creates git tags, GitHub releases, changelogs.
This skill provides comprehensive guidance for creating and managing Claude Code hooks—powerful automation tools that intercept and respond to Claude Code events.
Create a simple PreToolUse hook that validates Bash commands:
# ~/.claude/settings.json or .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-command.sh",
"timeout": 30
}
]
}
]
}
}
#!/bin/bash
# .claude/hooks/validate-command.sh
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
# Block destructive commands
if [[ "$command" =~ ^rm\ -rf ]]; then
echo "Destructive command blocked" >&2
exit 2
fi
exit 0
Hooks are event-driven automation points that intercept Claude Code execution at specific moments. They receive JSON input via stdin and communicate back through exit codes and stdout.
Command hooks (type: "command"): Execute bash scripts
Prompt hooks (type: "prompt"): Use LLM for intelligent decisions
Runs after tool parameters are created but before execution.
Common matchers: Bash, Write, Edit, Read, Task, Glob, Grep
Use cases:
Decision control:
allow: Bypass permission systemdeny: Prevent tool executionask: Show user confirmation dialogRuns when user is shown a permission dialog.
Use cases:
Runs immediately after successful tool completion.
Use cases:
Runs when user submits a prompt, before Claude processes it.
Use cases:
Runs when Claude or a subagent finishes responding.
Use cases:
Runs when Claude starts or resumes a session.
Matchers: startup, resume, clear, compact
Use cases:
$CLAUDE_ENV_FILERuns when a Claude Code session ends.
Use cases:
Runs when Claude Code sends notifications.
Matchers: permission_prompt, idle_prompt, auth_success, elicitation_dialog
Use cases:
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern|*",
"hooks": [
{
"type": "command|prompt",
"command": "bash-command",
"prompt": "llm-prompt",
"timeout": 60
}
]
}
]
}
}
Hooks are configured in settings files (priority order):
~/.claude/settings.json - User settings.claude/settings.json - Project settings.claude/settings.local.json - Local project (not committed)Write matches only Write toolEdit|Write matches Edit or Write.* or * matches all toolsAvailable in all hook commands:
$CLAUDE_PROJECT_DIR: Project root directory$CLAUDE_PLUGIN_ROOT: Plugin directory (plugin hooks only)$CLAUDE_CODE_REMOTE: "true" in remote web environment (unset locally)$CLAUDE_ENV_FILE: Persist environment vars (SessionStart only)Hooks receive JSON via stdin:
{
"session_id": "abc123",
"transcript_path": "/path/to/session.jsonl",
"cwd": "/current/working/directory",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "echo 'hello'"
},
"tool_use_id": "toolu_01ABC123..."
}
Return structured JSON in stdout for advanced control:
{
"continue": true,
"stopReason": "Message shown when stopping",
"suppressOutput": true,
"systemMessage": "Warning to user"
}
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "Explanation",
"updatedInput": {
"field_to_modify": "new value"
}
}
}
{
"decision": "block",
"reason": "Explanation",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Extra information for Claude"
}
}
{
"decision": "block",
"reason": "Why Claude should continue (required when blocking)"
}
Validate bash commands before execution:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-bash.sh",
"timeout": 30
}
]
}
]
}
}
#!/bin/bash
# .claude/hooks/validate-bash.sh
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
# Enforce better tools
if echo "$command" | grep -qE '\bgrep\b(?!.*\|)'; then
echo "Use 'rg' (ripgrep) instead of 'grep'" >&2
exit 2
fi
if echo "$command" | grep -qE '\bfind\s+\S+\s+-name\b'; then
echo "Use 'rg --files' instead of 'find -name'" >&2
exit 2
fi
exit 0
Auto-approve documentation file reads:
{
"hooks": {
"PermissionRequest": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-approve-docs.sh"
}
]
}
]
}
}
#!/usr/bin/env python3
# .claude/hooks/auto-approve-docs.sh
import json
import sys
input_data = json.load(sys.stdin)
file_path = input_data.get("tool_input", {}).get("file_path", "")
# Auto-approve documentation files
if file_path.endswith((".md", ".mdx", ".txt", ".json")):
output = {
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow"
}
},
"suppressOutput": True
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)
Load recent changes on session start:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/load-context.sh"
}
]
}
]
}
}
#!/bin/bash
# .claude/hooks/load-context.sh
cd "$CLAUDE_PROJECT_DIR" || exit 0
# Get recent commits (last 5)
recent_changes=$(git log -5 --oneline --pretty=format:"- %s" 2>/dev/null)
# Get current branch
current_branch=$(git branch --show-current 2>/dev/null)
if [ -n "$recent_changes" ] || [ -n "$current_branch" ]; then
echo "# Repository Context"
echo ""
echo "**Current branch:** $current_branch"
echo ""
echo "**Recent changes:**"
echo "$recent_changes"
fi
exit 0
Use LLM to intelligently decide if Claude should stop:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Analyze the conversation context: $ARGUMENTS\n\nDetermine if:\n1. All requested tasks are complete\n2. All tests pass\n3. No errors remain unaddressed\n\nRespond with JSON: {\"decision\": \"approve\"|\"block\", \"reason\": \"explanation\"}",
"timeout": 30
}
]
}
]
}
}
Target specific MCP tools:
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__memory__.*",
"hooks": [
{
"type": "command",
"command": "echo 'Memory operation' >> ~/hooks.log"
}
]
},
{
"matcher": "mcp__.*__write.*",
"hooks": [
{
"type": "command",
"command": "/path/to/validate-mcp-write.sh"
}
]
}
]
}
}
USE AT YOUR OWN RISK: Hooks execute arbitrary shell commands automatically. You are solely responsible for hook commands. Malicious or poorly written hooks can cause data loss or system damage.
"$VAR" not $VAR.. in file paths$CLAUDE_PROJECT_DIR.env, .git/, keysDirect edits to hooks don't take effect immediately. Claude Code:
/hooks menu for changes/hooks to verify hook registrationchmod +x)claude --debug for execution details\" inside JSON stringsRun claude --debug to see detailed hook execution:
[DEBUG] Executing hooks for PostToolUse:Write
[DEBUG] Found 1 hook matchers in settings
[DEBUG] Matched 1 hooks for query "Write"
[DEBUG] Executing hook command: <command> with timeout 60000ms
[DEBUG] Hook command completed with status 0
Plugins can provide hooks that integrate with user/project hooks:
{
"description": "Automatic code formatting",
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
"timeout": 30
}
]
}
]
}
}
Place in plugins/your-plugin/hooks/hooks.json or specify custom path in plugin metadata.
$CLAUDE_PROJECT_DIR for portabilitySee examples.md for complete working examples of common hook patterns.
For complete API details, see: