AI NewsletterSubscribe →
Resource HubComponents

Claude Code Hooks

Hooks are shell scripts Claude Code runs automatically on session events — PreToolUse, PostToolUse, PreCompact. Use them for logging, validation, and automated checkpointing.

Larry Maguire

Larry Maguire

GenAI Skills Academy

Hooks are automatic reactions to events in a Claude Code session. When something happens -- Claude is about to run a command, a file is written, a session starts -- a hook can run a script in response. Think of them as "if this, then that" rules for your workspace. Claude doesn't control hooks and isn't aware they're running. They operate at the system level, outside the conversation.

That separation is what makes hooks useful for infrastructure tasks: audit logging, safety guardrails, automatic backups, and session continuity. A skill is something you invoke intentionally. A hook is something that happens regardless of what you or Claude are doing.

Hooks vs skills

Skills are invoked intentionally by you or Claude. Hooks are triggered automatically by the system -- Claude doesn't know they're running. Use skills for deliberate workflows; use hooks for infrastructure behaviour that should always happen regardless of what Claude is doing.

All lifecycle events

Event When it fires Blocking? Common use
SessionStartSession opens (once)NoLog session start, load env state
SessionEndSession closes (once)NoFinal cleanup, archive session log
InstructionsLoadedCLAUDE.md and rules loaded (once)NoValidate config, inject env-specific context
ToolUseStartBefore each tool callYesValidate paths, block restricted operations
ToolUseEndAfter each tool call completesYesLog file writes, trigger downstream actions
PreCompactBefore context compressionNoWrite checkpoint -- preserve where you are
WorktreeCreateGit worktree created for an agentNoLog agent isolation, copy env files
WorktreeRemoveGit worktree removedNoClean up temp files, notify on completion
LoopStartA /loop skill beginsNoLog recurring task start
LoopIterationEach iteration of a loopNoMonitor, alert on anomalies
LoopEndLoop completes or is stoppedNoSend summary notification

Blocking vs non-blocking

Blocking hooks (ToolUseStart, ToolUseEnd) make Claude Code wait for the hook to complete before proceeding. They can return a JSON decision that allows or blocks the operation. All other events are non-blocking -- the session continues while the hook runs asynchronously.

A blocking hook returns JSON on stdout:

# Allow the operation
{"decision": "allow"}

# Block the operation with a reason
{"decision": "block", "reason": "Write to /etc is not permitted"}

# Override tool output (PostToolUse only)
{"decision": "allow", "output": "patched tool result"}

Exit code 0 = hook succeeded. Any non-zero exit code = hook failed. For blocking hooks, failure causes Claude Code to block the operation and report the error to the session.

What shell scripting means for hooks

Most hooks are written as shell scripts -- small text files that run commands on your computer, one line at a time. Shell scripts are the standard way to automate system-level tasks on macOS and Linux. You do not need to know how to write them from scratch: you can describe what you want a hook to do and ask Claude to write the script for you. The scripts that ship with the AI Business OS were written this way.

If you look inside a hook script and see something that looks unfamiliar, the most common elements are: reading input with cat (which collects the event data Claude sends), writing output with echo (which sends a response back), and checking conditions with if statements (which decide whether to allow or block something). Everything else is variations on those three ideas.

Hook input -- JSON on stdin

Every hook receives a JSON object on stdin describing the event. For tool hooks, this includes the tool name, inputs, and result (PostToolUse):

{
  "event": "ToolUseStart",
  "tool": "Bash",
  "input": {
    "command": "rm -rf /tmp/old-files"
  },
  "session_id": "sess_abc123"
}

Your hook script reads this with cat or jq to extract what it needs. jq is a small tool for reading structured data files -- it lets you pull out a specific value (like the tool name, or the file path being written) from the JSON that Claude sends to the hook:

#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool')
COMMAND=$(echo "$INPUT" | jq -r '.input.command // empty')

# Block rm -rf anywhere
if [[ "$COMMAND" == *"rm -rf"* ]]; then
  echo '{"decision":"block","reason":"rm -rf requires explicit confirmation"}'
  exit 0
fi
echo '{"decision":"allow"}'

Configuration

Hooks are registered in .claude/settings.json under the hooks key. Each entry specifies the event, an optional matcher, and the handler.

