CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

creating-hooks

Creates Claude Code hooks.

Quality

71%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

Fix and improve this skill with Tessl

tessl review fix ./.agents/skills/creating-hooks/SKILL.md
SKILL.md
Quality
Evals
Security

STARTER_CHARACTER = 🪝

Setup

First, update the reference docs to get the latest from Anthropic:

python ~/.claude/skills/creating-hooks/scripts/update-docs.py

What Hooks Are

Shell commands that execute at lifecycle points in Claude Code. Unlike prompts, hooks are deterministic—they always run when triggered.

Configuration

Hooks live in settings files:

  • ~/.claude/settings.json - User settings (all projects)
  • .claude/settings.json - Project settings (shared via git)
  • .claude/settings.local.json - Local project settings (not committed)
{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolPattern",
        "hooks": [
          {
            "type": "command",
            "command": "your-command-here"
          }
        ]
      }
    ]
  }
}

Matcher: Pattern to match tool names (case-sensitive)

  • Exact match: Write
  • Regex: Edit|Write
  • All tools: * or omit

Environment variables:

  • $CLAUDE_PROJECT_DIR - Absolute path to project root
  • $CLAUDE_ENV_FILE - File path for persisting env vars (SessionStart only)

Hook Events

Tool events (matcher applies):

  • PreToolUse - Before tool executes
  • PostToolUse - After tool completes
  • PermissionRequest - Permission dialog shown

Session events:

  • SessionStart - Session begins/resumes (matcher: startup/resume/clear/compact)
  • SessionEnd - Session ends
  • PreCompact - Before compaction (matcher: manual/auto)

Other events:

  • UserPromptSubmit - User submits prompt
  • Stop - Agent finishes
  • SubagentStop - Subagent finishes
  • Notification - Alerts sent (matcher: notification type)

Exit Codes

  • 0: Success. stdout shown in verbose mode. For UserPromptSubmit/SessionStart, stdout added to context.
  • 2: Block. stderr fed to Claude as error message. Blocks the action.
  • Other: Non-blocking error. stderr shown to user.

JSON Output

For advanced control, return JSON to stdout with exit code 0:

{
  "continue": false,
  "stopReason": "Message shown when stopping"
}

PreToolUse Control

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Auto-approved",
    "updatedInput": { "field": "modified value" }
  }
}

Decisions: "allow" (bypass permission), "deny" (block), "ask" (prompt user)

PostToolUse Feedback

{
  "decision": "block",
  "reason": "Explanation fed to Claude"
}

Stop/SubagentStop Control

{
  "decision": "block",
  "reason": "Must fix X before stopping"
}

Hook Input

Hooks receive JSON via stdin:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/dir",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": { "file_path": "/path", "content": "..." }
}

Common Patterns

Auto-format after edit:

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.file_path' | xargs -I{} sh -c 'echo {} | grep -q \"\\.ts$\" && npx prettier --write {}'"
      }]
    }]
  }
}

Block dangerous commands:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-bash.py"
      }]
    }]
  }
}

Inject context on prompt:

{
  "hooks": {
    "UserPromptSubmit": [{
      "hooks": [{
        "type": "command",
        "command": "echo '[REMINDER: Follow TDD]'"
      }]
    }]
  }
}

Desktop notification:

{
  "hooks": {
    "Notification": [{
      "hooks": [{
        "type": "command",
        "command": "osascript -e 'display notification \"Claude needs input\" with title \"Claude Code\"'"
      }]
    }]
  }
}

Hook Scripts

For complex logic, use external scripts. UV single-file format works well:

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = []
# requires-python = ">=3.11"
# ///

import json
import sys

data = json.load(sys.stdin)
tool_input = data.get("tool_input", {})

# Validation logic here

if should_block:
    print("Error message", file=sys.stderr)
    sys.exit(2)

sys.exit(0)

Anti-Patterns

  • Using exit code 2 without stderr message (Claude gets no feedback)
  • Forgetting to handle JSON parsing errors in scripts
  • Blocking without explaining why (Claude will retry the same thing)
  • Long-running hooks without timeout (default is 60s)
  • Modifying files in PreToolUse (use PostToolUse for modifications)

Reference

  • references/anthropic-hooks.md - Complete reference (input schemas, prompt hooks, MCP tools)
  • references/anthropic-hooks-guide.md - Quickstart and examples
Repository

saski/arnesto

Path

.agents/skills/creating-hooks/SKILL.md

Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.