Three bets on safety
pi bet on breadth plus in-process hardening. deepseek bet on layering, and has no permission layer at all. nerv bet on the question a tool is made to ask. Measured against the Rust ports, which is where these layers are comparable at all.
The shape of each
| pi | deepseek | nerv | |
|---|---|---|---|
| permission model | 3 global modes, allow or deny, not persisted | none | 4 verbs × 5 widths, with a session ledger |
| dangerous-command analysis | a shell-out, or ten in-tree danger classes | none | none |
| OS sandbox | none, in 527k lines | none | bwrap, through the process transport |
| secrets masking | mask, restore for the human, re-mask on the way out | none | masked by value before anything is journalled |
| path confinement | O_NOFOLLOW, re-stat the descriptor, reject if it moved | lexical prefix, no canonicalise | lexical normalise, then prefix |
| tool declaration | one implementation per tool — 38 of them | one per tool | one per transport — five |
One implementation per transport
That last row is nerv's one genuinely better idea, and it is architectural rather than stylistic: adding a way to reach a tool cannot add a way to run one. Every cross-cutting concern lives at a single funnel, and there is exactly one place to put it — the output cap, the schema check, the argument repair, the secrets mask.
pi's equivalent of the output cap is seven hundred lines of spill machinery inside its tool file, with its own redaction engine, because pi's tools each produce output independently. deepseek has no cap at all.
The claim, and its limit
nerv's command transport runs a program with an argument vector built
from the call. No shell, so a value containing ; or $(…) is one
argument, verbatim. It is the only one of the three where the default way to declare a new
tool cannot be command-injected.
That is a claim about the transport, not about the shipped tools. The shell tool
nerv actually ships is a Lua declaration that interpolates the model's string into a
compound sh -c. It is a shell string end to end, exactly like pi's and
deepseek's. Both facts belong in the ledger.
Why five widths
One request can be answered at several widths, because how much you want to grant depends on what was asked. Saying yes to this exact command, once and yes to anything under this directory, forever are both reasonable answers to the same prompt — and a system offering only one of them will be answered carelessly.
pi has three global modes and no persistence, so “allow always” is not expressible; the pressure that creates is toward the mode that stops asking. deepseek asks nothing. The widths are set out here.
What auditing this found
The design being the best of the three did not stop the implementation being weaker than it reads. Three holes were verified by running them, not by reading:
- A directory grant compared unnormalised paths, so a grant on
workcoveredwork/sub/../../secret. Fixed: paths are normalised before the action is built. - Confinement was silently dropped whenever anybody was watching. It applied only to headless sessions — the exact case where nobody is there to catch anything. Fixed.
- A grant on
gitcoveredgit status; rm -rf /, because the program was the first word and the command line sat in the rest. Fixed: a command carrying a shell metacharacter is covered by no program grant, so it is asked about instead.
git command” stops at the first ; — and the way it was found is the argument for the whole exercise: pi's equivalent check is about fifteen lines and six tests, and reading it is what exposed the gap.Still open
- Nothing analyses a command before running it. nerv decides whether an action is permitted, not whether it is wise. pi has ten danger classes.
- A grant on
shis an unbounded shell by construction, not by bypass — which is what the person asked for, and worth knowing they asked it. - Automatic approvals are not audited. A ledger hit returns quietly, so “what did this session do under standing grants” has no answer. pi audits every decision, including the ones nobody saw.
- File opens are not hardened against the race. Lexical normalise, then read. pi
canonicalises, opens with
O_NOFOLLOW, re-stats the descriptor and refuses if it moved.