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 be http)
  • 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:

  1. Agent PreToolUse fires before any tool inside the agent.
  2. Skill PreToolUse fires before the skill's first tool (if defined in the skill).
  3. Tool execution occurs.
  4. Skill PostToolUse fires after the tool completes.
  5. Skill Stop fires when the skill finishes.
  6. Agent Stop fires 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.md front-matter using the hooks: key with type, command, timeout, and async fields.
  • Hook scripts receive JSON payloads via stdin containing hook_event_name, session_id, and cwd.
  • 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →