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: '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.

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. 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:

  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 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.

An Instruxi solution. Verified against Enforcer Governor v2.8.0 on 25 September 2026. Runs in Claude Code today; other harnesses are coming. Nothing leaves your machine until you sign in to an Enforcer workspace. Next: Install, Stop an agent overspending, Receipts. Enforcer, the identity and authorization service, is at enforcer.instruxi.dev.