all guides

Hooks, explained by monkey

(plus 3 hook recipes)

comment: HOOKS

A hook is your own command that Claude Code runs by itself at a set moment, so it happens every time instead of whenever Claude remembers. Here are three recipes to paste: auto-format, a .env guard and a ping when Claude needs you.

You can tell Claude to format your code after every change, and sooner or later it'll forget. A hook doesn't forget. It's a shell command you write into a settings file, and Claude Code runs it at a set moment every time, like right after Claude edits a file. Claude isn't the one deciding, so there's nothing to forget.

Where hooks go

Hooks go in a "hooks" block inside one of these settings files:

  • ~/.claude/settings.json: all your projects.
  • .claude/settings.json: this project. Commit it and your team gets the same hooks.
  • .claude/settings.local.json: this project, just you.

Type /hooks inside Claude Code to see every hook you have. It's read-only, so to change or remove a hook, edit the file it lives in.

Recipe 1: format every file Claude edits

After Claude edits or writes a file, this runs Prettier on that file. Put it in .claude/settings.json in your project:

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

What each part does:

  • PostToolUse: run after a tool has run.
  • "matcher": "Edit|Write": only after the tools Claude uses to edit and write files.
  • The command: jq picks the edited file's path out of the details Claude Code hands the hook, and passes it to Prettier.

Recipe 2: keep Claude out of your .env file

This one runs before Claude edits or writes a file. If the file is a .env file, the hook writes a reason to stderr and exits with code 2. Exit 2 blocks the action, and the reason goes back to Claude, so it knows why it was stopped.

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "f=$(jq -r '.tool_input.file_path'); case \"$(basename \"$f\")\" in .env|.env.*) echo \"Blocked: $f holds secrets. Don't edit .env files, ask the user instead.\" >&2; exit 2;; esac"
          }
        ]
      }
    ]
  }
}
  • PreToolUse: runs before a tool runs, and can stop it.
  • The case part matches .env, plus anything starting .env. like .env.local. That includes .env.example, so delete |.env.* if you want Claude to edit those.
  • >&2: sends the message to stderr, which is where Claude reads the reason from.
  • exit 2: blocks the edit. Every other file goes through as normal.

Recipe 3: get a ping when Claude needs you

Walk away while Claude works, and get a Mac notification when it needs your attention. Put this in ~/.claude/settings.json so it works in every project:

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

All three in one file

A settings file gets one "hooks" block. To use more than one recipe in the same file, put each event inside that one block. Don't add a second "hooks" block. If the file already has other settings, "hooks" sits next to them. Here are all three together:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "f=$(jq -r '.tool_input.file_path'); case \"$(basename \"$f\")\" in .env|.env.*) echo \"Blocked: $f holds secrets. Don't edit .env files, ask the user instead.\" >&2; exit 2;; esac"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Rather not touch JSON? Ask Claude

Claude can write a hook for you. Say what should happen and when:

say this to Claude:
Add a hook to .claude/settings.json that runs Prettier on every file you edit or write. Use a PostToolUse hook with the Edit|Write matcher. Keep everything already in the file, and if there's already a "hooks" block, add it inside that one.
say this to Claude:
Add a PreToolUse hook to .claude/settings.json that stops you from editing or writing any .env file. It should write the reason to stderr and exit with code 2.

Then type /hooks to check it's there.

Why a hook beats asking nicely

A rule in CLAUDE.md is advice: Claude reads it and usually follows it. A hook is enforced. The docs say a PreToolUse hook that denies a tool call blocks it even in bypass permissions mode or with --dangerously-skip-permissions. So if something must always happen, or must never happen, make it a hook.

The events worth knowing

  • PreToolUse: before a tool runs. It can block it.
  • PostToolUse: after a tool runs. Good for formatting and checks.
  • Notification: Claude needs you.
  • Stop: Claude finished replying.
  • SessionStart: a session starts.

There are more than 30 events in all, like PreCompact, SubagentStop and SessionEnd. The hooks reference lists every one.

Gotchas

  • Matchers are case-sensitive and must match the tool name exactly: Edit, Write, Bash. A lowercase edit won't match.
  • Older tips use the matcher Edit|MultiEdit|Write. The current tools list has no MultiEdit tool, and the docs' own example uses Edit|Write.
  • A PostToolUse hook can't undo anything, because the tool has already run. To stop something, use PreToolUse.
  • Stop fires every time Claude finishes a reply, not only when a whole task is done.
  • Hooks run your commands with your permissions, so only use hooks you trust. A command hook times out after 10 minutes by default.
  • /hooks only shows your hooks. To remove one, delete its entry from the settings file.
Hooks guidethe official docs: the auto-format example, exit codes and blockingHooks referenceevery event, and the read-only /hooks browser

Monkey believe in you. Now go build.

follow @nocodemonkey

More guides