Documentation
¶
Overview ¶
Command sh drives the substrate.
It is not the product. This repository is the core — a parser and an interpreter other programs embed — and the shell people run, with its own choice of dialect and its own interactive surface, is a separate thing built on top. What is here exists to exercise the library, to make its behavior inspectable, and to be a column in the conformance harness.
It runs scripts, and the -dialect flag chooses which shell it is being: core refuses anything the panel disagrees about, and the others answer the way that shell does. That is what makes it a column in the conformance harness, which needs something it can hand a snippet to.
sh -h # what this is and how to invoke it sh # a prompt, if stdin is a terminal sh -c 'echo hi' # run a command sh script.sh a b # run a script, with $1 and $2 set sh < script.sh # or on stdin sh -dialect bash -c '…' # be bash where the shells differ sh -i # a prompt even where stdin is not a terminal sh -e script.sh # any set option, exactly as `set` reads them sh -tokens 'echo hi' # dump the token stream sh -parse 'a && b' # dump the syntax tree sh -trace-events script.sh # print every gated action to stderr sh -deny /etc script.sh # refuse every action at or under /etc sh -deny signal script.sh # refuse every signal the shell sends sh -deny exec:/usr/bin/** … # refuse one kind, under one subtree sh -policy p.policy script.sh # run it under a declarative policy sh -audit log.jsonl script.sh # record every action as JSON, one per line sh -acp # serve the Agent Client Protocol on stdio sh -mcp # serve the Model Context Protocol on stdio sh -plugin /opt/x/p script.sh # a builtin whose process is not ours sh -acp-connect npx pkg --acp # drive an ACP agent, under the same policy sh -blocks-list # the recent blocks: what ran, and how it went sh -blocks-show 1 # the most recent block, its record and output
Everything a shell reads at invocation — `-c`, a script path and its positional parameters, `-s`, a lone `-`, set options like `-e` — is read by the shared front end in driver, exactly as the dialect binaries read it. Only the flags no shell has — -tokens, -parse, -dialect, -trace-events, -deny, -policy, -audit, -plugin, -blocks-list and -blocks-show — are this binary's own, and they come first on the line: the first word that is not one of them belongs to the shell, so a script's own arguments can never be mistaken for them.
All four of the last ones reach the gate and event seam docs/design.md describes, and they exist because a seam nothing reaches is a seam nothing grades: the interpreter has asked a Gate about every exec, open, stat and directory read since its first commit, and until these flags no shipped binary ever set one, so a hole in the boundary would have looked exactly like a shell that works.
-blocks-list and -blocks-show read the store a prompt records into — see docs/design/blocks.md. They are here rather than as a builtin for the reason the others are here: a builtin would have to live in interp, which has no history and no business acquiring one, and no shell in the panel has such a command for a dialect to claim. Both read a store instead of running a shell, so they end the invocation — through whatever gate the same line asked for, because a policy that hides the store hides it from the tool that reads it too.
-trace-events and -deny are the debug half — a way to watch the gate refuse something. -policy and -audit are the shipped half: a declarative rule set read from a file, and the event stream written down in the schema its consumers share. docs/design/sandboxing.md is the specification for both.
The two halves share a rule language rather than each having one. A -deny value is a policy rule minus its decision word, spelled with a colon because a flag value is one shell word, and a bare path is the shorthand it has always had. Two rule languages over one gate would be two answers to the same question, and the one nobody exercises is the one that is wrong.
Neither half contains a *process*. The boundary is drawn around the interpreter: a policy refuses what the shell itself opens, stats and runs, and a command the shell was allowed to start makes its own accesses that nothing here sees — granting exec of `/bin/cat` is `allow read /**` spelled less obviously, which is why a policy has to write it `allow exec-unconfined /bin/cat`. Containing a running child is the job of an OS sandbox, which sits above the substrate.
A policy is never discovered: no environment variable, no dotfile. One that could be named by the environment could be replaced by anything able to set it, the sandboxed script included.
-h, -help and --help are the exception to "flags no shell has", and the exception is deliberate. bash and zsh read `-h` as `set -h` and dash refuses it; none of them would be a useful answer from a binary that is a harness column and an inspection tool rather than a shell anyone is being bash for. Fidelity to `bash -h` belongs to cmd/bash, which reaches driver without passing through here at all. A person meeting this binary types -h, and a usage message is not shell behavior — so it is answered before a dialect is resolved, before an axis is consulted and before a policy is read.
The default dialect stays `core`, which refuses at every axis the panel disagrees about, and that refusal is the point of the binary rather than a rough edge on it. Defaulting to `posix` would trade it for a worse failure: measured, that dialect cannot parse `a=(1 2 3)`, `[[ -n x ]]` or `${s:1:3}`, all of which every shell in the panel but dash accepts, so `sh -c` would stop at the front door on syntax rather than at the disputed semantics. Defaulting to `bash` would make the substrate's own driver answer every disputed axis the way one member of the panel does, which is the bash-first design this repository exists not to be, and would make the conformance harness's explicit `-binargs "-dialect bash"` indistinguishable from no choice at all. What the refusal owed a person was the name of the flag, and that is what it now carries — see axisRemedy.
With nothing to run and a terminal on stdin it prompts: a line editor with history that survives the session, Tab completion of commands and files, PS1 and PS2, and a continuation prompt for a construct that has not finished, and job control: ^Z stops what is running and gives the prompt back, `jobs` lists what is stopped, and `fg` and `bg` put one back.