A deny rule is the only boundary you get
· 5 min de leitura · Esta página está escrita em inglês. Os termos que explica são os que se procuram em inglês, e um termo técnico traduzido é um termo diferente.
Everything else an agent harness calls a permission is a judgement about cost. That is worth saying plainly, because the interface does not say it: an approval prompt, a mode named Auto, and a writable-roots setting all look like the same kind of thing, and only one of them is a line the system will hold no matter what.
OHarness has five permission modes. Manual asks before anything that changes state. Accept edits lets file writes through and asks about the rest. Plan refuses every change and reads only. Auto lets edits and routine commands pass and asks about the risky ones. Bypass approves everything.
Four of those five are a dial on how often you are interrupted. The fifth is not a mode at all.
What the classifier is actually measuring
Auto is the mode most people settle in, so it is worth knowing what decides. The classifier behind it measures blast radius, not intent: rm, sudo, git push, kubectl, npm publish, a write to an absolute path outside /tmp. Anything it does not recognise as routine still asks.
That bias is deliberate and it is the only sensible one. The failure that costs something is approving a command that destroys work. The failure in the other direction is being asked about one more ls, which costs a keystroke. A classifier tuned to be clever about the second is a classifier that will eventually be wrong about the first.
But a conservative classifier is still a classifier. It reads a command and guesses. bash -c 'python -c "..."' is one line of shell whose blast radius is not visible in its arguments at all, and no amount of pattern work fixes that, because the information is not there to read.
The rules that are not guesses
Under the modes sit three lists, matched against both a tool name and a Tool(argument) form:
{
"allow": ["Bash(npm run test:*)", "mcp__github__get_*"],
"ask": ["Bash(git push*)"],
"deny": ["Bash(rm -rf *)", "write(.env*)"]
}
deny beats everything, including bypass mode. That is the one sentence in the permission system a person can rely on absolutely: there is no mode, no approval, and no "always allow" that reopens a path a deny rule has closed. Everything above it — the modes, the classifier, the prompts — is scaffolding around a question of how much you want to be asked.
Approvals persist, and where they persist matters. "Always allow" is written to a settings file in one of three scopes: ~/.oharness/settings.json for a personal habit, .oharness/settings.json for something that belongs to the repository and can be reviewed in a pull request, and .oharness/settings.local.json for a per-checkout choice that stays out of Git. Denials accumulate across all three and are never overridden by a narrower scope, because a project has to be able to forbid what a user allowed globally.
What gets written is the most specific pattern that covers what you approved. Approving Bash(npm test) must not quietly grant every Bash(...), and approving one connected-app tool must not grant the rest of that app, let alone every app you have connected. A permission system that widens itself while you click through it is worse than no permission system, because it produces a settings file that reads like a policy and behaves like a bypass.
Writable roots are a policy gate, not a sandbox
workspaceMode is read-only, workspace-write or full-access, and the check runs before the approval prompt — offering someone a choice that policy already forbids is a good way to teach them that the prompts are decorative.
It is worth being exact about what that check is. It inspects tool arguments and refuses paths outside the allowed roots. It does not confine the process. A shell command can reach any path you can, and the python -c case above defeats argument inspection entirely. Real confinement is an operating system feature — a container, a VM, a user account with fewer rights — and a harness claiming otherwise is selling the feeling of a boundary.
So: a guard against mistakes, which is most of what goes wrong. Not a guard against a determined command, which is the thing people assume they bought.
The trail is the part you will want later
Every permission decision is recorded beside the transcript that produced it, as two lines paired by id:
{"kind":"decision","id":"dec_9f2c","tool":"bash","behavior":"ask","principal":"Bash(npm test)"}
{"kind":"answer","id":"dec_9f2c","outcome":"allowed","remembered":"Bash(npm test)"}
The decision is what the rules said. The answer is what the person said. An ask with no answer means the process stopped while the question was still on screen — which is exactly the state you cannot reconstruct from memory a week later. outcome: "unavailable" is the fail-closed case: nothing was able to ask, so the call was refused with nobody in the loop.
It is on unless you turn it off, because the trail costs a few short lines per turn and the state where it is missing is reliably the state where it turns out to have been wanted. It stays on the machine that made the decisions: sync pushes transcripts and artifacts and deliberately not this, and it is deleted with its session.
Writes to it are fire-and-forget but serialised. That is a trade made on purpose — recording must not delay or fail a tool call, so a failed write is a warning in the log rather than a refused command. This trail describes what happened. It does not gate it. Which is, once more, the same distinction: know which of your controls are judgements and which one is a boundary.
OHarness is an open source agent harness, MIT licensed. The permissions reference has the full table of modes, scopes and rule forms, and the architecture reference covers where the permission layer sits relative to the model and the tools.