Example

Decide which Pi mechanism a requirement needs

Build the review guard as a tool_call extension, and see why a force push is a prohibition while migrations/ is only a confirmation.

Two requirements that sound alike are different kinds of requirement. “Confirm before anything under migrations/” asks a person. “No force pushes” must hold whether or not a person is there to be asked. Only an extension can act on a tool call before it runs, so both belong in one tool_call handler — but they need different code.

Run it

Use Pi 1.0.4 and one of the example repository’s test runners. The file below is the chapter’s canonical extension, reproduced exactly:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
	pi.on("tool_call", async (event, ctx) => {
		if (event.toolName !== "bash" && event.toolName !== "edit") return;
		const raw = JSON.stringify(event.input);
		// \b, not (^|\s): in the serialised input "git" follows a quote, not whitespace.
		const forcePush = /\bgit\s+push\b.*(?:\s--force\b|\s-f\b|--force-with-lease\b)/.test(raw);
		// An unconditional prohibition, not a question: no confirmation prompt, so
		// there is nothing for a person to approve.
		if (forcePush) return { block: true, reason: "Force pushes are not permitted by this repository's guard" };
		// A different kind of rule. Ask, then proceed on the answer.
		if (!/\bgit\s+push\b/.test(raw) && !/migrations\//.test(raw)) return;
		if (ctx.hasUI && (await ctx.ui.confirm("Allow this call?", raw.slice(0, 120)))) return;
		return { block: true, reason: "Blocked by guard extension" };
	});
}

Load it with pi -e ./guard.ts, then ask Pi to run a command in one of these shapes.

Expected outcome

Call the model makes Guard behaviour Why
echo git push origin main One confirmation; allowed on yes An ordinary push is a confirmation
git push --force origin main Blocked outright, no prompt A force push is an unconditional prohibition
touch migrations/001.sql One confirmation; on no the file is never created A confirmation guard runs before the effect
echo hello Untouched The handler returns without deciding

The echo … forms matter: the pattern matches the serialised event.input, so echo git push is matched even though the command is harmless. That makes the command safe to run while still exercising the branch — the only thing that can turn it into an error is the guard itself.

Mechanism and limitations

Documented: exported ExtensionAPI declarations define pi.on("tool_call", …), the handler’s return shape ({ block: true, reason }), and ctx.hasUI / ctx.ui.confirm. Observed: guard.test.ts drives the real handler through an AgentSession with a scripted provider and asserts each row above, including that a blocked confirmation does not create the file. The regression test pins why the first version was wrong: the pattern (^|\s)git\s+push never fired on a plain push, because in {"command":"git push origin main"} the word git follows a quote, not whitespace. It only appeared to work for compound commands such as cd x && git push.

This is not a boundary. It matches bash and edit and nothing else — a write under migrations/, a multi_edit, or an extension’s own tool is not covered. It matches a serialised string, so git push origin +main rewrites a ref without matching the pattern, and echo "git push --force" matches it without doing anything. For a requirement that must hold rather than usually happen, the answer is an operating-system boundary, not a longer regular expression (Chapter 42). This example does not show a real model choosing a command; it shows the code around the model.

Full source and test .

Understand this example

Copy this prompt into your AI tool. No code runs here.

Apply this example

Copy this prompt into your AI tool. No code runs here.