Close-up of metal gears and a handle, representing automation mechanisms

Claude Code Hooks: Automate Your AI Coding Workflow

Claude Code hooks run shell commands automatically at specific points in the agent's lifecycle — auto-formatting files, blocking dangerous commands, sending notifications, and enforcing project rules without manual intervention.

11 March 20266 min readBy LibraBit Team

Every time Claude Code edits a file, you run Prettier. Every time it finishes a task, you glance at the terminal to see if it needs input. Every time it writes a Bash command, you scan it for anything destructive before approving.

These are all manual steps that should not be manual. Claude Code hooks exist to automate exactly this kind of repetitive, deterministic work — running shell commands at specific points in the agent's lifecycle without you lifting a finger. If you are using Claude Code without hooks, you are doing work the machine should be doing for you.

What Hooks Are

Hooks are user-defined shell commands that execute automatically when specific events occur during a Claude Code session. They are not prompts or suggestions — they are deterministic. When the event fires, the command runs. Every time, without exception.

Think of them like Git hooks or CI pipeline steps. You define a trigger (an event), an optional filter (a matcher), and an action (a shell command). Claude Code handles the rest.

The system supports 14 lifecycle events, from session startup to session end. The most useful ones for day-to-day development are:

  • PreToolUse — fires before a tool call executes. Can block it.
  • PostToolUse — fires after a tool call succeeds.
  • Notification — fires when Claude needs your attention.
  • Stop — fires when Claude finishes responding.
  • SessionStart — fires when a session begins, resumes, or clears.

Each hook receives JSON context about the event on stdin — the tool name, the command being run, the file being edited — and communicates back through exit codes and stdout. Exit 0 means proceed. Exit 2 means block the action. Print JSON for finer-grained control.

Setting Up Your First Hook

The quickest way to create a hook is through the interactive menu. Type /hooks in the Claude Code CLI and you will see every available event listed. Select one, configure a matcher, and add your command.

But the more maintainable approach is to define hooks in your settings JSON directly. Hooks can live in three places:

LocationScopeShareable
~/.claude/settings.jsonAll your projectsNo — local to your machine
.claude/settings.jsonSingle projectYes — commit to the repo
.claude/settings.local.jsonSingle projectNo — automatically gitignored

For hooks that apply everywhere (like notifications), use your user settings. For project-specific hooks (like formatters or linters), use the project settings so your whole team benefits.

Here is the basic structure:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "your-command-here"
          }
        ]
      }
    ]
  }
}

The matcher is a regex that filters when the hook fires. "Edit|Write" matches either tool. "Bash" matches only Bash commands. Omit the matcher entirely to fire on every occurrence of the event.

Five Hooks Worth Setting Up Today

1. Desktop Notifications

This is the single most useful hook, and it takes thirty seconds to configure. Instead of watching your terminal for permission prompts or idle messages, get a native desktop notification whenever Claude needs your attention.

macOS:

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

Linux:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
          }
        ]
      }
    ]
  }
}

Add this to ~/.claude/settings.json so it applies to every project. You will never miss a permission prompt again.

2. Auto-Format on Every Edit

Claude writes functional code, but it does not always match your project's formatting conventions. This hook runs Prettier on every file Claude edits or creates:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

The hook reads the file path from the JSON input using jq, then passes it to Prettier. Add this to .claude/settings.json in your project root. Your team's code style stays consistent regardless of which developer — human or AI — writes the code.

You can substitute any formatter: black for Python, gofmt for Go, rubocop -A for Ruby. The pattern is the same.

3. Block Dangerous Commands

Claude is generally cautious, but a PreToolUse hook gives you a hard guarantee. This script blocks any Bash command containing rm -rf:

#!/bin/bash
# .claude/hooks/block-destructive.sh
COMMAND=$(jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked by hook"
    }
  }'
else
  exit 0
fi

Register it in your project settings:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-destructive.sh"
          }
        ]
      }
    ]
  }
}

When Claude tries to run rm -rf /tmp/build, the hook intercepts it, returns a deny decision, and Claude receives the reason as feedback. It adapts without you needing to intervene.

You can extend this pattern to block access to production databases, prevent modifications to .env files, or enforce any other safety rule your team needs.

4. Protect Sensitive Files

Similar to blocking commands, you can prevent Claude from editing files that should not be touched:

#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0

Register this as a PreToolUse hook with the matcher "Edit|Write". Claude will be told why the edit was blocked and can adjust its approach — perhaps suggesting the change for you to make manually instead.

