ASK KNOX
beta
LESSON 665

PostToolUse Hooks: Verification After the Action

A PostToolUse hook cannot stop what already happened — but by verifying the aftermath and feeding the result back as context, it turns a silent-broken edit into an immediate correction loop.

9 min read·Skill Engineering Mastery

The Hook That Can't Block — and Why That's the Point

PreToolUse stops bad actions before they happen. PostToolUse fires after the tool already succeeded, which means it cannot stop anything. New authors hear "can't block" and assume PostToolUse is the weaker hook. It isn't. It is the hook that does something PreToolUse structurally cannot: it can inspect the result.

You cannot type-check a file before it exists. You cannot run the test that covers a function before the function is written. You cannot format code that hasn't been produced. Every check that needs the action to have already happened lives in PostToolUse — and feeding that check's result back into the model's context is how you stop the most common failure mode in agentic coding: the edit that looks done but quietly broke the build.

The Verification Sequence

The payload that reaches a PostToolUse hook carries one field the PreToolUse payload doesn't: tool_response, the result of the call. You get both the input and the outcome.

The sequence is always the same four beats. The tool ran. You verify the aftermath with a deterministic check the model cannot be trusted to run on its own. You feed the result back. Claude self-corrects. Here is the canonical post-edit type-gate:

#!/usr/bin/env bash
# ~/.claude/hooks/verify-types.sh — PostToolUse, matcher "Write|Edit"
input="$(cat)"

path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // ""' 2>/dev/null)"
case "$path" in *.ts|*.tsx) ;; *) exit 0 ;; esac   # only TS files

# The check can only run because the file now exists on disk.
if ! out="$(npx tsc --noEmit 2>&1)"; then
  echo "Type-check FAILED after editing $path:" >&2
  echo "$out" | head -20 >&2
  exit 2   # feed the errors back — Claude must fix before it's done
fi
exit 0

And the wiring in .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/verify-types.sh" }]
      }
    ]
  }
}

The two non-negotiables: the check has to actually run (not just log), and a failure has to reach Claude via exit 2 + stderr. A hook that runs the type-checker and then exits 0 has built an audit trail nobody reads. The exit-2-with-stderr path is the entire mechanism — it is what converts "tsc reported errors" into "Claude knows it has errors and is fixing them."

The Verify → Feedback Loop

The reason this matters so much is that it bounds the agent loop by a passing check instead of by the model deciding it is finished. Left alone, a model will declare a task done the moment its plan is complete — whether or not the result compiles. A PostToolUse verifier moves the goalpost from "I think I'm done" to "the check says I'm done."

Each failed verification feeds an error back; Claude fixes; the next edit re-triggers the hook; the check runs again. The arc repeats until the check is clean. The loop terminates on a green check, not on the model's confidence — which is exactly the property you want from automated work.

Pre vs Post — Choosing the Right Hook

The decision is mechanical. Ask one question of every check you want to automate: does this check need the action to have already happened?

  • No → PreToolUse. You can still stop it. Secret-in-content, push-to-main, write-to-protected-path. These are prevention.
  • Yes → PostToolUse. The action is the input to the check. Type-check, lint, format, run-the-covering-test. These are verification.

A subtle but important corollary: PostToolUse must stay fast and side-effect-light, just like PreToolUse, because it runs in the critical path of every matching call. Running the entire test suite on every single edit will make the agent unbearably slow. Scope the check to what the edit touched — type-check the file, lint the file, run the one test that covers it — and save the full-suite gate for the Stop hook, which fires once when Claude tries to finish, not on every edit.

Build It

Author the post-edit verifier end to end: the script that reads the edited file's path off the PostToolUse payload, runs the relevant deterministic check, and — critically — feeds a failure back to Claude via exit 2 and stderr while staying silent (exit 0) on success. Then wire it into settings.json under PostToolUse with the right matcher.

What's Next

You now have both halves of hook-based enforcement: gate before the action, verify after it. The next lesson leaves hooks behind and tackles the other thing that separates a one-shot script from a real skill — memory. A skill that reads a memory store at the start and writes lessons back at the end stops repeating its own mistakes across sessions.