E/2D8

The shell needs a separation of concerns

A shell is one program that reads text, works out what it means, decides whether to run it, and runs it, with no boundary between any of those jobs. Its only interface was built for a person at a terminal, so every machine that drives one impersonates a person to do it, and every capability anyone has wanted from it has been rebuilt privately on top of a string going out and a byte stream coming back. Separating those concerns puts an interface where a machine can address it, a representation where a command’s meaning is explicit before anything happens, and a record of what actually ran. Policy over that representation is one of the things that follows from the separation rather than the reason for making it.

part one

The shell has a lot of history

The shell’s interface was designed for a person at a terminal. Prompts, line editing, job control, output formatted as a stream of characters to be read and interpreted: every part of it assumes someone on the other end who can look at what came back and judge what it meant. That assumption held for decades because it was true. However, most of what drives a shell is no longer a person, and a program that needs to drive one finds no interface built for it, so it borrows the one that exists, allocating a pseudo-terminal and impersonating the human the interface was expecting.

None of that is an argument against the interactive shell. That interface is the right one for the person it was built for, and preserving it is a requirement rather than a concession. The argument is that it is now carrying a second job nobody designed it for, and that the second job needs an interface of its own.

That interface is the visible half of something structural. A shell is a single program that reads text, works out what it means, decides whether to run it, and runs it, with no boundary between any of those jobs. Software separated those concerns nearly everywhere else it met them, and the seams it opened became the places new capability attached: a compiler’s front end from its back end, a database’s parser from its execution engine. The shell never made the separation. What follows from that is not only that judging a command before it runs is difficult. It is that there is nowhere to attach anything.

Every agent that touches a shell today does so by writing a string and hoping. The harness assembles command text, hands it to an interpreter it does not control, and reads back whatever bytes emerge. The industry has seen this pattern before. It is how SQL was driven before injection forced the parameterized query, and it is paying the same tax again, this time with an operator at the keyboard that types faster than anyone can review.

The tax has two sides. On the input side, invoking a shell means constructing a program by string concatenation: assembling command text and hoping the receiving grammar agrees. Shell invocation never got its parameterized form, and the consequences are visible in shipping code. On the output side, the shell’s entire result interface is an exit code and a byte stream, with no structured outcome, no state delta, and no distinction between a program’s answer and its noise. Exit code 1 means no lines selected from grep and something went wrong from nearly everything else, and nothing in the interface tells them apart. All programs currently drive a shell the way one scrapes an HTML page in the absence of an API.

Inspecting the three leading agent harnesses finds roughly 48,900 lines of Rust in one dedicated to driving, containing, and gating the shell; a 136-entry dictionary of command arities in another, hand-maintained as source code because nothing specifies it; and in the third, a shell snapshot assembled from thirty echo statements, because a fresh child process loses every variable, option, and function between commands. One of them carries a 42-line module whose only job is unwrapping its own bash -lc wrapper so it can recognize an approval it already granted. None of that machinery expresses any tool’s intent. It is what every team pays, separately, because the boundary each has to build against guarantees so littleR/9B1.

A string is not a program

The deeper problem is formal. Shell meaning is constructed at runtime by expansion, substitution, and evaluation, so the pre-execution string is not the program that runs. A gate that parses command text and then hands the same text to a different engine for execution has left a permanent gap between what it judged and what happened. Dynamic evaluation (eval, sh -c, xargs, a command read from a variable) is a class no static parse can close by construction.

A command’s text has no fixed meaning until the moment it acquires one, and everything read before that is a forecast.

Two of the three inspected harnesses do exactly this, and both use a real parser, which is what makes the point structural rather than a criticism of anyone’s engineering. The third sidesteps the problem by not needing to understand the command at all: it contains execution in a genuine kernel sandbox. That works, and it pays the price semantic blindness predicts. A sandbox sees a process spawn with no idea whether the argv was a reviewed convention or an improvisation three substitutions deep, so its safe default is too restrictive to use and its usable default blocks routine work. The designed response to a denial, where policy allows, is to retry the command without the sandbox.

The taxonomy is therefore complete, and it is a taxonomy of layers. Before meaning exists, in the string, a gate reasons about a program that never runs. After meaning is gone, in the syscall, enforcement sees operations from which no intent can be reassembled. There is no third position currently occupied by anything. Pre-processing a string, however carefully, can never guarantee coverage of the command that actually runs.

Separation of concerns

A shell today is architecturally flat. One program reads text, expands it, runs it, and writes bytes back, and there is no seam anywhere between the part a person is talking to and the part that does the work. The terminal is not a client of the shell. It is the shell’s only interface, which is why everything that wants to drive a shell has to pretend to be a terminal first.

The seam is less invented than located. Everything that drives a shell already behaves as though one were there: a harness allocating a pseudo-terminal is writing an adapter across it, and the terminal is a client in every respect except the architectural one. No existing implementation respects that boundary, and that is precisely how it becomes visible, since each of them crosses it in the same places and pays the same costs for doing so. The split is emergent rather than proposed. What naming it changes is what can be built on it.

