Generative AIAI tools for developersClaude Code

Claude Code hooks

7 minutes read

Claude Code performs various tasks autonomously. However, critical production work often needs visibility and safety measures. For example, blocking dangerous operations such as reading sensitive files. Let's explore how you can design hooks to provide that control.

What hooks are

In simple terms, hooks are shell commands you define that execute at specific points when Claude Code is working. They function like lifecycle "interrupts" in Claude Code's execution pipeline, allowing you to add custom logic. These commands can perform actions, such as validating or blocking certain actions. While you can prompt the LLM to perform some of these actions at specific points, hooks ensure these actions always occur and are a more intuitive approach.

There are several hooks you can use in your workflow for various events. To view them, run the /hooks command from the interactive REPL. Let's go over them:

Hook

When it fires

What hooks can do

SessionStart

When a Claude Code session begins or is resumed (or /resume, /clear)

  • Initialize environment

  • Load project metadata

  • Load memory/context

UserPromptSubmit

When a user submits a prompt, before Claude processes it

  • Prompt validation (e.g., disallow dangerous commands)

  • Auto-augment prompts (e.g., add a system prompt)

  • Logging/analytics

PreToolUse

Just before Claude Code invokes a tool (after tool parameters are constructed)

  • Prevent destructive operations, enforce rules (e.g., no writes in production)

  • Sanitize inputs

  • Ask for confirmation

PostToolUse

After a tool finishes executing successfully

  • Validate results

  • Auto-format code

  • Log tool usage

  • Trigger follow-up tasks

Notification

When Claude Code sends a notification (e.g., "needs permission", "waiting for input (idle)")

  • Customize user-facing notifications (e.g., via desktop alerts)

  • Log notifications

Stop

When the main Claude Code agent finishes responding

  • Ensure final sanity checks (e.g., that tasks are done)

  • Enforce post-response constraints

  • Logging

SubagentStop

When a Claude subagent completes performing a task

  • Interact with or validate subagent output

  • Chain additional tasks

  • Error handling

PreCompact

Just before Claude Code performs compaction (e.g., when the context window is full or the user triggers /compact)

  • Save backups

  • Take snapshots of context

  • Preserve the state before compacting

SessionEnd

When a Claude Code session ends

  • Cleanup (e.g., delete temp files)

  • Flush logs

  • Persist summary stats or state

  • Send telemetry

As seen, you can control Claude Code's execution pipeline in many ways to validate input, block actions, log activity, and automate safety checks in your workflows.

Hooks scripts

You can write scripts in Python or Bash and point your hooks' configuration to these files. For simple use cases, you can write single commands that will run when a hook is triggered. However, scripts offer greater flexibility, allowing for complex logic, error handling, and reusability (you can check the files into version control). These are features that single inline commands cannot easily provide.

Let's create a script for a hook that prevents Claude Code from committing code to protected branches like main. Expand the section below to see the hook's script:

Script file
#!/usr/bin/env python3
import sys
import json
import subprocess
import os

def get_current_branch():
    """Get the current git branch"""
    try:
        result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
                              capture_output=True, text=True, cwd=os.getcwd())
        if result.returncode == 0:
            return result.stdout.strip()
    except Exception:
        pass
    return None

def is_protected_branch(branch):
    """Check if branch is protected"""
    protected_branches = ['main', 'master', 'production', 'release']
    return branch in protected_branches

def main():
    data = json.load(sys.stdin)
    tool = data.get("tool_name", "")
    tool_input = data.get("tool_input", {})

    # Check Bash commands for git operations
    if tool == "Bash":
        cmd = tool_input.get("command", "")

        # Block git commit operations on protected branches
        if "git commit" in cmd:
            current_branch = get_current_branch()
            if current_branch and is_protected_branch(current_branch):
                print(json.dumps({
                    "hookSpecificOutput": {
                        "hookEventName": "PreToolUse",
                        "permissionDecision": "deny",
                        "permissionDecisionReason": f"Committing to protected branch '{current_branch}' is prevented by policy"
                    }
                }))
                sys.exit(0)

    # Otherwise, allow
    sys.exit(0)

if __name__ == "__main__":
    main()

