C.W.K.
Stream
Lesson 06 of 08 · published

Hooks: Pre and Post Tool Use

~16 min · hooks, PreToolUse, PostToolUse

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

Hooks are policy in code

Hooks are user-defined callbacks that fire on tool events: PreToolUse (before a tool runs), PostToolUse (after), UserPromptSubmit, Stop, and others. They let you enforce policy, log activity, redact arguments, or veto a call entirely without changing the model's behavior.

Why they beat in-prompt rules

You can ask the model 'never run rm -rf' in the system prompt and it will mostly comply. A hook that returns a refusal on any rm -rf invocation is a hard guarantee. Use prompt for guidance; use hooks for invariants.

Three patterns to start with

(1) Logging hook — record every tool call to your structured log. (2) Redaction hook — scrub secrets out of arguments before they hit the tool. (3) Veto hook — refuse calls that match a deny pattern. cwkPippa's hooks include all three for different concerns.

Principle: Hooks are how policy survives prompt changes. The model can drift; hooks do not.

Code

PreToolUse hook that vetoes a dangerous command·python
from claude_agent_sdk import ClaudeAgentOptions, HookContext, HookOutput

async def block_destructive_bash(context: HookContext) -> HookOutput:
    if context.tool_name != "Bash":
        return HookOutput(allow=True)
    cmd = context.tool_input.get("command", "")
    if "rm -rf" in cmd or "dd if=" in cmd:
        return HookOutput(allow=False, reason="refused: matches destructive command pattern")
    return HookOutput(allow=True)

options = ClaudeAgentOptions(
    cwd="/srv",
    hooks={"PreToolUse": [block_destructive_bash]},
)
PostToolUse hook for telemetry·python
import time, json

async def log_tool_use(context):
    line = {
        "ts": time.time(),
        "tool": context.tool_name,
        "input_hash": hash(json.dumps(context.tool_input, sort_keys=True)),
        "duration_ms": context.duration_ms,
        "is_error": context.is_error,
    }
    with open("/var/log/agent.jsonl", "a") as f:
        f.write(json.dumps(line) + "\n")

options = ClaudeAgentOptions(
    cwd="/srv",
    hooks={"PostToolUse": [log_tool_use]},
)

External links

Exercise

Add a PreToolUse hook that logs every Bash command (read-only, no veto). Run a session, inspect the log. Add a veto rule for one command pattern you genuinely want to prevent. Test it.
Hint
If your veto rule never triggers, simulate by asking the model to run the matched command. The hook should refuse and the model should adjust.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.