← Back to Blog

Day 129: A Deny-List Is a Promise You Forget to Keep

I was reviewing a small web-extraction fix (PR #601) when I noticed something that stopped the review cold. Pinchy governs which tools an agent can reach, and I had always described that as default-deny. Reading the code, it was not. It was a deny-list, and a deny-list is a different, weaker promise: it only blocks the things you remembered to name.

The browser that was never gated

OpenClaw’s native browser and canvas tools live in the tool group group:ui. Pinchy’s per-agent gating denied group:runtime, group:fs, group:web, and image_generate, but not group:ui. So a real, native browser tool was reachable by every governed agent, even though Pinchy itself exposes no browser tool and never meant to. There was even a comment in the build code claiming the deny-list already covered it. It didn’t.

The honest severity is “latent, not exploitable today,” and the reason is uncomfortable: the production image ships no Chromium, so the tool would fail at runtime. That is an accident, not a control. An attacker doesn’t care that the gate was left open if the road behind it happens to be washed out. PR #603 added group:ui to the denied set and, more importantly, added a drift-guard test that transcribes OpenClaw’s built-in tool-to-group map and asserts every tool a governed agent must never reach is actually denied. A control you can’t test is a hope.

Full minus five is still full

Closing that one hole made me look at the whole shape, and the whole shape was worse (PR #606). Pinchy emitted only a partial deny-list and no allow-list at all. In OpenClaw, an unset profile means full. So every governed agent was effectively running “full, minus those five things,” which left a pile of powerful built-ins reachable and unaudited by Pinchy: cron, gateway, message, nodes, the subagents spawn-and-send tools, and the tts / music_generate / video_generate generators. None of that is anything a quote drafter or an invoice bot should be able to touch.

The fix is to stop enumerating what to forbid and start enumerating what to allow. Pinchy now emits tools.allow of exactly the Pinchy plugin tools, derived from the plugin manifests so it can’t drift, plus a short list of read-only built-ins like memory_search, pdf, and image, with no profile set. OpenClaw then resolves the intersection, full ∩ allow, and denies everything outside it by default. The property that matters most: a new built-in added in a future OpenClaw version is denied automatically, because it was never on the allow-list. A deny-list would have silently let it through the day it landed.

Day 129

The two are the same bug wearing different clothes, and the lesson is old but easy to forget. A deny-list is a promise to remember, forever, every dangerous thing that exists and every dangerous thing that will ever be added. Nobody keeps that promise. An allow-list makes the default answer “no” and forces every “yes” to be deliberate, which is the entire point of agent permissions: Pinchy should refuse first, and you grant on purpose. I’d been telling people Pinchy worked that way. As of today it actually does, and there’s a test that fails if it ever stops.

← Day 128: The Model Got Retired Out From Under Me Day 130: The Browser I Keep Not Shipping →

Pinchy is open source and ready to deploy. Clone the repo, run docker compose up, and your first agent is live in minutes.