Hooks are the part of the harness people get wrong first.

The intuition goes: "the agent keeps doing the dumb thing I told it not to do — I should make the dumb thing impossible." So they wire a hard block on the tool call, the agent now spends every session negotiating with the hook, and you've built a tiny process prison instead of a coding environment.

Hooks aren't for making the agent clever. They're for making the boring rules hard to forget.

The verification post was about giving the harness a callable gate. Hooks are the intercept surface — what happens before and around tool calls. The hard part is using that surface without turning the harness into safety theater.

Escalation ladder for Pi hooks showing notify, append context, soft block, and hard block around parser edit tool calls.

A small parser guard

tiny-ledger has an AGENTS.md rule from earlier in the series: parser changes need a fixture and a test. The agent will mostly honor it. Sometimes it won't — usually because the agent decided the fix was "too small" to warrant a test, or because the test was implied, or because the context window evicted the rule three turns ago.

A hook can catch the slip without forbidding the edit:

// .pi/extensions/parser-guard.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function extension(pi: ExtensionAPI) {
  pi.on("tool_call", async (event, ctx) => {
    if ((event.toolName !== "edit" && event.toolName !== "write") || !("path" in event.input)) {
      return;
    }

    const path = String(event.input.path);
    if (!path.startsWith("src/parser/") && path !== "src/parseReceipt.js") return;

    ctx.ui.notify("Parser edits need a fixture and a test before you claim done.", "warning");
  });
}

What this does: every time the agent calls edit or write on a path that touches the parser, the hook fires a UI warning. The edit still happens. The session still moves forward. The agent now has a fresh reminder right before the next tool call.

That's it. No blocking. No cage. Just a nudge at the exact moment the rule applies.

(Aside: Pi 0.75.3 wants "warning" for the notification level, not "warn". I learned this by writing "warn" first, watching the extension fail to load with zero useful logs, and feeling extremely smart for thirty minutes. Tiny typo, maximum embarrassment.)

Warnings before cages

The hook above is a soft gate. A hard gate would look like this:

return { block: true, reason: "Parser edits need a fixture and a test." };

Don't reach for that on day one. Hard gates make sense when the rule is unconditionally safe — never edit dist/, never push to main, never run rm -rf /. They stop being safe the moment the rule has edge cases. A parser edit might be paired with a test edit on the very next tool call. Blocking the first call because you can't see the whole diff yet is exactly how good intent becomes friction.

The escalation ladder I keep landing on:

  1. Notify. UI warning, no block. Agent gets a reminder, harness records the event. Cheapest possible enforcement.
  2. Append context. Inject a brief reminder into the next prompt ("you edited the parser without touching a fixture or test").
  3. Soft block with override. Block the call, surface the reason, let the agent or human acknowledge and proceed.
  4. Hard block. Refuse the call. Only when the rule is genuinely uncontestable.

Most rules belong at level 1 or 2. The cage is the exception.

Three extension surfaces

Pi's extension API has a small, learnable shape. From the verification post you already saw registerTool. With hooks, the picture fills out:

  • pi.registerTool({...}) — adds a new tool the agent can call. Project verification gates live here.
  • pi.on("tool_call", handler) — intercepts tool calls. Soft reminders, hard blocks, logging, audit trails.
  • pi.registerCommand({...}) — adds a slash command for the human in the loop (/verify, /handoff, project-specific session helpers).

That's a small enough surface that you can hold it in your head. Most repos won't use all three. Most will start with registerTool for the verify gate, add on("tool_call") for the one or two rules the agent keeps forgetting, and reach for registerCommand only when the same bash recipe shows up in three sessions in a row.

Local vs project hooks

Pi loads extensions from two places:

  • ~/.pi/agent/extensions/*.ts — personal harness behavior. Your tastes, your habits, your reminders. Loaded for every repo you work in.
  • .pi/extensions/*.ts — project-specific behavior. Committed with the repo. Loaded for anyone working in this codebase.

The split maps onto the same canon vs preference split from the relationship memory piece in my memory series. Personal reminders ("I always forget to update the CHANGELOG") go in ~/.pi/. Project rules ("parser changes need a fixture") go in .pi/extensions/. Don't put personal preferences in the project; don't put project rules in your home directory. The line is about who else has to live with your hook.

What hooks are not for

Not for making the model smarter. The model is the model. Hooks don't tune its priors.

Not for hiding failures. A hook that quietly fixes the agent's mistake by editing the input or rewriting the output is worse than a hook that surfaces the mistake. The whole point is making rules hard to forget — if the rule fires silently, you've added another layer of magic the next debugger has to peel back.

Not as a replacement for tests. A hook can remind the agent to write a test. It cannot decide whether the test is good. That's still a human or a review-agent's job.

Not as a replacement for AGENTS.md. The rule should live in the file the agent reads. The hook is the safety net for when the file gets evicted from context or the agent rationalizes around it. Both layers, both jobs.

Exercise

For tiny-ledger:

  1. Add the parser-guard hook. Trigger it intentionally by asking the agent to "make a tiny change to the parser, no need for a test, it's trivial."
  2. Watch the warning fire. Note whether the agent course-corrects without the warning being a hard block.
  3. Try the hard-block variant for the same edit. Note how the agent's behavior changes. Is the friction productive or theatrical?
  4. Move the parser-guard from .pi/extensions/ to ~/.pi/agent/extensions/. Open a different project. Does the hook still fire there? Should it?

The last one is the test for whether your hooks are scoped right.

Next primitive

The next post is about forks, clones, and subagents — when branching investigations earns its keep, and when it's just a way to make every thought look like a board meeting.