For AI agents: the complete documentation index is available at https://a3s-lab.github.io/ash/en/llms.txt, the full documentation bundle is available at https://a3s-lab.github.io/ash/en/llms-full.txt, and this page is available as Markdown at https://a3s-lab.github.io/ash/en/guide/cli.md.

CLI reference

The primary ash CLI is a machine interface. Its successful output and error diagnostics use stable ASON, with no human help page, color, or progress bar. The separate, feature-gated ash shell route is explicitly human-facing and uses conventional bytes, diagnostics, and exit statuses instead of ASON.

CommandInputOutput
ash --versionNoneStable version string
ash --build-infoNoneVersion, target, protocol, ASON, commit, and trust fingerprint
ash asonBare ASON on stdinUnique canonical ASON
ash runOne typed request on stdinOne final response
ash rpcLength-framed stdin/stdoutHandshake and ordered response frames
ash shell [--no-profile | --profile FILE] [-c SOURCE | FILE]Terminal, inline, native file, or bounded stdinHuman command output and source-spanned diagnostics
ash self status [--prefix PATH]Installation prefixCurrent/previous version and install state
ash self check --candidate FILECandidate binaryHealth, version, and target
ash self update [--prefix PATH] [--from DIR]Signed metadata or offline directoryActivation result
ash self rollback [--prefix PATH]Installation stateRollback or scheduled result
ash self recover [--prefix PATH]Installation journalRecovery result

ash self replace is an internal Windows helper used by activation, not a normal Harness interface.

Human shell checkpoints

The current human-shell checkpoints open a cross-platform line-edited REPL when stdin is a terminal and executes the current command and pipeline syntax sequentially against one persistent native state. It implements pwd, echo (with -n), cd, expanded export/unset, set pipefail control, exit, portable ls, raw-byte cat, text grep, portable cp, mv, rm, create-only touch, named and last-status parameter expansion, nested command substitution, quote-aware pathname expansion, native host executables, explicit WSL commands on Windows, plus same-line native/WSL/portable/stateful pipelines, left-associative &&/|| conditional lists, and ordered native, WSL, portable, and stateful redirections. Source may also come from -c SOURCE, a native script-file path, or stdin:

ash shell
ash shell -c "grep -in 'semantic' crates/ash-ops/src/semantic.rs"
ash shell -c 'export NAME=world; echo "hello $NAME"; echo $?'
ash shell -c "native:rustc --version"
ash shell -c "linux:uname -a" # Windows with WSL
ash shell -c 'native:rustc --version >version.txt 2>&1'
ash shell -c 'cat - <input.bin >copy.bin'
ash shell -c 'cp input.bin backup.bin; mv backup.bin archive.bin'
ash shell -c 'grep -F ready status.txt && echo complete || echo pending'
ash shell -c 'echo crates/*/Cargo.toml'
ash shell ./script.ash
printf 'echo from-stdin\n' | ash shell --no-profile
ash shell --profile ./profile.ash

File, stdin, and Profile sources must be valid UTF-8 and cannot exceed 1 MiB. Native file paths are not converted to UTF-8; use ash shell -- ./-script.ash for an operand beginning with -. Portable ls accepts one path (default .), emits one stably ordered native name per line, and supports -a/--all, -d/--directory, -1, combined short options, and --. Portable cat requires one path, writes exact bytes without an added newline, accepts --, and shares the 128 MiB read/capture ceiling. In a multi-stage pipeline, cat - consumes the incoming byte stream; in a simple command it consumes an explicit < file. An unredirected standalone -, options, and multiple files remain errors. Portable grep requires one valid-UTF-8 regular file and uses Rust regular expressions by default. It supports -E/--extended-regexp, -F/--fixed-strings, -i/--ignore-case, -n/--line-number, combined short options, and --. Searches are capped at 64 MiB; no matches return status 1 without a diagnostic. In a multi-stage pipeline, grep PATTERN - consumes incoming UTF-8 with the same semantics and a 128 MiB output ceiling; a simple command may provide - through <. Directories, multiple files, unredirected standalone stdin -, and unsupported options fail explicitly.

Portable cp SOURCE DESTINATION, mv SOURCE DESTINATION, rm PATH, and touch PATH accept only those exact regular-file arities plus --. The persistent cwd is their durable transaction root. Parent traversal, escape, symlink/reparse traversal, directories, files above 128 MiB, and paths that cannot be represented in the UTF-8 journal fail explicitly. Copy, move, and touch never overwrite; touch creates a new empty file rather than updating an existing timestamp. Copy, move, and remove derive a BLAKE3 source preimage immediately before the shared ASH/1 fs transaction revalidates it. A conflict returns status 1 without retry and preserves the external change or destination. The root's reserved .ash directory holds locking, rollback, and restart recovery state.

