← All articles

How to Add a Pre-Action Hook to Claude Code

· Agent Vigil Team

Claude Code ships with a hooks system that lets you run shell commands before and after every tool call. Most developers never configure them — which means every file write, every delete, every bash command the agent attempts runs without review.

This guide walks you through setting up a PreToolUse hook from scratch, then connecting it to a remote approval flow so you can approve or deny actions from your phone while the agent works.

What hooks are and when they fire

Hooks are shell commands that Claude Code executes at specific points in its lifecycle. You configure them in .claude/settings.json (project-level) or ~/.claude/settings.json (global).

The one we care about is PreToolUse. It fires before every tool call — Bash, Write, Edit, Read, and so on. Your hook receives the tool name and its input as JSON on stdin. Based on the exit code your script returns, Claude Code either proceeds or stops:

Exit codeWhat happens
0Action proceeds normally
2Action blocked — stderr is sent back to Claude as feedback
OtherAction proceeds — stderr is logged but ignored

The key insight: exit code 2 is your kill switch. Anything you write to stderr becomes the reason Claude sees for why the action was blocked.

Step 1: A basic logging hook

Let’s start simple. This hook logs every Bash, Write, and Edit call to a file so you can see what the agent is doing.

Create .claude/hooks/log-actions.sh:

#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
echo "$(date -Iseconds) $TOOL: $(echo "$INPUT" | jq -c '.tool_input')" \
  >> .claude/hooks/actions.log
exit 0

Then wire it up in .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/log-actions.sh"
          }
        ]
      }
    ]
  }
}

The matcher is a regex against the tool name. Bash|Write|Edit means this hook only fires for those three tools — reads, globs, and greps pass through untouched.

Run Claude Code, give it a task, and tail the log:

tail -f .claude/hooks/actions.log

You’ll see every destructive action the agent attempts. But logging isn’t blocking — the agent still proceeds with everything.

Step 2: A blocking hook with a local allowlist

Now let’s actually stop dangerous actions. This version blocks any bash command containing rm, DROP, or sudo unless you manually update an allowlist:

#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')

if [ "$TOOL" = "Bash" ]; then
  CMD=$(echo "$INPUT" | jq -r '.tool_input.command')
  if echo "$CMD" | grep -qiE '(rm\s|drop\s|sudo\s)'; then
    echo "Blocked: command matches dangerous pattern" >&2
    exit 2
  fi
fi

exit 0

This works, but it’s all-or-nothing. Every matched command is denied. You can’t approve case-by-case, you can’t review from your phone, and you can’t let the agent keep working while you decide. The hook blocks synchronously — it either exits 0 or exits 2, with no way to pause and wait for a human.

Step 3: A remote approval hook

This is where the architecture changes. Instead of a local blocklist, the hook sends the action details to a remote endpoint and waits for the response. The human reviews on their phone and taps approve or deny. The hook unblocks and returns the appropriate exit code.

#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
CONTEXT=$(echo "$INPUT" | jq -c '.tool_input')

RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
  https://api.agentvigil.com/v1/review \
  -H "Content-Type: application/json" \
  -d "{\"key\":\"$AGENT_VIGIL_KEY\",\"action\":\"$TOOL\",\"context\":$CONTEXT}")

HTTP_CODE=$(echo "$RESPONSE" | tail -1)

if [ "$HTTP_CODE" = "200" ]; then
  exit 0  # Approved
else
  echo "Action denied by reviewer" >&2
  exit 2  # Denied or timeout
fi

The curl request blocks until one of three things happens: the human approves, the human denies, or the 5-minute timeout expires. No response means denied — that’s the default-deny contract.

The full working setup

Here’s the complete configuration. The hook script gates Bash, Write, and Edit through remote approval while letting read-only tools pass through without delay:

.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/review.sh"
          }
        ]
      }
    ]
  }
}

.claude/hooks/review.sh:

#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
CONTEXT=$(echo "$INPUT" | jq -c '.tool_input')

RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
  https://api.agentvigil.com/v1/review \
  -H "Content-Type: application/json" \
  -d "{\"key\":\"$AGENT_VIGIL_KEY\",\"action\":\"$TOOL\",\"context\":$CONTEXT}")

HTTP_CODE=$(echo "$RESPONSE" | tail -1)

if [ "$HTTP_CODE" = "200" ]; then
  exit 0
else
  echo "Action denied by reviewer" >&2
  exit 2
fi

Make it executable:

chmod +x .claude/hooks/review.sh

Now when Claude Code tries to write a file or run a command, your phone buzzes with the full details. Tap approve and the agent continues. Tap deny (or do nothing for 5 minutes) and the agent is told the action was blocked.

What to hook and what to skip

Not every tool call needs human review. Gate the tools that can change state:

  • Hook: Bash, Write, Edit, NotebookEdit — these modify your filesystem and run arbitrary commands
  • Skip: Read, Glob, Grep, WebSearch, WebFetch — read-only operations with no side effects

You can further narrow with context-aware logic in your script. For example, only gate bash commands that touch paths outside the project directory, or only gate writes to specific file patterns. The less you gate, the faster the agent works. The more you gate, the safer you are. Find the line that matches your risk tolerance.

What this gives you

With this setup, your AI agent works autonomously on the easy stuff while every destructive action routes through your phone for a single-tap decision. You’re not babysitting a terminal — you’re approving actions from your lock screen while doing something else.

The hook is the enforcement point. The approval service is the decision point. The agent can’t bypass either one, because neither one lives inside the agent.

That’s Level 3 safety: infrastructure enforcement that the agent doesn’t control.

Feb 24, 2026, 8:12 PM · 0ca512d