The input to a hook is JSON data received through standard input. Depending on the event, this data can contain different pieces of information. Here's an example of the input available to our script:

  {
    "session_id": "session_abc123",
    "transcript_path": "/home/user/.claude/transcripts/session_abc123.json",
    "cwd": "/home/user/...",
    "hook_event_name": "PreToolUse",
    "tool_name": "Bash",
    "tool_input": {
      "command": "git commit -m \"Add new feature implementation\"",
      "description": "Create git commit with message",
      "timeout": 120000
    }
  }

We then parse this data and perform the necessary checks:

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

When our script runs, it checks the command that was run. If it detects git commit in the command, it checks the current branch using git rev-parse --abbrev-ref HEAD. If it is main, master, production, or release, it sends a JSON output that has a "permissionDecision": "deny" key back to Claude Code. This tells Claude to block the tool call.

JSON output provides a more advanced way to return output to Claude Code. For simpler cases, you can use exit codes:

  • Exit code 0 for success, allowing the tool to proceed.

  • Exit code 2 for errors. The event is blocked and stderr is fed back to Claude Code.

  • Other codes — for non-blocking errors. stderr is shown to the user, and execution continues.

Next, save the file in a convenient location since you will need to reference it later.

To implement your own hook scripts, check out the hooks reference for more details, such as event-specific data and other details.

Configuration

Before adding this script to your hook configuration, make the file executable:

 $ chmod +x <file_name>.py

Next, type /hooks and select the PreToolUse hook. For tool-related hooks, PreToolUse and PostToolUse, you first need to define a pattern to match tools targeted by the hook. So, select + Add new matcher and type Bash to match Bash commands (used for git operations in our case). Then select + Add new hook... and enter the command to run the script (replace with the absolute path to your script):

python3 /absolute/path/to/script.py

Hooks configurations are stored in your settings.json files, as we saw earlier. For this example, let's store the configuration in our user settings (~/.claude/settings.json). That's it! Now, Claude Code will not commit to protected branches that likely contain critical production code:

Claude Code prevented from pushing to main by our hook.

Common uses

With hooks, you can enforce crucial rules for style, security, and behavior outside of your prompts. This ensures that important checks run automatically every time. You no longer have to rely on manually performing actions before a commit or other key actions.

Hooks also act as a safety net for your codebase. You can configure them to prevent destructive operations before they happen. We saw how you can automatically block a direct commit to the main branch. You can also use them to prevent accidental modifications to sensitive configuration files, giving you an essential layer of protection.

If you need to streamline your daily workflow by automating routine tasks, hooks can help. Imagine running linters, formatters, and style checks right after you accept Claude Code's edits or just before you commit your code. This saves you valuable time and ensures your entire codebase remains consistent without extra effort.

Hooks are designed to fit into your broader development environment. You can integrate them with your existing tools to automate parts of your local pipeline. This allows you to connect Claude Code's capabilities with the other scripts and processes you already use, creating a more cohesive workflow.

In a team environment, hooks are invaluable for maintaining consistency across all contributions. They can enforce shared standards for everything from directory structures and naming conventions to testing requirements. This guarantees that everyone on the team automatically adheres to the same policies, making collaboration much smoother.

Key considerations

As you integrate hooks into your workflow, here are some important points to remember:

  • Hooks run with your environment's permissions. Poorly written hook commands and scripts can accidentally delete files, leak secrets, or execute unintended commands.

  • Always validate and sanitize inputs to hooks (e.g., user prompt data, file paths) to prevent injection attacks or path traversal.

  • Use matchers carefully so hooks only fire when necessary — too many hooks or overly broad matchers will slow down your workflow.

  • Ensure your hook commands and scripts are robust—handling missing tools, unexpected input, and edge cases gracefully.

  • Use absolute paths or environment variables like $CLAUDE_PROJECT_DIR to locate scripts reliably.

  • Regularly review and audit hook scripts since they run automatically. Changes made by others could potentially cause harm.

Conclusion

Hooks provide specific interruption points in Claude Code's execution where you can perform validation, logging, transformation, or block actions. These lifecycle events include session start/end, pre/post-tool use, and more. You can use hooks to enforce security and style policies, prevent harmful actions, and automate routine checks without modifying prompt instructions. The example demonstrates how a simple script can prevent a potentially risky automatic action through a controlled, policy-driven decision.

For security, handle hooks like regular production code. Since hooks run with your system user's permissions, you should maintain strict review and audit processes. Keep them robust by testing for both normal operations and error scenarios. When implemented properly, hooks transform Claude Code into a controlled, auditable part of your development workflow.

How did you like the theory?
Report a typo