Startup Profiles are opt-in. --profile FILE selects one explicitly, a non-empty ASH_PROFILE supplies the configured default, and --no-profile disables both. Relative paths are anchored to the initial cwd. A Profile is parsed completely before any command runs: malformed startup never applies a prefix. A non-interactive invocation stops with status 2; an interactive shell prints the same source-spanned diagnostic and opens in safe mode. Profile state, including cd and exported variables, persists into the selected input.

The prompt defaults to ash> ; ASH_PROMPT replaces it and must be valid UTF-8. Ctrl+C at the prompt sets $? to 130 and continues, while EOF returns the previous status. exit [STATUS] stops the remaining submitted source and the REPL. An omitted status reuses $?; explicit values must be integers from 0 through 255, and invalid arguments return 2 without exiting.

ASH_HISTORY selects persistent history relative to the initial cwd, and an empty value disables it. Defaults are $XDG_STATE_HOME/ash/history then $HOME/.local/state/ash/history on Unix-like hosts, and %LOCALAPPDATA%\ash\history on Windows. Leading-space or leading-tab lines are not recorded. Symbolic links and non-regular targets are rejected, Unix history files are forced to mode 0600, and an unsafe or unavailable path produces a warning while the in-memory editing session remains usable.

export NAME=VALUE and unset NAME persistently update both shell-variable and exported environment state. Each accepts one expanded argument and --; names must be ASCII shell identifiers, empty values are preserved, and unsetting a missing name succeeds. Quote values that can contain separators, as in export COPY="$SOURCE". Listing and multiple names are not implemented yet.

set -o pipefail persistently enables rightmost-failure pipeline status; set +o pipefail restores the default final-stage policy. Any other set form returns status 2. A startup Profile can select the policy. In a pipeline, set uses that stage's state clone and cannot mutate the parent policy.

$NAME, ${NAME}, and $? expand immediately before each command resolves. Shell variables precede host-aware environment lookup and undefined names are empty. Single quotes and escaped dollars remain literal. Double quotes preserve one native field, including an empty field; unquoted values split on fixed ASCII space, tab, and LF separators, and an unquoted empty expansion can remove its word. IFS does not configure splitting in this checkpoint. Native string units remain lossless through expansion and direct argv launch.

Nested $(SOURCE) command substitutions are parsed into the same typed script plan, with exact outer-source diagnostics and a 32-level depth limit. They run in source order in command words and file-redirection targets. Each receives a full ShellState clone, so cwd, variables, environment, options, status, and exit remain local; ordinary external process and filesystem effects remain visible, including when a later pipeline stage fails outer preflight. Ash captures stdout, removes every trailing LF, preserves one field and internal newlines inside double quotes, and applies the fixed ASCII split only when unquoted. A nonzero nested status does not replace parent $? or block the outer command; stderr and diagnostics propagate once. NUL output is rejected. Unix preserves non-UTF-8 bytes, while Windows requires valid UTF-8. Substitution values, stdout, and stderr share the remaining 128 MiB synchronous allowance; a capture failure prevents the outer command from running. Short-circuited pipelines execute no substitutions, and full-source parsing still precedes all effects.

After fixed unquoted field splitting, pathname expansion recognizes active *, ?, [abc], ascending [a-z], and negated [!abc] or [^abc] operators in command fields and redirection targets. Single quotes, double quotes, and a backslash protect operators; unquoted parameter and command-substitution output can introduce them. Relative patterns enumerate from persistent cwd, absolute patterns stay absolute, and results sort by lossless native path units with case-sensitive matching. A wildcard component selects a leading-dot name only when it begins with a literal dot. ** is not recursive. Malformed classes and patterns with no matches fail before launch; redirections still require exactly one match. Each command and its redirections share limits of 32,768 active pattern units, 65,536 inspected directory entries, and 4,096 matches. Skipped conditional branches perform no directory scan.

Native programs resolve through the persistent PATH or an explicit native: prefix. The resolved host executable receives the parsed argument vector, current directory, and exported environment directly; ash never inserts a host shell command string. An unredirected standalone child receives null stdin and synchronously captured output, so programs that require their own foreground terminal remain part of H4 job control rather than this line-edited REPL. Stdout and stderr share the remaining 128 MiB capture allowance, and a normal nonzero native exit becomes the command status without an added diagnostic.

On Windows, linux:COMMAND explicitly selects WSL. Ash first resolves wsl.exe; a missing launcher returns status 126 with a resolution diagnostic, and an unresolved ordinary command never falls through to WSL. The wrapper receives an optional distribution, the current Windows cwd through --cd, then --exec, the Linux command, and each argument as a separate native string. No host or Linux shell string is inserted. The CLI currently uses the user's default distribution; an embedding may select one through ShellOptions, and the selected WSL backend/distribution is retained in command status.

