GVNR

Stopping an AI agent running a destructive command

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.

Do it before the tool runs, not at the API

Two places you can intervene, and they are not equivalent.

At the APIBefore the tool runs
Sees the model callyesyes
Sees a shell commandnoyes
Sees a file readnoyes
Can refuse before anything happensno, the call is already going outyes

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.

The contract, which has one trap in it

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.

A hook you can paste

This is self-contained. It needs nothing installed.

#!/usr/bin/env node
import { readFileSync } from 'node:fs';

const RULES = [
  { name: 'pipe the internet into a shell', 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.

Three decisions, not two

The design choice that matters most is having ask at all. Reckless and irreversible are different problems:

What this cannot do, stated plainly

Pattern rules are a speed bump, not a sandbox. Published research testing open-source agent guards found most of them defeated by trivial shell obfuscation: 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:

  1. Against accidents, it works. The overwhelming majority of destructive agent actions are mistakes, not attacks, and a pattern rule catches mistakes.
  2. Against an adversary, isolate instead. Run the agent in a container or VM with no credentials in reach and a mounted working copy. Then the blast radius is bounded by construction rather than by your regex. Put the pattern guard on top of that, not instead of 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.

If you want this maintained rather than pasted

GVNR ships these rules plus spend and rate limits in one local process, free and self-hosted. We make it, so weigh that accordingly.

npx --yes enforcer-governor install-hook

It adds what the snippet above deliberately leaves out: rate limits in dollars per minute, agents per minute and errors per minute, and a hash-chained record of every decision. The snippet is genuinely enough for the destructive-command problem on its own.

Powered by Instruxi. Verified against the GVNR source on 15 September 2026. Free and open, runs on your own machine, nothing is sent to Instruxi. Related: GVNR · Stop an agent overspending · Block dangerous commands · Quickstart · Configure · Receipts · How it compares · Live demo · Enforcer. Enforcer, the identity and authorization service, is at enforcer.instruxi.dev.