Named, the interactive terminal keeps its dialect syntax, its prompt, its line editing and its job control, and becomes one component among several: a user interface, and a legacy-compatibility surface for the dialects people already write. It stops executing anything. Depending on which dialect it has been configured for, it delegates across a machine interface to an execution environment that takes structured input and returns structured output. Presentation sits on one side, execution on the other, the way the notebook split from the kernel.

Nothing here needs inventing. The separations software makes everywhere else are simply absent from the one program everyone runs.

Constraint is not a new wish

The desire to bound what a shell will do is as old as multi-user Unix, and its instruments are old enough to be instructive. Restricted shells forbade changing directory, setting PATH, naming a command with a slash in it, or redirecting output. chroot bounded what the filesystem could show. sudo arrived with a policy file that enumerates ahead of time which principals may run which commands, denying everything not listed. That last one got the form right: deterministic, authored at rest, reviewed before the work it governs, deny by default. It got the object wrong. A sudoers entry names a command, and permitting a command by name permits everything that command can be made to do, so a rule admitting find admits find -exec and hands back a shell.

That is not an isolated defect. It is the pattern, and it survives every attempt to correct it by binding to something better. A restricted shell is escaped by any program it still permits that offers a shell of its own, a limitation bash’s own documentation concedes. chroot was never a security boundary and says as much in its own manual. The mandatory access control systems that followed did bind to operations rather than to names, which is the obvious repair, and it is not enough: AppArmor records that forbidding a rename is defeated by a copy, and SELinux that deleting one file and deleting every file are the same permission checked a different number of timesR/7A6. Every one of these mechanisms decides a single request against a single object. None of them holds anything that represents what a sequence of permitted requests adds up to.

None of this is a failure of effort. Exhaustive filtering of shell commands has been attempted continuously since multi-user Unix and has never been achieved, and the reason sits in the interface rather than in the attempts. What the shell hands a caller is a string and a command name. An effect is not among the things it offers, so a control built over it has nothing to bind an effect to, and each one binds to whatever is available instead R/7A6. This is an unsolved problem with a long record of serious attempts, not a solved one being applied carelessly.

Agents did not change that. They make it worse in two ordinary ways. The instruments assume a rate a person can sustain, and commands now arrive as fast as a model produces them. They assume someone is present who wrote the command and can be asked what they meant by it, and an agent runs under the credentials of whoever launched it, so the party authorized is not the party that composed the command and the work proceeds unattended. Neither is a new failure. Both exercise an old one harder, and what agents changed is how much now rests on a problem nobody has solved. The wish has been constant for as long as the shell has had more than one user. What has never been available is a policy that can speak about the result.

What the approval dialog is actually asking

Since nothing in the path can supply that, the decision goes outward, and two answers are in production: ask a person, or contain the process. The dialog is the first, and it asks for more than it appears to: string-to-effect prediction, performed by a person, statelessly, at whatever rate the system produces commands.

fig. 1
Dangerous commands caught under interactive approval, after Anthropic’s published figures (2026): human reviewers decay from roughly 17% to 5% across fifty prompts, while their model classifier holds near 89%, flat.
Human review
Model classifier
CATCH RATE
% OF DANGEROUS
COMMANDS CAUGHT
first prompt25 prompts50 prompts25%50%89%

The measurements are published. Users approve 93 to 97% of agent permission prompts. In a controlled study of 1,053 professional testers, humans caught 13.6% of dangerous commands, about 17% early in a session and roughly 5% after fifty prior prompts. Every major agent tool grew a skip-permissions mode, and its heaviest users run in it. A control whose equilibrium state is off is not imperfect; it is bankrupt.

None of that is specific to terminals. Browser interstitials are clicked through at 70%, operating-system elevation prompts approved at 91%, mobile permission requests granted at 84%, and a terms-of-service study recorded 97% consent to a policy assigning the reader’s first-born child as payment, with 98% missing the clause. Hospitals named this failure two decades before software borrowed it, and the vigilance literature supplies the mechanism: in visual search, observers miss 7% of targets when targets are common and 30% of the same targets when they appear on 1% of trials. Rarity alone quadruples the miss rate. A reviewer whose dangerous case is rare is a reviewer optimized, by their own perceptual system, to not see itR/5C2.

A second study the same month adds a finding about the unit rather than the reviewer. Reviewing agent commands, roughly two thousand participants missed obviously destructive commands 11.7% of the time and exfiltration or code execution 33.4% of the time, so calibration runs inverted with respect to consequence. More pointedly for this argument, an agent gated on command execution could equally have edited package.json, planted code in build.js, or modified a file under node_modules, none of which reaches the gate at allR/6D2. Gating command execution while permitting arbitrary file writes gates the wrong object, and a runtime that reads resolved operations and classified effects sees the write as readily as the invocation.

The model classifier that now replaces that dialog by default is a real advance, and its published ceiling is instructive: a miss rate near one in six on real overeager actions, with a primary failure mode of consent scoping, in which the classifier finds approval-shaped evidence in the conversation and misjudges whether it covers the blast radius of the specific action. That is not a capability gap that better models close. It is a missing data structure, because the classifier infers at runtime, from conversational context, the fact that an explicit grant would simply record. And because its miss rate is flat rather than decaying, residual risk is linear in volume: as agents multiply actions by orders of magnitude, a constant percentage becomes a growing absolute count. Ninety percent judgment applied to a production database is not a score. It is a frequency at which the database is dropped. Dropping a production database is acceptable zero percent of the time, and a probabilistic gate cannot reach zero, because a rate is the only thing it can produce. Raising the percentage is not a path to the answer; it is a path to a smaller number that is still not zero.

A rate is the only thing a probabilistic gate can produce. The acceptable rate for dropping a production database is zero.

What a sandbox bounds

Containment is the industry’s other answer. A sandbox bounds the region an action can reach. It does not establish what the action means, which is why its controls are expressed as extents rather than as operations. The primary control is filesystem scope, and scope trades directly against usefulness: the narrower the region, the less work the agent can do inside it. What results is a configuration layer, authored per project and maintained as the project moves, describing which paths are in play. That configuration determines where damage can occur. It determines nothing about what happens inside the boundary, and a destructive operation on a permitted path is a permitted operation.

The next move is the old jail technique of restricting which binaries exist at all, which means maintaining a separate tool environment for every contained context. It holds exactly as long as nothing inside that context can extend it. One binary able to fetch and execute arbitrary code collapses the arrangement: curl piped into a shell interpreter installs whatever it likes, inside the boundary, in a single line. The restriction does not fail loudly when that happens. It stops being true.

Both controls arrive as configuration, and the person who authors it is the person it is blocking, at the moment it blocks them. That is the ad hoc decision again in a configuration file: one perspective, under time pressure, with the work waiting on the answer. The friction measured earlier applies unchanged, and a control that costs something and blocks work believed legitimate becomes a candidate for removal. The measured response to permission prompts was a skip-permissions mode, and the designed response to a sandbox denial, in one shipping harness, is to retry the command without the sandboxR/9B1.

Writing that configuration earlier would not repair it, because the dial is extent, and extent trades directly against usefulness. Widening the region so the work can proceed widens what can be damaged, by the same motion and in the same proportion. The argument is not that containment is worthless. It is that containment is coarse. It constrains where a process can reach and never what an operation means, and it reaches the user as one more thing standing between them and the task.

The default configuration is where this becomes a transfer of responsibility. A setting tight enough to be defensible cannot do the work the agent was installed to do, so the first thing done with it is to widen it, and the widening happens under exactly the pressure that makes it careless. The tool was acquired to finish something. Every denial stands between the person and finishing it. What they widen, they then own.

The result can be described as safe, accurately, on the strength of a default that almost nobody keeps. Usefulness sits on the other side of loosening it, and the loosening is performed by the person who needed the work done rather than by the party that chose the arrangement. The safety is the vendor’s to claim and the exposure is the user’s to have created, and nothing in the arrangement requires anyone to have intended it.

part two

Architecture informs capability

There is exactly one moment in the life of a shell command when its meaning is explicit: after expansion, before effect. Upstream sits the string, a program not yet constructed. Downstream sit the syscalls, where intent has already decomposed. Everything that follows comes from placing a runtime at that moment and never leaving it.

What binds there has to be deterministic. A rule that resolves the same way on every evaluation, read against resolved operations and classified effects rather than against text or syscalls, is the only construct that returns the same answer as often as it is asked. That is what makes zero a reachable value rather than an aspiration. And the rules have to be authored and reviewed before any of the work they govern begins, out of the workflow entirely, because judgment solicited in the middle of a task is precisely the judgment that decays. Rules are written, reviewed, and versioned the way code is, at rest and under scrutiny; execution then consults them and does not ask.

Concretely, the proposal is a shell interpreter that reads more than one dialect and generalizes them into one shared core, coupled to a rule language evaluated after the parse and before execution.

Behind the human interface

The machine interface is the durable half, and it is the one agents should be addressing. An agent driving it needs no terminal emulation, because there is no terminal in the path; PTY emulation becomes a service for programs that genuinely require it rather than the mandatory adapter every caller pays for today. And because the seam is a real interface rather than a byte stream, other things can stand in front of it. An MCP server is a client of it. So is an interface as ordinary as HTTP. So is the interactive terminal. So, for that matter, is a container build file, whose every build step is already a shell command run by something that had to arrange its own execution. None of these is a new appetite. Each names a way of driving a shell that the industry already wants badly enough to have built by hand, separately, on top of a byte stream. A formalised interface makes them presentations of one execution environment instead of that many reimplementations of a shell.

Past the interface, the structure is a compiler’s. Each supported dialect gets its own lexer and parser frontend, and each frontend lowers that dialect into one intermediate representation, on the pattern LLVM established: many source languages, one IR, one backend. The IR is where a program stops being bash or zsh or fish and becomes resolved words, typed operations, and classified effects. Dialect differences are absorbed at the frontend, where they are a parsing problem, instead of propagating into everything downstream.

The shell is not a program in the sense Unix means. It is a language implementation, and the architecture of that kind of program has evolved out of decades of learning.

The decomposition is the ordinary one. A dialect is lexed and parsed into a tree, the tree is lowered into the shared representation, and that representation is what everything downstream reads. What the tree holds is references rather than values, which is how trees work in any language with a resolution phase: a name in the tree is a name, and what it denotes waits for an environment.

That layer is where a program is comprehensively understood, stated once and identically whichever dialect it arrived in, and what people want from execution follows from having that rather than having to be designed on top of it. Enforcement is one of those things. So is a traceback: with the structure in hand, a failure can be reported through the functions, sourced files, and subshells it happened inside, which is something the shell has never handed back, because an exit status and a byte stream have nowhere to put it.

Fidelity to a dialect does not survive lowering unaided, so what will not generalise travels beside the dialect as sidecars rather than being pushed into the shared core: the behaviours it requires at execution time, applied before the backend runs the work, and the conventions by which it scopes state at parse time, which differ from one dialect to the next. The backend itself generalizes what any shell interpreter has to provide: a variable store, a process layer, redirection, job control, and the trace, which is a truthful structured record of what each command actually did, the argv that really ran, the state that really changed, the effects that really occurred, attributed to the actor that caused them. It knows nothing about which dialect produced the program it is running.

None of those interfaces had to be designed as interfaces. They fall out of the layering, because each layer carries its own granularity, its own delegation, and its own purpose, and a boundary between two such layers is already an interface whether or not anyone names it. The frontend boundary yields dialect support. The IR boundary yields policy, filtering, and audit. The seam yields agents, MCP, HTTP, and the terminal itself. A layered system absorbs a requirement it did not anticipate by adding a client or a reader; a flat one absorbs it by growing.

A computed record of action

The trace is the shell’s record of what it actually did, and no shell has ever kept one. Scrollback is not a record. It is a transcript of whatever happened to be printed, in the order it was printed, and it cannot be queried, diffed, replayed, or handed to anyone as evidence of what occurred. That absence has been normal for so long that it reads as a property of terminals rather than as something missing from them.

A shell can print the commands it ran. It has never been able to report what they changed.

What the record is worth depends on who reads it. Read by a person, it is explanation. Read by an agent, it is environmental awareness: the working directory changed, these variables were set, this command failed with this status. It replaces the opening ritual of git status and pwd that agents perform today because they are blind, and that blindness is structural rather than incidental, since two of the three inspected harnesses run every command as a stateless child process. Read by a team, it is the raw material of runbooks. Read by review, it is audit. Read by a policy engine, it is enforcement. One record, many consumers, which is why each new capability arrives as a new reader of existing organs rather than as a new subsystem.

Nothing in that list is taken from the person at the terminal in order to give it to the agent. The terminal keeps working as it always has, and the record is new for both of them.

The system as composed

The pieces have been introduced where each was needed. Stated together, with the responsibility belonging to each, the whole is small enough to hold in view.

fig. 2
The system as composed. Clients meet one machine interface; per-dialect frontends lower into a shared representation; policy reads that representation; the backend runs what survives and knows nothing of the dialect that produced it.
frontendsno matchpermitted

interactive terminal

machine interface

agent

other protocol

sh

bash

zsh

fish

intermediate representation

audit log

rule language

refused

execution backend

dialect sidecar

trace

  1. Clients. Anything that drives execution: the interactive terminal, an agent, an MCP server, an HTTP caller. Each is a presentation of one execution environment, and none of them is the architecture.
  2. The machine interface. The seam. Structured invocation inward, structured outcome back, with no unframed string crossing in either direction.
  3. Frontends. One lexer and parser per dialect. Dialect differences are absorbed here, where they are a parsing problem, rather than propagating downstream.
  4. The intermediate representation. Resolved words, typed operations, classified effects. The one form every dialect lowers into, and the only place where a command’s meaning is explicit.
  5. The rule language. Policy read against that representation. Deny by default, inheritable, authored and reviewed before the work it governs begins.
  6. Sidecars. Dialect-specific semantics that do not generalise, carried beside the dialect they belong to: parse-time scoping conventions for the frontend, and execution behaviour applied before the backend runs anything.
  7. The execution backend. Variable store, process layer, redirection, job control. It knows nothing about which dialect produced the program it is running.
  8. The trace. The structured record of what actually happened, returned across the interface, read by agents as awareness, by people as explanation, and by audit as the permitted decision and the executed result set side by side.

That is the system. Each layer does one job, hands its result to the next, and needs to know nothing about how the others do theirs, which is the separation the shell has never had. What it amounts to is a shell that can be driven by something other than a terminal, that holds a complete account of what it has been asked to do, and that keeps a record of what it did.

part three

In practice

The system is bounded from underneath, and deliberately so: no virtualization, no sandbox, no interception of raw system calls. Understanding what an operation means and bounding what a process can possibly do are different jobs, and conflating them is how a system ends up with a sandbox that half-understands and an interpreter that half-contains. Where segregation is required, this runs inside it and composes with it, never in place of it.

Followed outward, that boundary reaches a limit worth stating plainly: a fully contained agent needs none of what this essay proposes. Inside a zone with no actuator reaching past it there is nothing to gate, and the agent’s freedom costs nothingE/71B. What this addresses is the world as it currently is, in which agents work inside privileged environments that were never given a boundary to cross.

The approval problem, inverted

Work that can be undone runs and is reviewed afterwards. The rest interrupts once, and the answer it gets becomes a rule that holds.

Bound at this layer, the approval problem from part one inverts. Policy attaches to resolved reality: actual executables, canonical paths, classified effects. Contained, reversible operations execute freely and are reviewed after the fact against the trace, which reviews what happened rather than what was proposed. The irreversible minority pauses mid-execution and presents the human with the resolved operation rather than the string. And every decision compiles into policy. An approval becomes an allowlist entry, a denial becomes a rule that carries its reasons, an escalation becomes a runbook, so interruptions acquire a half-life instead of a flat rate.

This also resolves the policy-unit problem that the inspected harnesses each answer differently and each answer wrongly. Claude Code stores approvals as a command prefix, which is too broad, since blessing git checkout blesses arguments never reviewed. Codex CLI stores an exact canonicalized command, which is too narrow, since approving a single invocation teaches the system nothing about its siblings. OpenCode maintains a curated arity dictionary, splitting the difference by hand at the cost of encoding semantic knowledge as source code for 136 commands and counting. All three are reaching for a generalizable unit, and none of them can have one, because the unit is semantic and no granularity of string carries semantics. A parameterized operation family with typed constraints and a declared effect profile is available only at the layer where meaning is explicit.

The shape of a policy

A policy is written in the rule language and evaluated against the intermediate representation, after a command has been resolved and before any of it runs. What a rule sees there is what every dialect lowers into: resolved words, typed operations, classified effects. It matches on what a command does rather than on how it was spelled, which is what lets a rule be specific without being brittle.

Binding policy at this layer is not a way to narrow where agents may operate. The intent runs the other direction: an agent should be able to work in every shell-interpreted environment a person actually needs it in, including the ones where dropping a production database is the failure being designed against. That calls for policies with the resolution to permit nearly everything and refuse the specific thing, which is a different artifact from a permission attached to a command name.

One shape it takes is the runbook, implemented rather than written down. A developer or an organization publishes a task, and the task is reviewed before it is used, by one or more people and by one or more agents. What results is a verified artifact for a repeatable goal, and the range is wide: work done many times a day belongs in the same corpus as work done twice a year, and the twice-a-year work is where institutional memory usually fails. Each artifact carries its intent, the occasions on which it was applied, and the record of its own past executions. That last part is what makes fidelity checkable, because a runbook whose executions have begun to diverge from its record has drifted, and the drift is detectable without anyone noticing it by hand. Knowledge shared this way reaches the organization’s agents by the same route it reaches its people, since both read the artifact.

The other shape is a policy for a single tool. The surface of a shell tool is large, idiosyncratic, and unevenly known, and the arity dictionary quoted earlier is one private approximation of it, hand-maintained inside one product because nothing publishes it. That work is duplicated because it is private, not because it is difficult. A policy for ls is close to trivial: enumerate the flags, establish that none of them modify anything, and the artifact is finished. It is worth writing anyway, because it fixes the form. A policy for find is where the form earns its keep, because enumerating that surface is genuine work and it ends in decisions: -delete modifies the filesystem and -exec runs arbitrary commands, and neither fact is visible from the tool’s name or from a prefix rule that permits find.

Policies of this kind already exist. They are simply not readable. One harness inspected here withholds ten find predicates from prefix approval, -delete and -exec among them, and carries comparable guard tables for awk and jq, all of it compiled into a binaryR/9B1. Someone made those calls, and they may well be the right calls. A developer running that tool has no way to learn which effects are guarded, which are not, or on what reasoning, and therefore no way to disagree with any of it. The status quo is not an absence of policy. It is policy nobody outside the product can read, review, or correct.

Collective review changes what the user knows as much as what the system permits. A policy that is published, argued over, and inherited teaches its readers where a tool’s edges are, and find -delete is precisely the kind of edge most people meet by accident. Vetting in the open yields both a better artifact and a better informed audience, and neither is reachable while the reasoning stays compiled into someone’s binary.

Care spent at the moment of decision is care spent too late.

Policies of this kind are inheritable, and they are meant to be shared. An organization holds private policies for its own tools and its own conventions and inherits public ones for everything standard, the way it already handles dependencies. A public policy is vetted by the audience that depends on it, which is a larger and more adversarial review body than any single organization can staff.

The corpus exists for the review surface it creates. A policy that has been read by many people, corrected each time someone finds the flag nobody had considered, and inherited by everyone downstream, has absorbed more scrutiny than a decision made in the moment by one person with work waiting on the answer. That in-the-moment decision is the one the measurements find failing. Replacing it asks nobody to be more careful. It asks that the careful work happen somewhere else, earlier, where there was time for it.

Fidelity to the dialect

The discipline holding it together is fidelity. The runtime adds no dialect, extends no syntax, and improves no semantics. Scripts mean exactly what they mean in the shells they were written for, verified continuously against those shells as oracles by a differential conformance suite. All added power lives above and beside the language, never inside it. That restraint is what separates this from every prior attempt to fix the shell by changing it.

Fidelity of that kind cannot be asserted, only demonstrated. No major dialect beyond the POSIX subset has a definition other than its own implementation, so there is no document to conform to and no way to claim conformance except by running both and comparing what comes backR/B84. That makes the conformance suite the specification in practice, and building it a deliverable of the work rather than overhead beside it.

The limit that arrives with it is the proposal’s own rather than the field’s. A pass rate measures gaps against a finite set of tests, no set of tests covers a dialect, and a claim of near-completeness is unfalsifiable because the denominator does not exist. That is the largest technical risk the work carries, and it is worth naming as a risk rather than a footnote.

It is a hard problem rather than an unsolved one, and the distinction is visible in what is already shipping. The instruments are scattered rather than missing. Oils maintains spec tests recording expected output separately for each shell; brush carries roughly 1,700 oracle-based compatibility tests run against the bash binary; tree-sitter-bash parses four corpora including GNU bash’s own repository and files every input it cannot handle; fish’s Rust port used the previous fish binary as its oracleR/B84. Each was built by a project that needed it for itself, in its own format, against its own oracle. Consolidating them into one differential suite is real work and should be counted as such, but it is consolidation rather than invention.

What has changed recently is the cost of the cases themselves. A differential suite is only as good as the programs it runs, and hand-writing programs that reach a dialect’s dark corners is exactly the slow, unrewarding labour that has kept these corpora small and project-shaped. Generating shell programs mechanically, executing them against the official upstream shell and against the runtime, and comparing observable behaviour was a technique available in principle and prohibitive in practice. It is no longer prohibitive. A system that derives candidate programs from real-world scripts, runs them against the native implementations as oracles, and records every divergence is buildable now by a small team, and it runs continuously instead of once.

This does not manufacture the missing denominator. Generated coverage grows the numerator cheaply and leaves a claim of completeness exactly as unfalsifiable as it was. What it changes is the slope: how fast divergence is found and what it costs to find, which is the variable that decides whether an undertaking like this is tractable at all. The symmetry is worth stating plainly. The capability that makes verifying a shell implementation affordable is the same capability that made the interface urgent in the first place.

The extent of the shell

The edge of this layer is deliberate, and it falls at the language rather than at the route. Anything that is shell is inside it, however it got there. Anything that stops being shell is outside it, whatever called it.

The largest thing outside is another interpreter. A shell that can reach a Python interpreter can reach everything Python can do, and a policy reading resolved shell operations sees python and its arguments, not the program those arguments name. The same holds for every interpreter within reach, node and perl and ruby among them, and for the interpreters embedded in tools that do not present themselves as interpreters at all. The harnesses inspected here are already fighting this at the margins, catching system() inside an awk program and module imports inside a jq one, one tool at a time R/9B1. That approach does not generalise, and this system does not attempt it. Seeing inside a running interpreter would mean enforcing at the kernel, which this system has already declined to do. What policy still decides is whether an interpreter runs at all, under which arguments, and reading which paths.

What is inside is wider than it looks, and the route is what people expect to matter. Shell typed at a prompt, shell read from a file, shell produced by eval, shell fetched over a network and piped into an interpreter: all of it is shell, and all of it is parsed, lowered, and evaluated against policy the same way. curl https://example.com/install.sh | sh is the line that trivially defeats a jail, and at this layer it is unremarkable. What arrives is shell arriving at a shell execution environment this system owns. Nothing about coming over a network exempts it, and nothing about being piped into an interpreter conceals it. Shell nested inside shell stays visible at any depth. What that script installs and then invokes is a program this layer does not read, which is the same edge again rather than a new one.

Adoption

Adoption has two sides. What already exists has to keep working without anyone being asked to change it, and what could work better has to be reachable without a rewrite standing in front of it. Neither is a matter of persuasion. Both are properties the design either has or does not. Nothing here is unprecedented either: execution environments reached through a protocol rather than a terminal already exist, the notebook and its kernel among them, and shells carrying structured values instead of bytes have been built more than once. What is untried is getting structure without asking anyone to write a new language for it.

Supporting the products that already integrate with shell interpreters is therefore not a separate burden. The interactive interface is being preserved for people regardless, and today’s harnesses drive that same surface, because impersonating a person is how they work at all. The legacy adapter is the human interface with a machine on the other end, which is what it has always been. Keeping one keeps the other.

What that adapter has to cover is a question to be measured rather than assumed, which is why the harness interface was inspected before the runtime was designed. An agent does not choose a shell the way a person does. Its harness resolves an executable, invokes it under a fixed argument contract, and reads back an exit status and a captured buffer, and the particulars differ at every step: Claude Code validates a shell by testing its path for a substring, Codex CLI invokes with -lc, OpenCode cascades to /bin/shR/9B1. None of that is negotiable from the outside. Supporting it means presenting an emulated interface aimed at the subset those harnesses actually exercise, rather than a cleaner one.

One asymmetry shapes the build order. Models write bash, whatever a person keeps as a login shell, and the harnesses reinforce it by requiring a bash- or zsh-family shell to begin withR/9B1. That settles which frontend comes first, and it puts the dialect agents speak most fluently and the one with no definition of its own in the same placeR/B84.

That costs the person nothing, because the dialect is a preference rather than a commitment. A person keeps the shell they prefer and changes it later without consequence downstream, since the policies, the runbooks, and the trace are written against the representation rather than against a syntax. A corpus survives a move from bash to fish unchanged. Two actors on one session need not agree either: the person works in the dialect they think in, the agent emits the one it was trained on, and both lower to the same thing before anything decides what they mean.

That emulation is a stopgap. Its purpose is to remove the friction of adoption, so that nothing has to change on the caller’s side for the runtime to be usable at all. The intent is that mainstream harnesses come to address the machine interface directly instead of the legacy one, because that interface is where the structure they are currently rebuilding by hand already lives. The same holds for anything else that calls out to a shell as a matter of course: container build tooling, task runners, deployment systems, any product whose real work is arranging commands and reading back what happened. The end state is not that one implementation wins. It is that the industry settles on a machine interface to the shell and stops asking every product to invent its own.

part four

Design in implementation

What is left divides into measurement and design. Two questions have measurable answers, and those answers decide how much of the safety argument the deterministic layer carries. Everything else is design, running from what a policy looks like on the page to how a system like this reaches the people who would run it and what keeps them using it. That second kind is a product question rather than an architectural one, and there is more of it.

The premeditable fraction

What share of consequential operations can premeditated policy actually adjudicate, versus the residue that is irreducibly contextual, where the danger lives in the mismatch between intent and moment rather than in the operation’s shape? If that fraction is high, especially in production environments, the deterministic layer carries the safety story. If it is low, the safety story belongs mostly to probabilistic judgment wherever it runs. The answer is measurable from real operational corpora, and measuring it is among the first work products.

Part of it is visible without measuring anything. A large share of what runs at a shell cannot change the machine at all: reading a file, listing a directory, printing a value, searching text. That set is finite and enumerable on any common distribution, and enumerating it is tedious rather than difficult. It takes flag-level care, since a command is inert only in some of its forms and find is inert until it is handed -delete or -execR/9B1, but that is the distinction the representation makes available.

Baseline policies covering that surface ship with the system rather than being left to each installation to derive. A fresh install is therefore not deny-everything-until-configured. It refuses what can cause harm and permits the inert surface from the first command, so the low-friction path and the safe path are the same path. That is the answer to the transfer described earlier, in which a defensible default cannot do the work, and the person who needs the work done widens it themselves and inherits whatever follows. The enumeration happens once, in the open, where it can be reviewed and corrected, rather than privately and under pressure by everyone who installs the thing.

The half-life

The half-life is where this system’s prediction is most exposed. Figure 1 puts two curves on one axis. Human review decays with exposure, from roughly seventeen percent to five across fifty prompts. The classifier holds flat near eighty-nine and never arrives at a hundred, because a rate is the only thing it can produce. A reviewed corpus should do neither of those things. It should rise, and that prediction is checkable in deployment.

The reason to expect it is not optimism about policy files. It is the difference between what can be spent on a decision made once and a decision made in the moment. An ad hoc judgment gets a single pass, from one perspective, under time pressure, with the work waiting on it: zero-shot and on demand, whether the judge is a person or a model. A policy in a shared corpus gets many passes from many perspectives and keeps them. Orders of magnitude more human reasoning and more compute can be justified on an artifact that will be inherited a thousand times than on an approval used once. This plainly expects sustained collaboration between people and machines on the correctness of those artifacts, and there is no reason to be shy about saying so. What is being applied to policy is an iterative agentic harness, the same thing already applied to code, rather than one evaluation at the moment of use.

The starting point matters as much as the slope. Deny by default, with baseline policies inherited rather than written from nothing, means a fresh system permits exactly zero harmful actions, and every capability past that is added deliberately. Movement runs in one direction, from a floor, through explicit acts that are themselves reviewable.

The curve is a property of practice as much as of artifacts. Take a service that has to be brought back in production by a route nobody has used before: no policy covers it, no runbook describes it, and there is nothing in the corpus to inherit. What should happen is that the person doing it works with someone who knows the system as well as they do, and the approach is examined before it runs rather than reconstructed afterwards. That review is no longer limited to two people. Several can attend it and several agents with them, each reading the same proposed operations. Whether it happens at all is an organizational choice, which is the sense in which the half-life is not purely a property of the system. It rests on a willingness to adopt procedures that keep the artifacts under review.

The honest risk lives in those acts. Capability gets added by people with work to finish, and a team or a community can admit a policy less carefully considered than the one it extends, so the curve can be pushed down as well as up. The claim is not that this cannot happen. It is that a policy admitted carelessly into a corpus, where it stays visible and inheritable and correctable, does not decay at anything close to the rate of a decision made once in a dialog and never seen again.

The rule language

The rule language is the largest of the undesigned pieces. It is not an existing notation adopted for the purpose, and nothing here specifies it beyond the properties the argument requires of it: deterministic, legible at rest, inheritable, and evaluated against resolved operations rather than against text.

What a policy written in it takes as its shape, and what extent that policy governs, are unsettled together. Two shapes appear above, the runbook and the policy for a single tool, and they are treated as artifacts of one kind. Whether they are is not established. A tool policy says what a command may do and says nothing obvious about where it may do it, and an extent, whether that is a path subtree, a host, an environment, or a single session, may belong to the policy, to whatever binds the policy, or to both. A rule that reads identically in a development environment and in production is too loose in one of them, and the mechanism that tells those apart is not specified here.

A related question sits underneath it. Parse-time state is scoped, and the scoping rules differ by dialect, so something has to carry the scope a given region was parsed under. Annotating the tree itself is the obvious candidate, and it would pay for itself if anything downstream needs to reconstruct that environment, whether to re-parse deferred text correctly or to let a rule ask what was in force where an operation appeared. It is not obviously necessary, and it should not be built until it is shown to be. The test is whether a consumer exists that cannot do its job without it.

Composition

How the two shapes compose is open as well. A runbook is a reviewed sequence, and every command inside it carries a policy of its own. If a runbook amounts to the union of what those policies already permit, it adds review and no authority, and the cases worth reviewing are excluded by construction. If instead a runbook can authorise an aggregate that no single command policy would, then the runbook is the artifact where an effect becomes expressible at all, which is the thing nothing in the recorded history of these mechanisms can express R/7A6. That would make the runbook the load-bearing form and the per-tool policy its vocabulary. It also raises the question this essay does not answer: what happens when a runbook and a command policy disagree, and which of them prevails.

Composition is larger than that pair. Policies group, and the groups are what anyone would actually inherit: everything concerning a version control system, everything concerning a package manager, everything that writes to a filesystem at all. A set assembled around a technology and a set assembled around a behaviour are different objects, maintained by different people, and useful at different moments. Which of the two a corpus is organised around decides how much a newcomer has to read before they can trust what they are taking on.

Which policies are in force is contextual as well, along axes that do not obviously commute. An organization may make a set available to a role rather than to a person. A set may attach to a host, to a service, or to whatever realm a session is currently inside, so that connecting somewhere changes what is permitted without anyone editing anything. Public vetting is a further axis: a policy that has survived review by the audience depending on it carries a different weight from one written locally last week, and a corpus with no way to express that difference will be trusted uniformly or not at all.

None of this is settled by measurement. They are design decisions, and the argument here is that they should be made in the open, against a corpus, by the people who will inherit the result.


related
R/9B1
Three agent harnesses drive the same shell through three separately built integration layers
The mechanics of three harnesses, counted: invocation shapes, state machinery, parsers, approval units, and the improvised runtime around them.
R/7A6
Shell execution controls mediate operations one at a time, and none surveyed mediates their aggregate effect
Restricted shells, sudo, chroot, jails, seccomp, SELinux, AppArmor, Capsicum and JEA, catalogued by the object each one binds to.
R/B84
No major shell dialect beyond the POSIX subset has a definition other than its implementation
How shell languages are specified, tested, and reimplemented: spec-test corpora, mechanised semantics, and the oracle problem.
R/5C2
Interfaces that interrupt a person for a decision are waved through in every domain measured
Five classes of interrupting interface across four decades, and what becomes of the judgment they ask for.