GVNR
The expensive mistakes are free. rm -rf costs nothing. curl … | sh costs nothing. Reading .env costs nothing. A spend cap waves every one of them through on a full budget, because it is answering a different question.
This is the other check: not can it afford this, but may it do this at all.
Two places you can intervene, and they are not equivalent.
| At the API | Before the tool runs | |
|---|---|---|
| Sees the model call | yes | yes |
| Sees a shell command | no | yes |
| Sees a file read | no | yes |
| Can refuse before anything happens | no, the call is already going out | yes |
An API-level guard never sees rm -rf, because deleting a directory is not an API call. If your agent exposes a pre-execution hook, that is where this belongs. Claude Code calls it PreToolUse.
Claude Code pipes a JSON event on stdin before each tool call and reads your JSON on stdout. Three decisions: allow, deny, ask.
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "why, in words the human will read"
}
}
The trap: exit 0 and print JSON. Exiting 2 with JSON is ignored, which fails silently in the one direction a guard must never fail in. You get no error, and every action is allowed.
This is self-contained. It needs nothing installed.
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
const RULES = [
{ name: 'run a script downloaded from the internet', tool: 'Bash',
match: /(curl|wget)[^|]*\|\s*(ba|z|fi)?sh/i, act: 'deny' },
{ name: 'delete a whole tree', tool: 'Bash',
match: /rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)/i, act: 'ask' },
{ name: 'rewrite git history', tool: 'Bash',
match: /push\s+(--force|-f)\b|reset\s+--hard|filter-branch/i, act: 'ask' },
{ name: 'read or write credentials', tool: '',
match: /\.env\b|id_rsa|\.pem\b|credentials\.json|\.aws\/|\.ssh\//i, act: 'ask' },
{ name: 'publish or deploy', tool: 'Bash',
match: /npm\s+publish|vercel\s+.*--prod|kubectl\s+(apply|delete)|terraform\s+apply/i, act: 'ask' },
];
function emit(decision, reason) {
process.stdout.write(JSON.stringify({ hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: decision,
permissionDecisionReason: reason,
}}));
process.exit(0); // never exit 2 with JSON
}
let ev = {};
try { ev = JSON.parse(readFileSync(0, 'utf8') || '{}'); } catch {}
const tool = String(ev.tool_name || '');
const text = `${tool}:${JSON.stringify(ev.tool_input ?? '')}`;
for (const r of RULES) {
if (r.tool && r.tool.toLowerCase() !== tool.toLowerCase()) continue;
if (!r.match.test(text)) continue;
if (r.act === 'deny') emit('deny', `Refused: this would ${r.name}.`);
emit('ask', `This would ${r.name}. Allow it this once?`);
}
emit('allow', 'ok');
Save it, then wire it in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{ "matcher": "*", "hooks": [ { "type": "command", "command": "node /abs/path/to/guard.mjs" } ] }
]
}
}
Start a new session for it to load. Test it on something harmless first, for example a command containing .env, and check you get the prompt rather than silence.
The design choice that matters most is having ask at all. Reckless and irreversible are different problems:
Pattern rules are a speed bump, not a sandbox. In June 2026, GuardFall research from Adversa AI tested eleven open-source coding agents and found the command guards in ten of them defeated by old shell tricks: r''m -rf, $IFS in place of spaces, base64 round-trips. The guard above is in that class and so is every denylist of this shape.
That is an argument for two things, not for skipping it:
If you only do one thing, take the credentials out of reach. Most of what makes a destructive action catastrophic is what it can authenticate to afterwards.
GVNR ships these rules, plus spend and rate limits, in one install, available for Claude Code today with other agents coming. It is free. We make it, so weigh that accordingly.
/plugin marketplace add instruxi-io/claude-plugins
/plugin install enforcer-governor@instruxi
It adds what the snippet above deliberately leaves out: a safer rewrite where one exists (git push --force becomes --force-with-lease), rate limits in dollars, agents and errors per minute, your organisation's policy on each matched rule, and a hash-chained record of every decision. The snippet is genuinely enough for the destructive-command problem on its own.