← All guides

Guide · Automation

Claude Code hooks, with examples

Run your own script every time Claude is about to use a tool. My example: a secret scanner that stops API keys from leaving my machine.

Last updated

What a hook is. A command that Claude Code runs at a fixed moment, such as before a tool call (PreToolUse), after one (PostToolUse) or when a session starts (SessionStart). Unlike an instruction in CLAUDE.md, a hook always runs: Claude can't forget it or decide to skip it.

1Know where hooks go

  • ~/.claude/settings.json: runs in every project. My scanner lives here.
  • .claude/settings.json in a repo: only that project, and shared with anyone who clones it.

Type /hooks in Claude Code to see which hooks are active.

2The problem my hook solves

I let Claude hand big reads to Gemini through MCP (setup guide). That means files leave my machine. I wanted a check that a prompt, or any file it points at with @path, doesn't contain a key, a password or an email address.

3Register the hook

This is the real block from my ~/.claude/settings.json (only the user folder is changed). The matcher is a regex on the tool name, so it only fires for Gemini tools:

"hooks": {
  "PreToolUse": [
    {
      "matcher": "mcp__gemini-cli__.*",
      "hooks": [
        {
          "type": "command",
          "command": "node",
          "args": ["C:/Users/<you>/.claude/hooks/gemini-secret-scan.js"],
          "timeout": 30,
          "onFailure": "block",
          "statusMessage": "Scanning Gemini prompt for sensitive info..."
        }
      ]
    }
  ]
}

onFailure: "block" means that if the script crashes, the call is stopped instead of going through unchecked.

4Read what Claude is about to do

Claude Code sends the hook a JSON object on stdin: the tool name, its input and the working folder. The script reads it in one line:

const input = JSON.parse(fs.readFileSync(0, 'utf8')); // { tool_name, tool_input, cwd, ... }

Then it checks every string in tool_input against a list of patterns:

const CONTENT_RULES = [
  ['private key', /-----BEGIN [A-Z ]*PRIVATE KEY-----/],
  ['AWS access key', /\bAKIA[0-9A-Z]{16}\b/],
  ['GitHub token', /\b(gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{20,})/],
  ['API key (sk-...)', /\bsk-(ant-)?[A-Za-z0-9_\-]{20,}/],
  ['email address', /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/],
  // ...plus Google, Slack, JWT, passwords and connection strings
];

5Answer with allow, ask or deny

If the script prints nothing, the call goes ahead. To stop and ask me first, it prints a decision:

process.stdout.write(JSON.stringify({
  hookSpecificOutput: {
    hookEventName: 'PreToolUse',
    permissionDecision: 'ask',           // or 'deny' to block outright
    permissionDecisionReason: 'Possible sensitive info would be sent to Gemini:\n- ' + hits.join('\n- '),
  },
}));

The reason names the rule and where it matched, never the value itself. Otherwise the hook would leak the secret into the chat log.

6Test it without Claude

A hook is just a script, so pipe it fake input and look at the output:

echo '{"tool_input":{"prompt":"summarise this, key AKIAABCDEFGHIJKLMNOP"},"cwd":"."}' \
  | node ~/.claude/hooks/gemini-secret-scan.js

# {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask",
#   "permissionDecisionReason":"Possible sensitive info would be sent to Gemini:\n- AWS access key in prompt"}}

A clean prompt prints nothing and exits 0. Test both cases before you trust it.

7Let Claude write the next one

You don't have to write a hook yourself. Describe it in plain words and Claude writes both the script and the settings block. A prompt like this gets you most of the way:

Add a PreToolUse hook in ~/.claude/settings.json for mcp__gemini-cli__ tools. It runs a
Node script that scans the prompt and every @path file for keys, tokens, passwords and
emails, and asks me before the call if it finds any. Report the rule and location only,
never the matched value.

Then read the script yourself. A hook runs with your full permissions on every matching call.

Keep hooks fast and narrow. A hook on .* runs before every single tool call, and a slow one makes the whole session feel slow. Match only the tools you care about, and set a timeout.

Next: package a whole workflow so Claude loads it only when needed, in writing Claude Code skills.