← Back to posts

A Quick Introduction to Claude Code Hooks

claude-codehooksautomation

I’ve written a lot about sub-agents lately, but there’s another Claude Code feature that’s been quietly saving me from myself: hooks.

What Are Hooks?

Hooks are shell commands that run automatically at specific points in Claude Code’s lifecycle. Think of them as guardrails—or tripwires, depending on how you look at it.

The key difference from sub-agents: hooks are deterministic. They don’t involve Claude making decisions. You write a script, it runs, it either passes or fails. No LLM involved (usually).

When Do They Fire?

Claude Code has about ten different hook events, but the ones I actually use are:

PreToolUse — Runs before Claude executes any tool. This is where you can block dangerous operations or validate inputs.

PostToolUse — Runs after a tool completes. Useful for auto-formatting code or running linters.

UserPromptSubmit — Runs when you submit a prompt. You can add context or block certain requests.

Notification — Fires when Claude needs your attention. I use this for desktop notifications when I’m working on something else.

There are others like SessionStart, Stop, and SubagentStop, but I haven’t found myself reaching for those yet.

A Simple Example

Here’s one that’s been useful: preventing edits to files I don’t want Claude touching.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 -c \"import json, sys; data=json.load(sys.stdin); path=data.get('tool_input',{}).get('file_path',''); sys.exit(2 if any(p in path for p in ['.env', 'package-lock.json', '.git/']) else 0)\""
          }
        ]
      }
    ]
  }
}

This goes in .claude/settings.json (or settings.local.json if you don’t want to commit it). The matcher specifies which tools trigger the hook—in this case, Edit or Write. Exit code 2 blocks the operation and shows the error to Claude.

It’s a bit ugly as a one-liner, but it works. You can also point to an external script if you want something more readable.

How Hooks Talk to Claude

The communication model is simple:

  • Input: Your hook receives JSON via stdin with details about the operation (tool name, file path, command, etc.)
  • Output: Exit code 0 means proceed, exit code 2 means block and show stderr to Claude

If you want to get fancy, you can return JSON to modify tool inputs or inject context into the conversation. But honestly, I’ve found simple pass/fail scripts cover 90% of what I need.

Where I’ve Found Them Useful

A few patterns that have stuck:

File protection — The example above. Keeps Claude from touching .env files, lock files, or anything in .git/.

Auto-formatting — A PostToolUse hook that runs Prettier on any TypeScript file after Claude edits it. No more “can you fix the formatting” follow-ups.

Command validation — Blocking rm -rf or other commands I never want run, even accidentally.

Desktop notifications — When Claude’s waiting for input and I’ve tabbed away. Small thing, but it’s nice.

Hooks vs Sub-Agents

The mental model I use: sub-agents are for delegating decisions, hooks are for enforcing rules.

If I want Claude to think about something (like reviewing code for security issues), that’s a sub-agent. If I want to guarantee something happens (like never editing .env files), that’s a hook.

Hooks are faster too—they’re just shell scripts, no LLM calls involved.

Getting Started

The easiest way to configure hooks is the /hooks command in Claude Code. It opens an interactive menu where you can add and edit hooks without hand-writing JSON.

Or you can edit .claude/settings.json directly. The structure is straightforward once you’ve seen an example.

One thing to know: Claude Code captures hook configuration at startup. If you change your hooks mid-session, you’ll need to restart for the changes to take effect.

The Gotchas

Security: Hooks run with your credentials. If someone gets malicious code into your settings file, those hooks run automatically. Be careful about what you execute.

Quoting: Shell escaping in JSON is painful. When in doubt, put your logic in an external script and call that instead.

Debugging: Use claude --debug to see what’s happening with your hooks. Saved me a lot of head-scratching.

I’m still discovering new uses for hooks. They’re one of those features that seems simple until you realize how much tedious stuff you can automate away.