Guide

Claude Code hooks

Harrowtech.ai · last checked 13 October 2026

Claude Code hooks are commands that run automatically at set points in a session, such as before a file edit or when Claude needs your attention. This page explains what they are, where to set them up, and walks through one safe example, tested on Claude Code 2.1.296 on 13 October 2026.

Harrowtech.ai is independent of Anthropic.

What a hook is

Anthropic describes hooks as user-defined shell commands that Claude Code runs at specific points in its lifecycle. The point of them is certainty: a certain action always happens, instead of relying on the model to choose to do it. Typical uses are formatting a file after every edit, blocking changes to protected files, and sending a desktop notification.

A hook is not the same as telling Claude a rule in CLAUDE.md. A rule is an instruction the model may or may not follow. A hook is a program Claude Code runs itself.

Events

Each hook is attached to an event. Anthropic's reference lists 33 of them. These are the ones you will meet first:

PreToolUseBefore a tool call runs. It can block the call.
PostToolUseAfter a tool call succeeds.
UserPromptSubmitWhen you submit a prompt, before Claude processes it.
NotificationWhen Claude Code sends a notification, for example because it is waiting for input.
StopWhen Claude finishes responding.
SessionStart and SessionEndWhen a session begins or resumes, and when it ends.
PreCompactBefore the conversation is summarised to free up context.

Where hooks are configured

Hooks live in a hooks block inside a settings file. Where you put it sets who it applies to:

~/.claude/settings.jsonAll your projects. Not shared.
.claude/settings.jsonOne project. Can be committed so the team shares it.
.claude/settings.local.jsonOne project. Not committed.
Managed policy settingsOrganisation-wide, set by an admin.

Plugins, skill files and subagent files can also carry hooks. Entries from different levels merge rather than replace each other. Inside Claude Code, type /hooks to browse everything configured, grouped by event.

Most hooks have "type": "command", which runs a shell command. Anthropic lists four other types: http, mcp_tool, prompt and agent. The last is marked experimental.

How a hook talks back

Claude Code sends the event details to your command as JSON on standard input. Your command answers with its exit code:

A matcher narrows when a hook fires. For tool events it filters on the tool name, so "Edit|Write" fires only for those two tools. Matchers are case-sensitive.

A worked example: protect files from edits

This is Anthropic's own example. It stops Claude editing .env, package-lock.json or anything under .git/. First, save this script as .claude/hooks/protect-files.sh:

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
FILE_PATH="${FILE_PATH//\\//}"

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

Make it executable with chmod +x .claude/hooks/protect-files.sh. Then register it in .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

Test the script on its own first, as Anthropic's troubleshooting section suggests, by piping in sample JSON and checking the exit code:

echo '{"tool_name":"Write","tool_input":{"file_path":"/x/.env"}}' | .claude/hooks/protect-files.sh
echo $?

What happened in testing

The hook was set up in an empty test folder, and Claude was asked, in print mode, to create a .env file and a notes.txt file. Two results are worth knowing about.

The Python line, if you have no jq

Replace the FILE_PATH=$(echo "$INPUT" | jq ...) line in the script with this one. It reads tool_input.file_path from the JSON and prints an empty string when the field is missing, as the // empty part of the jq version does:

FILE_PATH=$(echo "$INPUT" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("file_path",""))')

Tested on 13 October 2026, on a machine without jq, using Python 3.12.3. With the line in place, piping in the .env sample above printed the Blocked message and returned exit code 2. The same input with /x/notes.txt as the path returned 0. The line depends on python3 being installed, so run both piped tests again on the machine where the hook will live.

Security

Anthropic's warning, in its words: "Command hooks execute shell commands with your full user permissions. They can modify, delete, or access any files your user account can access. Review and test all hook commands before adding them to your configuration."

Two more points from the same section:

What this guide does not cover

It does not go through all 33 events, the JSON each one receives, http, mcp_tool, prompt and agent hooks, or hooks inside skills and plugins. The hooks reference has those. It also does not cover Windows PowerShell hooks, which need a "shell": "powershell" setting.

Harrowtech.ai's AI Setup service covers coding tools set up with rules and guardrails around how a team's developers work. See services.

Sources

Related: All Harrowtech.ai guides · Claude Code CLI and headless mode · Claude Code subagents explained · Claude Code best practices