5. Log Every Command

For audit trails or simply understanding what Claude did during a session, log every Bash command to a file:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
          }
        ]
      }
    ]
  }
}

This is particularly useful when onboarding new team members to AI-assisted development. They can review what Claude actually ran, building confidence in the tool and catching any unexpected patterns early.

Advanced Patterns

Re-inject Context After Compaction

When Claude's context window fills up and compaction occurs, important details can be lost. A SessionStart hook with a compact matcher re-injects critical information every time this happens:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Reminder: use pnpm, not npm. Run pnpm test before committing. Current sprint: auth refactor.'"
          }
        ]
      }
    ]
  }
}

Any text your command writes to stdout is added to Claude's context. You could replace the echo with a script that runs git log --oneline -5 to show recent commits, or reads a status file maintained by your CI pipeline.

Async Hooks for Long-Running Tasks

Some hooks take time — running a full test suite, for instance. By default, hooks block Claude's execution until they complete. Setting "async": true runs the hook in the background so Claude keeps working:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests-async.sh",
            "async": true,
            "timeout": 300
          }
        ]
      }
    ]
  }
}

When the tests finish, the results are delivered to Claude on the next conversation turn. If tests fail, Claude sees the failure output and can act on it. If they pass, Claude gets a confirmation message. All without blocking the current task.

Prompt and Agent Hooks

Not every decision can be reduced to a regex match or a shell script. For cases requiring judgement, Claude Code supports two additional hook types.

Prompt hooks ("type": "prompt") send the hook input to a fast Claude model for single-turn evaluation. The model returns a yes/no decision. This is useful for checks that need natural language understanding — like verifying that all requested tasks are complete before allowing Claude to stop:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Check if all tasks mentioned in the conversation are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
          }
        ]
      }
    ]
  }
}

Agent hooks ("type": "agent") go further, spawning a subagent that can read files, search code, and use other tools to verify conditions. Use these when the check requires inspecting the actual codebase — like confirming that tests pass before a task is marked complete.

Sharing Hooks With Your Team

Because hooks defined in .claude/settings.json can be committed to your repository, they become shared team infrastructure. This is where hooks deliver the most value at scale.

Consider establishing a standard set of project hooks that every developer gets automatically:

  • Auto-formatting on edit (consistent code style)
  • Protected file patterns (no accidental .env modifications)
  • Command logging (audit trail for AI-assisted changes)
  • Post-compaction context injection (project conventions survive context resets)

New team members who clone the repo immediately get these guardrails. No setup required, no configuration to remember.

For hooks you want across all projects but that are personal (like notification preferences), use ~/.claude/settings.json. For hooks that are project-specific but should not be shared (like paths to local tools), use .claude/settings.local.json.

Debugging Hooks

When a hook is not behaving as expected, start with these steps:

Check the /hooks menu. Type /hooks in the CLI to confirm your hook appears under the correct event.

Toggle verbose mode. Press Ctrl+O during a session to see hook output in the transcript, including exit codes and any stderr messages.

Run with --debug. Start Claude Code with claude --debug for full execution details — which hooks matched, their exit codes, and output.

Test manually. Pipe sample JSON to your script and check the exit code:

echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $?

The most common issue is shell profiles that print text on startup (like welcome messages in .zshrc). This output gets prepended to your hook's JSON and breaks parsing. Wrap any echo statements in your shell profile with an interactive check:

if [[ $- == *i* ]]; then
  echo "Shell ready"
fi

Key Takeaways

  • Hooks run deterministically. Unlike prompts, they are not suggestions — they execute every time the event fires. Use them for anything that must always happen.
  • Start with notifications. The highest-impact, lowest-effort hook is a desktop notification for the Notification event. Set it up in your user settings and never miss a prompt again.
  • Auto-format everything. A PostToolUse hook on Edit|Write that runs your formatter keeps code style consistent without thinking about it.
  • Protect what matters. PreToolUse hooks can block destructive commands, prevent edits to sensitive files, and enforce safety rules with hard guarantees.
  • Share via version control. Define hooks in .claude/settings.json and commit them. Your entire team gets the same guardrails automatically.
  • Use async for slow tasks. Set "async": true on hooks that run test suites or deployments so Claude keeps working while they complete.

Hooks turn Claude Code from a tool you supervise into one that supervises itself. The five minutes you spend configuring them will save you hours of manual formatting, vigilant terminal-watching, and post-hoc cleanup.


References