{
  "hooks": {
    "PreCompact": [
      {
        "command": ".claude/hooks/precompact-checkpoint.sh"
      }
    ],
    "ToolUseEnd": [
      {
        "matcher": "Write",
        "command": ".claude/hooks/log-file-writes.sh"
      }
    ],
    "ToolUseStart": [
      {
        "matcher": "Bash",
        "command": ".claude/hooks/validate-bash.sh"
      }
    ]
  }
}

The matcher field filters which tool calls trigger the hook. It supports exact tool names ("Write"), glob patterns ("Write(./.env*)"), and regex patterns. Omit the matcher to fire on every tool call for that event.

Handler types

  • Shell scripts -- most common; any executable bash/zsh script, receives JSON on stdin
  • HTTP endpoints -- Claude Code sends a POST request with the event JSON to a local or remote URL
  • LLM handlers -- Claude Code makes a model call to evaluate the event, asking something like "should this bash command be allowed?" and using the AI's response as the decision. Slower than a script, but capable of nuanced judgement that a simple if-statement can't make -- for example, deciding whether a file path looks suspicious rather than matching against a fixed list.

Example: checkpoint before compaction

This hook appends a timestamped breadcrumb to a checkpoint file whenever the context window is about to be compressed -- marking the exact point where session memory may be lost:

#!/bin/bash
# .claude/hooks/precompact-checkpoint.sh
CHECKPOINT_DIR="$HOME/Documents/Agent Outputs/general"
mkdir -p "$CHECKPOINT_DIR"
FILE="$CHECKPOINT_DIR/$(date +%Y-%m-%d)-session-checkpoint.md"
{
  echo ""
  echo "---"
  echo "## Compaction Event — $(date +%H:%M)"
  echo "**Context compacted.** Resume from this point forward."
} >> "$FILE"

Example: block writes to sensitive paths

#!/bin/bash
# .claude/hooks/protect-sensitive-paths.sh
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool')
if [[ "$TOOL" == "Write" ]]; then
  PATH_ARG=$(echo "$INPUT" | jq -r '.input.file_path // empty')
  if [[ "$PATH_ARG" == *"/.env"* || "$PATH_ARG" == *"/secrets"* ]]; then
    echo '{"decision":"block","reason":"Direct writes to .env and secrets paths are blocked"}'
    exit 0
  fi
fi
echo '{"decision":"allow"}'

Testing hooks

Before relying on a hook in production, verify it works as expected. The simplest approach is to trigger the event manually and check the output.

  1. Run the script directly in terminal. Pass a sample JSON payload via echo: echo '{"event":"ToolUseStart","tool":"Bash","input":{"command":"rm -rf /tmp"}}' | bash .claude/hooks/validate-bash.sh. The script should print its JSON decision to the terminal.
  2. Check exit codes. A decision of "allow" with exit code 0 means the hook passed. A "block" decision with exit code 0 means it blocked correctly. A non-zero exit code means the script itself errored.
  3. Test in a live session. Perform the action the hook is watching for and confirm the expected behaviour. For a logging hook, verify the log file was written. For a blocking hook, confirm the operation was stopped.
  4. Check the log. Claude Code logs hook execution. On macOS: ~/Library/Logs/Claude/. If a hook isn't firing, this is the first place to look.

Common hooks for business use

You do not need to build hooks from scratch. These patterns cover the most common business uses -- ask Claude to write the script for any of them.

  • Change log. A ToolUseEnd hook on the Write tool that appends every file write to a daily log file. Useful for reviewing what changed in a session, or recovering from an unintended edit.
  • Completion notification. A SessionEnd or LoopEnd hook that sends a desktop notification (macOS: osascript -e 'display notification...') when a long-running task finishes.
  • Auto-backup. A ToolUseEnd hook on the Edit tool that runs a git commit after each file is modified, creating a granular history you can roll back through.
  • Path protection. A ToolUseStart hook on the Bash tool that blocks commands matching patterns you want to require confirmation for -- rm -rf, writes to system directories, or commands that touch client folders you've marked as protected.

Good uses for hooks

  • Session checkpointing before context compression (PreCompact)
  • Audit log of all file writes for recovery or review (ToolUseEnd + Write matcher)
  • Block writes to .env, system directories, or out-of-scope paths (ToolUseStart)
  • Send a notification when a long-running task finishes (SessionEnd or LoopEnd)
  • Auto-commit after a successful code change (ToolUseEnd + Edit matcher)
  • Copy .env into a new agent worktree on creation (WorktreeCreate)

GenAI Skills Academy

Achieve Productivity Gains With AI Today

Send me your details and let’s book a 15 min no-obligation call to discuss your needs and concerns around AI.