Configuring Skill Hooks (PreToolUse, PostToolUse, Stop) for Lifecycle Automation in Claude Code
Claude Code skill hooks let you inject custom commands at three critical lifecycle points—before tools run (PreToolUse), after they complete (PostToolUse), and when the skill finishes (Stop)—by defining a hooks: object in your skill's front-matter that executes async scripts receiving JSON payloads via stdin.
The shanraisshan/claude-code-best-practice repository demonstrates how to automate skill workflows using lifecycle hooks defined in SKILL.md front-matter. By attaching PreToolUse, PostToolUse, and Stop hooks, you can log operations, validate arguments, trigger notifications, or clean up resources without blocking the primary skill execution.
Understanding the Three Core Skill Hooks
Claude Code emits hook events at specific execution phases. Each hook receives a JSON payload on stdin containing metadata like hook_event_name, session_id, and cwd.
PreToolUse Hook
The PreToolUse hook fires immediately before any tool (e.g., Read, Write, or Bash) is invoked. Use this to log impending operations, validate arguments against security policies, or block unauthorized tool calls.
PostToolUse Hook
The PostToolUse hook executes after a tool finishes successfully. This is ideal for emitting notifications, updating status files, or triggering downstream CI jobs when a specific file operation completes.
Stop Hook
The Stop hook runs when the skill finishes responding—after all tool calls have resolved. According to the source code in .claude/hooks/HOOKS-README.md, this is the deterministic point for clean-up resources, writing execution summaries, or firing final sound cues to signal completion.
How to Configure Skill Hooks in Front-Matter
Hooks are defined as a YAML list under the hooks: key in a skill's front-matter. The default handler type is command, which executes a shell command or script.
Complete Three-Hook Example
Save this to .claude/skills/code-review/SKILL.md:
---
name: code-review
description: Run an automated code-review checklist
hooks:
PreToolUse:
- type: command
command: python3 .claude/hooks/scripts/log-pre-tool.py
timeout: 5000
async: true
statusMessage: PreToolUse
PostToolUse:
- type: command
command: python3 .claude/hooks/scripts/log-post-tool.py
timeout: 5000
async: true
statusMessage: PostToolUse
Stop:
- type: command
command: python3 .claude/hooks/scripts/log-stop.py
timeout: 5000
async: true
statusMessage: ReviewComplete
---
# Code-Review Skill
...
Key configuration fields:
- type: Handler type (default is
command; can also behttp) - command: Executable path or shell command
- timeout: Milliseconds to wait before killing the hook (default 5000 ms)
- async: Boolean determining if the hook blocks skill execution (default true)
- statusMessage: Text displayed in Claude Code's UI during hook execution
Minimal Stop-Only Configuration
For simple clean-up tasks, define only the Stop hook:
---
name: summarize-tests
description: Summarize test results after running npm test
hooks:
Stop:
- type: command
command: ./scripts/write-test-summary.sh
timeout: 8000
async: true
statusMessage: Summarizing
---
Run `npm test` and let this skill produce a concise markdown report.
Setting async: true ensures the summary write does not block Claude Code from returning control to the user while the script runs in the background.
Global Hook Configuration and Local Overrides
The repository provides tiered configuration files to control hook behavior across teams and individual developers.
| File | Purpose | Scope |
|---|---|---|
.claude/hooks/config/hooks-config.json |
Team-wide defaults (e.g., disableStopHook, disableLogging) |
Shared repository settings |
.claude/hooks/config/hooks-config.local.json |
Personal overrides | Git-ignored, per-developer |
Global settings in hooks-config.json can disable specific hooks entirely (e.g., setting disableStopHook: true). The local JSON file supersedes the shared configuration, allowing individual developers to enable verbose logging or extend timeouts without affecting teammates.
Hook Execution Model and Payload Structure
When Claude Code loads a skill, it parses the front-matter and registers handlers for each defined event. During execution, the engine dispatches events to these handlers, which receive a JSON payload via stdin.
Python Hook Script Template
Create .claude/hooks/scripts/log-pre-tool.py:
#!/usr/bin/env python3
import json, sys, os
payload = json.load(sys.stdin)
hook = payload.get("hook_event_name")
session = payload.get("session_id")
cwd = payload.get("cwd")
# Example: write a tiny log file
log_path = os.path.join(
os.getenv("CLAUDE_PROJECT_DIR"),
".claude", "hooks", "logs",
f"{hook}.log"
)
with open(log_path, "a") as f:
f.write(f"{hook} fired in {cwd} (session {session})\n")
Make the script executable (chmod +x) and reference it in your skill's command field. The CLAUDE_PROJECT_DIR environment variable is automatically injected by the Claude Code runtime.
Bash Hook Alternative
For lightweight operations, use shell scripts:
#!/usr/bin/env bash
# .claude/hooks/scripts/agent-stop.sh
read -r payload
echo "Agent Stop hook received: $payload" >> "$CLAUDE_PROJECT_DIR/.claude/hooks/logs/agent-stop.log"
Reference this with command: bash .claude/hooks/scripts/agent-stop.sh in your front-matter.
Agent-Level vs. Skill-Level Hook Chains
Hooks can be defined at both the agent and skill level, creating a nested execution chain. The repository's .claude/agents/weather-agent.md demonstrates this pattern:
---
name: weather-agent
description: Orchestrates weather fetching and SVG creation
hooks:
PreToolUse:
- type: command
command: python3 .claude/hooks/scripts/agent-pretool.py
async: true
statusMessage: Agent PreToolUse
Stop:
- type: command
command: python3 .claude/hooks/scripts/agent-stop.py
async: true
statusMessage: Agent Done
skills:
- weather-fetcher # preloaded skill with its own Stop hook
---
Your agent instructions…
Execution order when the agent runs:
- Agent
PreToolUsefires before any tool inside the agent. - Skill
PreToolUsefires before the skill's first tool (if defined in the skill). - Tool execution occurs.
- Skill
PostToolUsefires after the tool completes. - Skill
Stopfires when the skill finishes. - Agent
Stopfires after the entire agent completes.
All hooks in the chain share the same JSON payload format and respect their individual timeout and async settings.
Advanced Hook Patterns
HTTP Webhooks for External Notifications
Beyond shell commands, hooks can POST to external services using the http type. Add this under Stop: to trigger CI pipelines or Slack notifications:
{
"type": "http",
"url": "https://example.com/webhook/skill-complete",
"timeout": 3000,
"allowedEnvVars": ["WEBHOOK_TOKEN"],
"headers": {
"Authorization": "Bearer $WEBHOOK_TOKEN"
}
}
The allowedEnvVars array explicitly permits specific environment variables to be injected into the header template, following the security model documented in .claude/hooks/HOOKS-README.md.
Structured Logging with All 19 Hook Events
While PreToolUse, PostToolUse, and Stop cover the primary lifecycle, the repository documents 19 total hook events in HOOKS-README.md. You can configure handlers for granular events like PreBash, PostRead, or OnError using the same schema, enabling comprehensive audit trails without modifying skill logic.
Summary
- Skill hooks (
PreToolUse,PostToolUse,Stop) provide deterministic automation points in Claude Code's execution lifecycle. - Configure hooks in
SKILL.mdfront-matter using thehooks:key withtype,command,timeout, andasyncfields. - Hook scripts receive JSON payloads via stdin containing
hook_event_name,session_id, andcwd. - Global defaults live in
.claude/hooks/config/hooks-config.json; developers override via.claude/hooks/config/hooks-config.local.json. - Agent-level hooks fire before skill-level hooks, creating nested lifecycle chains as shown in
weather-agent.md. - All hooks run asynchronously by default with a 5000 ms timeout to prevent blocking the primary workflow.
Frequently Asked Questions
How do I debug a hook that isn't firing?
First, verify the hook is enabled in .claude/hooks/config/hooks-config.json (check for disableStopHook or similar flags set to false). Ensure your script is executable (chmod +x) and that the command path is relative to the project root. Check .claude/hooks/logs/ for output, or add 2>&1 to your command to capture stderr. Finally, confirm your SKILL.md front-matter syntax is valid YAML—missing indentation will prevent hook registration.
Can I use hooks to cancel or block a tool operation?
Yes. The PreToolUse hook can inspect the incoming tool call via the JSON payload and exit with a non-zero status code to block the operation. Because hooks run as separate processes, you can implement complex validation logic in Python or Bash that validates file paths or arguments against security policies before allowing the tool to execute.
What is the difference between async: true and async: false?
When async: true (the default), the hook runs in parallel with Claude Code's main execution thread, and the skill continues regardless of hook completion. This is ideal for logging or notifications. When async: false, the skill pauses until the hook exits or hits the timeout, which is necessary if subsequent tool calls depend on the hook's side effects (e.g., generating a required config file).
Do environment variables persist across all hook types?
Yes. The Claude Code runtime injects standard variables like CLAUDE_PROJECT_DIR into all hook subprocesses. For HTTP hooks, you must explicitly declare which environment variables are accessible via the allowedEnvVars array to prevent accidental credential leakage in webhook headers.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →