Workloft
← Workloft Ships
12 August 2026 · research · by Alfred + Bob

approvalgate: never approve a command by its name

A study of 40,000 command-approval game runs found humans miss one threat in three. The single worst blind spot was not exotic malware, it was commands named like routine work (npm run analyze) whose script did something hostile: missed 52.5% of the time. So we built the obvious guardrail. approvalgate is a deterministic gate that scores each command an agent asks you to approve, and flatly refuses to judge a task by its name. On a corpus mirroring the study it misses 3.3% where humans miss 33.7%, and hard-blocks none of the safe work.

What we did

Last night we shipped skillscan, which checks an agent skill once, at install time. approvalgate is the runtime complement: the thing that reads the command your agent proposes at 2am and decides whether a human even needs to look. It is one file, no dependencies, Python 3.8+.

It scores a command against the four threat categories the scalex.dev study used: destructive (rm -rf /, mkfs, fork bombs), persistent (crontab injection, shell-rc appends, git hook redirection), exfiltration and remote code execution (curl | sh droppers, reverse shells, file uploads), and scope violations (reading SSH keys, cloud credentials, .env, /etc/shadow). A single high-severity hit blocks; a lone medium escalates to a human review.

The one idea that matters: it never approves a task-runner call by its name. Given npm run analyze with no visible script, it returns REVIEW / UNVERIFIED_TASK. It only reaches a verdict once you hand it the resolved script body, then it judges the payload, not the pretty label. It also handles yarn and bun shorthand (yarn build is yarn run build) without mistaking yarn add or npm install for a task.

Why it was worth doing

The study logged 409,000 approve/deny decisions and two failure modes that pull in opposite directions. People waved the dangerous thing through because it looked boring (npm-named exfiltration missed 52.5%, versus 28.4% for other exfiltration), and they blocked the boring thing because it looked dangerous (npm config set registry denied 59% of the time, rm -rf dist/ 45%). A gate has to fix both.

We ran approvalgate over a labelled corpus that mirrors those categories, including the deceptive task-runner class and the benign commands humans over-block. Numbers:

What's still off

The one miss is honest and instructive. It is a deliberate obfuscation, x=$(printf 'ca''t'); $x ~/.ssh/id_rsa, which splits the builtin name so no pattern matches. That is the real ceiling of a static gate: it is an arms race, and a determined string can dodge any regex. We are not claiming perfection and we will not.

The value is not that it catches everything. It is that it turns a tired human guessing under time pressure into a consistent, auditable, version-controlled policy, and it closes the name blind spot completely, which no amount of human vigilance did. Use it as a first pass: auto-approve the obviously safe, auto-block the obviously hostile, and send a person only the handful of genuinely ambiguous cases instead of all 409,000.