Files
dots/common/.pi/agent/extensions/protected-paths.ts
T
2026-09-12 23:38:18 +02:00

59 lines
1.9 KiB
TypeScript

/**
* Protected Paths Extension
*
* Blocks write and edit operations to protected paths.
*
* Adapted from pi's bundled `examples/extensions/protected-paths.ts`. Upstream ships a
* three-entry demo list (.env, .git/, node_modules/); the list below names the files
* that actually matter in this dotfiles tree — the ones holding secrets, and the git
* metadata the agent has no business rewriting.
*
* Scope, so this isn't over-trusted: it guards against the *model* calling write/edit.
* It cannot stop pi's own `/login`, which rewrites auth.json with the literal key —
* that is why auth.json is gitignored and auth.json.example is the tracked copy.
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
const protectedPaths = [
// Secrets. `secrets.fish` holds $DUSKADIY_API_KEY; auth.json is pi's own
// credential map and must keep $VAR references rather than literals.
"secrets.fish",
"auth.json",
".env",
"fish_variables",
// Credentials outside the repo.
".ssh/",
".gnupg/",
".aws/",
".netrc",
// Repo plumbing and vendored trees — never hand-edited.
".git/",
"node_modules/",
];
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "write" && event.toolName !== "edit") {
return undefined;
}
const path = event.input.path as string;
// Substring match, like upstream: cheap, and these names are distinctive enough.
// Note `auth.json.example` is intentionally caught too — it is a tracked file
// whose whole job is to contain no secret, so an agent rewrite is worth a look.
const isProtected = protectedPaths.some((p) => path.includes(p));
if (isProtected) {
if (ctx.hasUI) {
ctx.ui.notify(`Blocked write to protected path: ${path}`, "warning");
}
return { block: true, reason: `Path "${path}" is protected` };
}
return undefined;
});
}