R/9B1

Three agent harnesses drive the same shell through three separately built integration layers

What the shell guarantees a caller is thin: a string goes in, eight bits and an unmarked byte stream come back, with no content type, no error taxonomy, and no record of what changed. Three coding agent harnesses drive that boundary, and each has built its own machinery to cover the difference: shell selection by substring test in one, kernel containment in another, session state reconstructed by a generated startup script in a third, and an approval stored as a prefix, an exact argument vector, or a curated arity pattern depending on which harness stores it. The machinery is substantial and none of it is shared. Codex CLI spends 48,900 lines of Rust on its shell path alone, and the mechanisms that recur across the three were each implemented independently.

part one

Method

Three systems were inspected in August 2026. Claude Code (v2.1.228)1 ships as a Bun-compiled native ELF executable of roughly 300 MB; where a finding is an absence, it was checked against the five versions present on the inspected machine, 2.1.214 through 2.1.232; it is not stripped, and its JavaScript is embedded as plain text, so the bundle yields to strings extraction of 41 MB across 468,887 lines, which was searched mechanism by mechanism. The binary was never executed as part of this inspection. Codex CLI2 (openai/codex at a3cb1c14) and OpenCode3 (sst/opencode at e23586af) are open source and were read directly from shallow clones at those commits; every line count below was measured against those trees, excluding tests.

Each finding carries its sourcing: from binary, from source, or measured. Reverse engineering is recorded as a technique employed. Quantities are stated as measured; where a quantity could not be measured directly, the record says so.

part two

The interface under inspection

The shell’s invocation interface is a string, assembled by the caller and passed to an interpreter the caller does not control. Its result interface is an exit status of eight bits and an undifferentiated byte stream carrying a program’s output, progress messages, and warnings without markers distinguishing them. The interface defines no content type, no error taxonomy, and no record of state change. Exit code 1 denotes no lines selected from grep and a general error from most other utilities; the interface does not distinguish these, so per-tool knowledge is held by the caller.

All three harnesses relate to the shell as callers rather than as pipeline participants: each spawns a child process, waits for exit, and reads a captured buffer. All three cap that buffer. Claude Code truncates command output at 30,000 characters by default and 150,000 at the configured maximum, substituting ... [N lines truncated] ... (from binary).

Sections three and four record what each harness implements against this interface, and section five records the annotations each harness carries about its own limits.

part three

The mechanics

Claude Code

Claude Code requires a bash- or zsh-family shell. Validation is a path substring test, requiring that the shell path contain the literal text bash or zsh, implemented across sixteen distinct .includes() call sites, which also select the rc file to source (.bashrc versus .profile) and sniff shebangs; an override failing the test logs “invalid bash/zsh path, falling back to detection” (from binary). A non-bash executable at a path containing the substring satisfies the test; a bash executable at a path not containing it does not.

Every command runs as a fresh child, invoked through execFile with the argument vector ["-c", "-l", script], placing the login flag between -c and its operand, an ordering no other inspected tool uses. There is no persistent shell, so working directory is carried out of band: the harness appends a second command to every invocation, joining with &&, that writes the resolved directory to a temp file under /tmp/claude, and the next invocation starts by reading it (from binary).

listing 1
Invocation shapes and the out-of-band state carry, as inspected. Sourcing: Claude Code from binary; Codex CLI and OpenCode from source.
invocation shapes
three harnesses · august 2026
1 2 3 4 5 6
# Claude Code: login flag interposed; cwd carried through a temp file
execFile(shell, [“-c”, “-l”, script])
eval <cmd> && pwd -P >| /tmp/claude-<id>-cwd
# Codex CLI inside a kernel sandbox; OpenCode at host authority
[shell, “-lc”, command]
[shell, “-c”, command]

Because each child is fresh, exported variables, shell options, and function definitions die between every pair of commands. The harness reconstructs them at session start with a generated snapshot script: roughly 3,657 bytes of template emitting thirty >> "$SNAPSHOT_FILE" echo statements, which run unalias -a, capture each function via typeset -f, re-encode functions as base64 to be decoded and evaluated on replay, and record option state by scraping shopt -p, set -o | grep "on" | awk ..., and setopt | sed ..., each capped at head -n 1000. The generator is embedded twice in the binary (from binary). The mechanism reconstructs, in shell script, state that the interface does not expose.

Command-effect knowledge is carried in the binary as guard tables. Ten find predicates are withheld from prefix-rule approval on the recorded grounds that they execute commands or modify files: -exec, -execdir, -ok, -okdir, -delete, -fprint, -fprint0, -fprintf, -fls, and -files0-from. Comparable tables cover awk programs containing system(), a command pipe, @load or @include, extension(), or an /inet/ socket, and jq programs containing system() or module imports, along with the flag sets that read a program from a file. Refusals are classified into a fixed taxonomy recorded as bashMissKind, whose values include process-substitution, shell-expansion, shell-operators, flag-validation, sed-dangerous, and too-complex (from binary). The tables encode which effects each tool can produce, a fact the shell interface does not carry.

The tool environment is curated by shadowing: the snapshot installs functions that shadow rg, find, and grep, re-executing the harness’s own multicall binary under a different process name via exec -a and ARGV0, dispatching to embedded implementations: bfs for find, ugrep for grep (from binary). Gating is model-mediated: a tree-sitter pre-check (five grammars are bundled) escalates ambiguous cases to a small model that extracts an allowlist prefix, and a transcript-aware classifier decides auto-approval, both issued over the same API endpoint as ordinary inference. Prefix machinery accounts for 439 matched references in the bundle, including an ambiguity guard (Ambiguous prefix '...', matches: ...), a documented caveat that a trailing :* performs “prefix STRING matches with NO flag-level analysis,” and a special case for find and jq annotated as a “tree-sitter mis-parse” (from binary).

Codex CLI

Codex supports multiple shell families, detects the user’s shell, and builds each invocation as [shell, "-lc"|"-c", command], spawned inside a kernel-enforced sandbox: Seatbelt on macOS with 351 lines of SBPL policy, Landlock plus seccomp and bubblewrap on Linux, restricted tokens on Windows (from source). It is the only inspected harness applying kernel-enforced containment, and it carries the largest shell-facing codebase of the three.

Approvals are keyed on a five-field structure: environment id, canonicalized argument vector, working directory, sandbox permission level, and an optional additional-permission profile. That specificity means the same command in a different directory prompts again. Before matching, the harness must undo its own wrapper: a dedicated 42-line module exists to canonicalize bash -lc-style invocations so that approval decisions stay “stable across wrapper-path differences (for example /bin/bash -lc vs bash -lc)”, backed by an explicit table of recognized wrapper forms, and complex scripts that resist unwrapping collapse to a placeholder token (from source). The module exists to reverse a transformation the same harness applies on invocation.

On sandbox denial, a heuristic classifies the failure, quick-rejecting exit codes 2, 126, and 127 as ordinary errors, and the orchestrator may re-run the command with the sandbox removed, surfacing the prompt “command failed; retry without sandbox?” Policy gates this: under stricter settings the retry is refused and a comment in the source notes that “retrying without the sandbox requires a fresh guardian review” (from source). Under permissive settings the documented response to a containment failure is execution without containment. File edits leave the shell entirely: a 4,710-line apply-patch crate handles them as a first-class tool, and it also intercepts patch invocations that arrive through the shell tool.

OpenCode

OpenCode selects a shell through a cascade: a configured value, then $SHELL validated against /etc/shells, then /bin/zsh on macOS or whatever which bash returns, with /bin/sh as the final fallback. It runs each command as a fresh detached child with no snapshot and no state carried between commands (from source). There is no OS sandbox anywhere in the execution path; the tool’s own description states that commands run with the host user’s filesystem, process, and network authority, and its out-of-workspace path scan is labelled advisory.

Gating is a wildcard rule engine (allow/ask/deny, last match wins, defaulting to ask) over commands extracted by parsing input with tree-sitter-bash compiled to WebAssembly and walking descendantsOfType("command"), a full recursive descent, so commands nested inside pipelines, subshells, and command substitutions are each extracted and gated individually. This closes the naive chaining hole where an allowlisted prefix would bless an appended destructive command.

To determine how much of a command an approval should generalize over, the harness consults a hand-maintained arity dictionary of 136 entries mapping commands to their defining token counts (git: 2, "npm run": 3, gh: 3, "docker compose": 3, cat: 1) and truncates the token list to that arity, producing a sticky pattern like git checkout *. The file’s own header records that it was generated by prompting a language model for the dictionary. A parallel note in the codebase’s newer core module reads // TODO: Port BashArity reusable command-prefix approvals (from source). Command semantics not carried by the shell interface are maintained here as source data, generated by prompting a language model rather than derived from a specification.

part four

Executable resolution

Each harness selects a shell by a different procedure, and each procedure constrains which executables are reachable.

Claude Code accepts only bash- and zsh-family shells. The path is tested for the literal substrings bash or zsh across sixteen call sites, and the same test selects the startup file to source, resolving to .bashrc where the path contains bash and .profile otherwise. An override is read from CLAUDE_CODE_SHELL and validated by the same substring test before use; a path failing it produces the log line “invalid bash/zsh path, falling back to detection”. The test is on the path string, not on the executable: a path containing the substring satisfies it regardless of the binary, and a conforming binary at a path without the substring does not (from binary).

Codex CLI performs its own shell detection and supports multiple families, including POSIX shells, PowerShell, and cmd. Separately, exec_policy.rs carries an explicit table of invocation forms recognised as wrappers for canonicalization purposes, containing ["/bin/bash", "-lc"], ["/bin/sh", "-lc"], and ["bash", "-lc"] among others; an invocation not matching an entry in that table is not unwrapped and is instead reduced to the placeholder form ["__codex_shell_script__", <mode>, <script>] (from source).

OpenCode resolves in a cascade: a configured value if present, otherwise $SHELL validated against the contents of /etc/shells, otherwise /bin/zsh on macOS or the result of which bash, with /bin/sh as the final fallback. Validation against /etc/shells means an executable absent from that file is not reachable through the $SHELL path of the cascade (from source).

Invocation contract

All three harnesses invoke non-interactively with the script supplied as a single argument. The argument vectors differ in flag composition and order.

table 1
Invocation contracts as constructed by each harness. Sourcing: Claude Code from binary; Codex CLI and OpenCode from source.
HarnessArgument vectorNotes
Claude CodeexecFile(shell, ["-c", "-l", script])login flag follows -c; script is the third element
Codex CLI, POSIX[shell, "-lc", command]-c when login shell is not requested
Codex CLI, PowerShell[shell, "-NoProfile", "-Command", command]profile suppressed
Codex CLI, cmd[shell, "/c", command]
OpenCode, POSIX[shell, "-c", command]via the node child-process shell option
OpenCode, PowerShell[shell, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command]

Claude Code’s ordering places -l after -c rather than combining them, so an implementation parsing -c as taking the next argument as its operand receives -l as the script. Codex CLI combines the two as -lc. No harness passes the script on standard input.

Environment inheritance is on by default in all three. Claude Code reads CLAUDE_CODE_DONT_INHERIT_ENV to suppress it, and resolves its temporary directory from MODE_TMPDIR, then CLAUDE_TMPDIR, then /tmp/claude. Codex CLI spawns inside kernel-enforced confinement, so the executable and any subprocess it starts run under Seatbelt on macOS, Landlock with seccomp or bubblewrap on Linux, and a restricted token on Windows (from source).

Command parsing

Each harness parses command text before deciding what to do with it, and the parsers are not the same. Codex CLI and OpenCode both use tree-sitter-bash, the grammar maintained at tree-sitter/tree-sitter-bash under MIT, on the same core runtime, tree-sitter 0.25.10, but on different grammar releases (0.25.1 and 0.25.0, which differ by a single grammar change). Codex CLI links the Rust crate natively; OpenCode loads the WebAssembly build at runtime through web-tree-sitter (from source).

Claude Code carries no tree-sitter. Across all five versions checked the binary contains no tree-sitter runtime symbols, no grammar tables, and no WebAssembly grammar; the only tree_sitter_ string present is a telemetry event name, tengu_tree_sitter_parse_abort. In its place is a hand-written recursive-descent parser in JavaScript, bounded by a 64 MiB input limit, a 50 ms deadline, and a 50,000-node budget, which aborts to that telemetry event when a bound is crossed. It emits 59 of the 62 node types tree-sitter-bash emits; the three it omits are hidden supertypes tree-sitter does not emit either. The vocabulary is reproduced and no code is shared (from binary).

tree-sitter-bash is a syntax parser. It produces a concrete syntax tree and performs no expansion, substitution, globbing, or evaluation, which is why each harness implements expansion itself: OpenCode by regular expression over ${env:X}, $HOME, and ~, and Claude Code by an abstract-value engine over its own tree that substitutes sentinel placeholders for values not knowable before execution (from source, from binary).

Parse failure is represented inside the tree rather than raised. Unparseable spans become ERROR nodes, the parser inserts zero-width MISSING nodes where it recovers, and a caller therefore receives a tree in every case and must ask whether it is clean. Codex CLI asks, returning no commands when has_error() holds at the root. OpenCode does not: no error check appears anywhere in its TypeScript, and its extraction walks whatever tree was produced. Claude Code classifies its own ERROR node as too-complex with the reason “Parse error” (from source, from binary).

The grammar records its own limits. Its test harness parses four corpora, among them the GNU bash repository, and files every input that yields an ERROR or MISSING node into a known-failures list, which at 0.25.1 holds 363 files, 27 of them bash’s own test-suite files.

Shell-language constructs emitted by the harnesses

Beyond the commands a model produces, each harness emits shell text of its own. That text is a compatibility requirement: an implementation that does not accept it cannot carry the harness’s state or environment machinery. The following constructs appear in harness-generated text.

table 2
Shell-language constructs appearing in harness-generated command text, with the mechanism each serves. All entries are Claude Code unless stated; its snapshot generator accounts for most of them.
ConstructMechanism served
&& sequencingappending the working-directory probe to every command
pwd -Presolving the physical working directory
>| clobber redirectwriting the directory probe irrespective of noclobber
unalias -aclearing aliases at snapshot time
typeset -flisting function definitions for capture
base64 encode, decode, then evalreplaying captured functions into a new session
shopt -p, shopt -s expand_aliasescapturing and restoring bash option state
set -o output parsed by grep and awkcapturing option state as re-executable statements
setopt, output filtered by sedzsh-family option capture
head -n 1000bounding captured functions, options, and aliases
heredocsdefining the shim functions in the snapshot
exec -a and ARGV0 assignmentinvoking the multicall binary under an alternate process name
command -vtesting tool availability before shimming
>> append redirectionaccumulating the snapshot file
tree-sitter-bash parseabilitygating in OpenCode; safety classification and canonicalization in Codex CLI

Two entries carry consequences beyond the construct itself. exec -a is a bash builtin without a POSIX equivalent, and it is used to launch the harness’s own multicall binary under the names rg, find, and grep; an implementation lacking it cannot install Claude Code’s tool shims. Parseability by tree-sitter-bash is required not of the implementation but of the command text passing through it, because OpenCode derives its permission patterns from that parse and Codex CLI derives its approval-cache key and its dangerous-command classification from it; text that fails to parse is gated or keyed differently rather than rejected.

Result handling

All three harnesses read results as a captured buffer and an exit status. No harness receives a structured outcome, and none receives a record of state change; Claude Code’s directory probe exists to recover one field of that state out of band.

Buffers are bounded. Claude Code truncates command output at 30,000 characters by default with a configured maximum of 150,000, substituting ... [N lines truncated] ..., and applies a separate 32,000-character limit to subagent task output. Codex CLI retains head and tail segments around an elided middle. OpenCode truncates. Timeouts are configurable in Claude Code through BASH_DEFAULT_TIMEOUT_MS and BASH_MAX_TIMEOUT_MS (from binary; Codex and OpenCode from source).

Exit status carries interpretation in Codex CLI alone. Its sandbox-denial heuristic classifies a non-zero exit as a probable containment denial, with exit codes 2, 126, and 127 quick-rejected as ordinary errors rather than denials. An implementation returning those codes for other conditions is classified accordingly, and a denial classified as such may cause the orchestrator to re-run the command without confinement where policy permits (from source).

part five

Quantities

Codex CLI’s shell-facing machinery totals approximately 48,900 lines of Rust, excluding tests. By subsystem, containment accounts for the largest share. The second-largest block is a crate for parsing and canonicalizing command strings, and the fourth-largest implements file editing outside the shell.

fig. 1
Codex CLI shell-facing code by subsystem, thousands of lines, tests excluded (measured at a3cb1c14).
SUBSYSTEM
THOUSANDS OF LINES
TESTS EXCLUDED
Windows sandbox
17.6k
Command parsing
6.9k
Linux sandbox
6.3k
Edits around shell
4.7k
Core exec + approvals
4.6k
Sandbox policy
3.7k
Persistent sessions
3.2k
Exec policy
1.9k
5k
10k
15k
20k

Claude Code and OpenCode are smaller but not cheaper in kind. OpenCode spends 645 lines on the shell tool proper, 226 on shell discovery, 223 on the permission engine, and 163 on the arity dictionary. Claude Code’s equivalent machinery is not measurable in source lines from a compiled bundle, but it is countable in artifacts: sixteen substring validations, thirty snapshot echo statements, two embedded copies of the snapshot generator, three shadowed tools backed by two vendored search implementations, one hand-written bash parser covering 59 node types, and 439 prefix-matching references.

The workaround inventory, counted across the three harnesses.
48.9k
lines, codex shell path
136
arity dictionary entries
30
snapshot echo statements
16
substring shell checks
42
lines to unwrap own wrapper
table 3
Summary of shell interaction mechanics, three harnesses, as inspected August 2026.
Claude CodeCodex CLIOpenCode
InvocationexecFile, -c -l script[shell, -lc, command][shell, -c, command]
Shell selectionpath substring testshell detectioncascade to /bin/sh
Process modelfresh childfresh child; PTY subsystemfresh detached child
State between commandscwd via temp filenone carriednone carried
Session setup30-line snapshot scriptnonenone
Tool environmentrg/find/grep shadowedunmodifiedunmodified
Parserhand-written, 59 node typestree-sitter-bash 0.25.1, nativetree-sitter-bash 0.25.0, WASM
OS sandboxnoneSeatbelt / Landlock / tokensnone
Approval unitcommand prefixexact canonical argvcurated arity pattern
On denialask the humanretry without sandboxask the human
File editsthrough the shelldedicated crate, 4,710 linesthrough the shell
Outputtruncated at 30,000 charshead/tail buffertruncated
part six

Self-documented limits

Each harness carries in-source annotations describing limits of its own mechanisms. Claude Code’s permission documentation records that a trailing wildcard performs prefix string matching “with NO flag-level analysis”; its parser refuses sixteen shell keywords in command-name position under an annotation reading “tree-sitter mis-parse”, in a parser containing no tree-sitter code, and carries an ambiguity guard for prefixes matching more than one rule. Codex’s canonicalizer records that it exists to keep decisions “stable across wrapper-path differences”, and its orchestrator records that a sandbox-free retry “requires a fresh guardian review”. OpenCode’s arity file records that it was generated by prompting a language model, and its newer core module carries a TODO to port the mechanism forward.

Findings

Four observations follow from the record above.

Parse and execution are performed by different engines. All three harnesses parse command text and then pass the original string to the system shell for execution. Any divergence between the parsed representation and the executed program is not detected, and no specification of the dialect exists against which either could be checkedR/B84. The parse is a model of the shell’s behaviour constructed without the shell.

Commands assembled at runtime, read from variables, or passed through eval carry text that does not exist at parse time, and no static parse resolves them. The limit is structural rather than a shortfall of these implementations: a gate built on a pre-parse decides a proper subset of what the shell will execute, and cannot be made exhaustive by improving the parser. Text that defeats the parse is not rejected: OpenCode gates it under a different rule and Codex CLI keys its approval cache differently, so it still reaches the shell.

Codex CLI consults its parse in safety classification as well, under a helper documented as suitable for identifying dangerous literal commands but which “must not be used to prove that a command is safe.” It also applies kernel-enforced containment, which operates without reference to command semantics; its documented behaviour on denial includes retry without containment where policy permits.

Three harnesses run two parsers, and one vocabulary. Codex CLI and OpenCode share the upstream tree-sitter-bash grammar. Claude Code reproduces that grammar’s emitted node-type vocabulary in a parser written independently and sharing none of its code. A defect in the grammar is therefore held simultaneously by two of the three, narrowed further by their differing release pins, while the third carries whatever defects its own parser has. No harness checks its parse against another.

Three incompatible approval units are in use. Claude Code stores an approval as a command prefix, Codex CLI as an exact canonicalized argument vector, and OpenCode as a curated arity pattern. A prefix generalizes over arguments not present at review: an approval of git checkout applies to argument values never seen. An exact argument vector generalizes over no other invocation, so repeated invocations re-prompt. A curated arity pattern is intermediate and requires per-command maintenance, currently 136 entries. Measurements of approval behaviour under repeated prompting are recorded separatelyR/5C2.

Five mechanisms recur across harnesses without shared code. State carried between stateless processes via a temporary file; session state reconstructed by a startup script that captures and replays functions, aliases, and options; tool environments curated by shims routing common utilities to vendored implementations; approvals cached per session; and command-effect knowledge maintained as source data. Each appears in at least one harness, implemented independently.

Mechanisms not related to gating account for a substantial share of the code. Independent of approval logic, the record includes commands assembled by string concatenation under invocation contracts that differ between tools in flag ordering; shell validation by path substring; wrapper canonicalization prior to approval matching; working-directory state carried through temporary files; results recovered from an exit code and a truncated buffer; and file editing implemented outside the shell in a 4,710-line component. Measured totals are given in figure 1 and the accompanying paragraph.


related
R/5C2
Interfaces that interrupt a person for a decision are waved through in every domain measured
Five classes of interrupting interface and what becomes of the judgment they ask for.
E/2D8
The shell needs a separation of concerns
The thesis these findings support, argued in full: the diagnosis, the construction, and the two falsifiable claims.

sources

The systems inspected, at the versions inspected. Open-source trees are cited by commit; the compiled binary by version. Internal artifacts are cited by their permanent identifier.

1
Anthropic. Claude Code v2.1.228 — distributed binary, inspected by strings extraction, August 2026
Bun-compiled ELF, roughly 300 MB · code.claude.com
2
OpenAI. Codex CLI — source tree at commit a3cb1c14, August 2026
Rust; tests excluded from all counts · github.com/openai/codex
3
SST. OpenCode — source tree at commit e23586af, August 2026
TypeScript; tests excluded from all counts · github.com/sst/opencode