DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

Hooks: Intercepting Tool Calls Before the Damage Happens

Hooks: Intercepting Tool Calls Before the Damage Happens

The agent decides to call Bash("rm -rf /"). You want to stop it before it happens. Or: it reads a config file and you want to record who read it, when, and why. Or: it tries to write outside the project directory and you want to block that without having to restrict every tool individually.

Hooks solve this. They're callbacks the harness executes before or after each tool call, before each user prompt, and at session transition points. In any agentic system that moves beyond a prototype, hooks are the layer that separates "it works" from "it works with accountability."

Interception points

Five points where you can inject logic:

  • UserPromptSubmit: when the operator sends a prompt, before the model sees it
  • PreToolUse: before executing a tool call the model decided on
  • PostToolUse: after execution, before the result returns to the model
  • Stop: when the model decides to stop without calling a tool
  • SessionStart / SessionEnd: bootstrap and teardown

Not every harness exposes all five. Claude Code exposes all five. The concept is the same regardless of harness.

PreToolUse is the most valuable

If you only have budget to implement one type of hook, implement PreToolUse. That's where most protections and validations live.

Concrete example: blocking direct pushes to main during auto-pilot sessions, even if the agent has Bash(*) in its allowlist:

#!/bin/bash
# pretooluse.sh
TOOL_NAME=$(jq -r '.tool_name' "$1")
ARGS=$(jq -r '.tool_input.command' "$1")

if [[ "$TOOL_NAME" == "Bash" ]] && echo "$ARGS" | grep -q "git push.*\bmain\b"; then
  echo "BLOCKED: push to main requires explicit operator confirmation" >&2
  exit 2  # exit 2 = block tool call
fi

exit 0  # exit 0 = allow
Enter fullscreen mode Exit fullscreen mode

When the agent attempts git push origin main, the harness calls the hook, the hook detects the pattern, returns exit 2. The harness shows the block message to the model. The model now knows it can't push to main without explicit confirmation.

A hook is not magic: it's a script that returns an exit code. 0 = allow, 2 = block, others = error. Arguments arrive via JSON at $1.

Scope validation

A classic hook: ensuring the agent doesn't write outside the project directory. Defense in depth against an overly broad tool surface:

#!/bin/bash
TOOL=$(jq -r '.tool_name' "$1")

if [[ "$TOOL" == "Write" ]] || [[ "$TOOL" == "Edit" ]]; then
  TARGET=$(jq -r '.tool_input.file_path // .tool_input.path' "$1")
  PROJECT_ROOT=$(realpath "$PWD")
  TARGET_ABS=$(realpath -m "$TARGET" 2>/dev/null)

  if [[ "$TARGET_ABS" != "$PROJECT_ROOT"* ]]; then
    echo "BLOCKED: write attempt outside project directory" >&2
    exit 2
  fi
fi

exit 0
Enter fullscreen mode Exit fullscreen mode

Any write attempt outside pwd is blocked, regardless of how the agent tried to get there. The realpath call resolves relative paths, symlinks, chained ../ sequences, everything before the comparison.

Audit chain via PostToolUse

PostToolUse is where you record. The hash-linked NDJSON append-only pattern creates an audit chain where any tampering breaks the chain: altering a line in the middle invalidates every line that follows.

Each tool call generates one NDJSON line with timestamp, type, data, previous hash, and current hash. To verify integrity: walk the log, recompute all hashes, verify the chain. If anyone altered a line, even just the timestamp, verification fails immediately.

When someone asks "what did the agent do on Wednesday night?", you have the answer in an append-only log that cannot have been altered without leaving a trace.

If you can't reconstruct every agent action from the last 24 hours from disk, you're operating without an audit trail. Don't call it production.

UserPromptSubmit: blocking credentials in input

A less obvious hook: validating what the operator sends. Defense against accidentally pasting credentials:

#!/bin/bash
PROMPT=$(jq -r '.prompt' "$1")

if echo "$PROMPT" | grep -qE "(sk-[a-zA-Z0-9]{40,}|ghp_[a-zA-Z0-9]{36}|AKIA[0-9A-Z]{16})"; then
  echo "BLOCKED: prompt contains what appears to be a credential" >&2
  exit 2
fi

exit 0
Enter fullscreen mode Exit fullscreen mode

The operator pastes a CI log into the prompt. The log contains a secret. The hook intercepts before the prompt reaches the model and before it lands in the session history. Secrets leaked into an LLM session can become part of exported or shared history.

Anti-pattern: hook as business logic

The temptation: using hooks to implement business logic that the agent should be handling. Symptom: a PreToolUse hook with 200 lines deciding what to do for each combination of tool and arguments.

It fails on two fronts. First, logic inside a hook is invisible to the model: the model doesn't know it exists, so it can enter a loop trying an action that always gets blocked. Second, a hook doesn't have the context the model has.

The rule: hooks are for invariants, not for policies. Invariant: "never write outside the project" (always true). Policy: "only write if there's a pending review" (context-dependent). Invariants in hooks; policies in the agent's system prompt.

Performance

Hooks run synchronously. Each tool call waits for the hook to finish before proceeding. A 500ms hook across 30 tool calls adds 15 seconds to the session. Keep hooks fast: avoid network calls, use exit code 0 early if no rule applies, background logs with & disown if you don't need the log before making a decision. A good hook executes in under 50ms.

Defense in depth

Serious agentic systems don't rely on a single layer of defense:

  1. System prompt restricting behavior (layer 1: instruction)
  2. Tools allowlist limiting capabilities (layer 2: capability)
  3. PreToolUse hooks validating arguments (layer 3: validation)
  4. PostToolUse hooks recording actions (layer 4: audit)
  5. SessionEnd hook consolidating state (layer 5: teardown)

Each layer can fail individually. All five together rarely fail simultaneously. When you hear "AI agents are risky in production," the person is almost always operating without hooks. It's not the model, it's the absence of defense in depth.

Top comments (0)