Native, WSL, and portable commands plus implemented stateful builtins accept <, >, >>, 2>, 2>>, 2>&1, and 1>&2. Assignments apply from left to right: command >out 2>&1 merges both streams into out, while command 2>&1 >out leaves stderr on the original stdout capture. Every file target, including $(...) and pathname patterns, must finish as exactly one native field; relative paths resolve against persistent cwd. Files connect directly to child or parent-task OS handles, so redirected output does not consume the synchronous capture allowance. One validated graph order interleaves native, portable, and stateful files by stage and redirection source order; superseded targets are still opened. Missing, ambiguous, or unopenable targets return status 1 with a redirection diagnostic. Source-spanned shell diagnostics remain shell stderr, not raw command stderr. Stateful and portable-mutation arguments and targets preflight before opens; simple-command files open before parent mutation, and failed opens block the state mutation, portable filesystem transaction, or exit request. WSL redirection files are opened by the Windows host and connect directly to the wrapper's standard handles.

A same-line | forms a pipeline of two to 32 stages. Each may be a native host command, explicit WSL command on Windows, portable pwd, echo, ls, cat, grep, cp, mv, rm, or touch, or implemented stateful cd, export, unset, set, or exit; aliases, functions, and unimplemented stateful commands fail during complete preflight. Native and WSL wrapper pairs remain connected by direct OS pipes. Boundaries involving an in-process stage keep only explicit asynchronous parent ends and poll the bounded task concurrently with native execution and capture, preserving OS backpressure without materializing native or portable cat/grep streams in full. Only cat - and grep PATTERN - consume incoming stdin; other portable forms close that reader, so an upstream producer can observe broken-pipe failure. Final in-process output, final native output, and captured native stderr share the remaining 128 MiB allowance. Status defaults to the final stage. With set -o pipefail, the rightmost unsuccessful native, WSL, portable, or stateful stage is selected, including 128 + signal; an all-success pipeline still uses its final stage. Stateful stages never consume stdin, execute against independent ShellState clones, and close their empty stdout on completion. The parent cwd, variables, environment, options, and exit request remain unchanged; downstream readers see EOF, and pipeline exit contributes only an ordinary stage status. Mutation stages likewise close stdin, emit no stdout, and contribute their transaction conflict or filesystem status to final-stage/pipefail selection. Their redirection files open in graph order before the transaction begins. Any native, WSL, portable, or implemented stateful stage may redirect stdin, stdout, or stderr, with descriptor duplication still applied in source order. If a producer no longer references an internal pipe, ash closes its parent writer and the downstream reader sees EOF. If a consumer replaces its pipe stdin, ash closes the parent reader and the upstream writer sees native broken-pipe behavior. Final-stage or pipefail selection then determines the visible status. A descriptor copy that still references the pipe remains connected even if the original descriptor is later redirected. Stateful files participate in the same global open order; these builtins emit no raw command output, and redirecting their empty stdout closes an outgoing pipe normally. A WSL stage reuses the same validated pipe and file graph without a relay buffer. After parent pipe/file handles are claimed, ash converts the native graph into one job supervisor. Native exits retain specification order, and any setup, capture, or wait failure terminates and reaps every native member's owned process tree before the pipeline returns. Portable/stateful futures and capture drains settle before the supervisor enters a non-cancelled ordered wait under that same completion boundary.

&& and || link complete pipelines with equal precedence and evaluate left to right. && admits the next pipeline only after status 0; || admits it only after a nonzero status. The input is the visible final-stage or pipefail pipeline status. A skipped branch leaves that status unchanged for the next link and $?, emits no diagnostic, and performs no expansion, resolution, argument preflight, redirection open, process launch, state change, or portable filesystem transaction. Only an admitted exit stops the source. The complete submitted source still parses before effects, so malformed skipped syntax blocks every prefix command. Layout, comments, and newlines may continue a source after an operator; an unlinked newline or ; starts a new unconditional list.

The remaining command language (including aliases, functions, and subshell state), terminal streaming, foreground interactive programs and jobs, plus installed WSL distribution probing, backend policy/configuration, general argument path mapping, environment forwarding, Linux-side ownership, and interruption normalization are not implemented yet. The regular binary enables the human-shell feature; cargo build -p a3s-ash --no-default-features keeps a minimal machine-only build.

Error contract

Example:

s:1
e{c}:
4

For machine commands, exit codes map stably to error classes. Callers should branch on ASON diagnostics and exit status, never localized text. Missing arguments and unknown machine commands return a usage-class machine error instead of printing help. ash shell deliberately emits human diagnostics and conventional command statuses.

Working directory

File operations are confined to the startup workspace. Paths must use canonical logical form and cannot escape through lexical traversal, symbolic links, or Windows reparse points. exec never switches to an implicit host shell.

Human-shell state instead uses native PathBuf and OsString values. cd, export, unset, ls, cat, and grep inherit normal user authority. Portable cp, mv, rm, and touch deliberately bind the current cwd as a journaled workspace and cannot traverse above it. Native external programs inherit the same cwd/environment state and ordinary OS authority. Native path and environment units are preserved or reversibly escaped; this does not weaken or replace ASH/1 workspace confinement.