Skip to content

Claude Code hooks: run your own script before a tool call

What Claude Code hooks are, where they are configured, how a PreToolUse hook can block a command with exit code 2, and how to write one that finds its script from any folder.

Short answer: a hook is your own command that Claude Code runs at a set moment, configured under hooks in a settings file. Before a tool call (PreToolUse), exit code 0 lets it continue as normal and exit code 2 blocks it, with what you print to stderr passed back to Claude as the reason.

Permission rules decide whether a tool call may run based on its name and arguments. Sometimes you need more than a pattern: look at the actual command, check a file, or log what happened. That is what hooks are for. A hook is a command of your own that Claude Code runs at a given moment, and its exit code tells Claude Code what to do next.

Where are hooks configured?

Hooks live under hooks in a settings file, ~/.claude/settings.json for yourself or .claude/settings.json for the project. Each hook names the event, a matcher for the tool, and the command to run:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "timeout": 30,
        "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/guard.sh"
      }]
    }]
  }
}

This one runs guard.sh before every Bash command, and gives it 30 seconds.

What does a hook's exit code mean?

Exit codeWhat happens
0The call goes ahead
2The call is blocked, and what the script wrote to standard error goes to Claude

Blocking with a message is the useful part: Claude reads your reason and can try something else, instead of just hitting a wall.

How does a hook find its script from any folder?

Claude Code can be started in any subfolder of a project. A relative path like ./.claude/hooks/guard.sh then points nowhere. $CLAUDE_PROJECT_DIR is the project's root, so a hook written with it finds its script wherever the session started.

What does a first guard hook look like?

A guard.sh that refuses one dangerous pattern reads the call from standard input, checks it, and exits with 2 and a reason when it does not like it:

#!/usr/bin/env bash
input=$(cat)
if printf '%s' "$input" | grep -q 'rm -rf /'; then
  echo "Refusing to delete from the root of the file system." >&2
  exit 2
fi
exit 0

Make it executable with chmod +x .claude/hooks/guard.sh, commit it with the project, and everyone on the team gets the same guard.

What does the script receive?

The details of the call arrive as JSON on standard input: the session id, the working folder, the event name and, for tool events, tool_name and tool_input. For a Bash call, the command itself is in tool_input.command, so a sturdier guard reads that one field instead of searching the whole payload:

cmd=$(jq -r '.tool_input.command // empty')
case "$cmd" in
  *"rm -rf /"*) echo "Refusing to delete from the root of the file system." >&2; exit 2 ;;
esac
exit 0

Reading one field means a file name or a comment that happens to contain the pattern no longer trips the guard.

Which other moments can a hook run at?

PreToolUse is the one that can stop a call, but it is not the only event. The ones people use most:

EventWhen it runsTypical use
PostToolUseAfter a tool call succeedsFormat the file Claude just edited
UserPromptSubmitWhen you send a promptAdd context, or stop a prompt that contains a secret
NotificationWhen Claude Code wants your attentionA desktop notification while you are in another window
StopWhen Claude finishes its answerRun the tests, play a sound
SessionStartWhen a session starts or resumesLoad the current branch or open tickets as context

What about exit codes other than 0 and 2?

Any other code counts as an error in the hook, not a decision. Claude Code notes it and the call goes ahead, so a hook that crashes does not lock you out. That is also the trap: a guard with a typo fails open. Test a new hook once on a harmless command that should be blocked before you rely on it, and keep each script short enough to read in one go.

A hook also has a time limit, 60 seconds unless you set timeout. Keep it fast: a PreToolUse hook runs before every matching call, and a slow one makes every step of the session slower.

Where can you check this?

How do hooks and permissions work together?

Use permission rules for what you can say with a pattern (never read .env, always ask before git push), and hooks for what needs a look at the details. See Claude Code permissions: allow, ask and deny for the rules.

Config Studio for Claude Code has hooks as one of its 20 surfaces: it explains each event, writes the settings block, and its install script adds the hook file and makes it executable. Its Try it tab answers "Would this hook run?" and "Would this be allowed?" for a call you describe, before you install anything.

The tool

Do it in your browser.

Claude Code

Config Studio for Claude Code

Every file Claude Code reads, explained: what each setting does, where it lives, which file wins, and an editor that writes it for you.

Keep reading

More from the blog.

Every post

Contact