Skip to main content
Mirage Bash is how agents act on the workspace. execute() parses a bash-style command, looks up the target session, resolves mounts, runs the executor, applies I/O side effects, and records history through the Observer.

Per-call overrides: cwd, env

Providing cwd or env runs the command in an ephemeral session clone, like a bash subshell (cd /data && cmd). Mutations like cd or export inside the call do NOT persist back to the workspace’s session. To change persistent state, run the command without these options.
This makes per-call overrides safe under concurrent calls on the same session. Two parallel execute() calls with different cwd see their own cwd without cross-contamination, even on the same session.

Subshells (...)

Wrapping commands in ( ... ) runs them in an isolated copy of the session: cd, export, and other mutations inside the parens do not leak back. It is the same isolation as the cwd / env overrides above, and the CLI’s stand-in for them (there are no --cwd / --env flags).
The isolation holds under concurrency, and subshells are still covered by the per-session mount allowlist and the cancellation boundaries below.

Mid-flight cancellation: cancel / signal

Both bindings support cooperative cancellation observed at recursion boundaries (LIST, PIPELINE, FOR/WHILE/UNTIL iterations, COMMAND, subshells, command substitution) and inside sleep. On cancel, the call raises an abort error.

Three Scopes for State

Supported bash syntax

Mirage Bash is a tree-sitter-bash parser plus a custom executor. It implements the constructs LLMs reach for most often. What is not supported returns a clear, parseable error so an agent can self-correct on its next turn.

Supported

  • Operators: pipes |, |&; lists &&, ||, ;; background &.
  • Redirects: >, >>, <, 2>, 2>&1, &>, &>>, heredoc <<, herestring <<<.
  • Substitutions: command substitution `cmd` and $(cmd); arithmetic $((expr)); parameter expansion ${VAR}, ${VAR:-default}, ${VAR%suffix}, etc.; input-direction process substitution <(cmd).
  • Control flow: if/elif/else/fi, for, while, until, case, select, function name() {}, break, continue, return.
  • Grouping: subshells (cmd), compound { cmd; }, negation ! cmd.
  • Builtins: cd, pwd, echo, printf, printenv, read, source, ., eval, export, unset, local, set, shift, trap (no-op), test, [, [[, true, false, sleep, xargs, timeout, bash, sh, python, python3.
  • Builtin options (GNU semantics): echo -n/-e/-E (leading-word option rule: echo hi -n prints hi -n), read -r, xargs -n/-0/-d/-r/-- (batching, GNU exit codes: 123 when an invocation fails, 126/127 stop the run), timeout DURATION with s/m/h/d suffixes (kills at the deadline with exit 124, usage errors exit 125). shift and return report bash’s numeric argument required errors.
  • Globs: *, ?, [...] classes and [!...] negation (Python fnmatch semantics in both implementations), resolved by the shell or pushed down to the resource.
  • Comments: #.

Unsupported (returns clear error)

  • Job control: bg, disown. (fg, jobs, wait, kill, ps work; use the --background flag and mirage job CLI for long-running work.)
  • Shell internals: exec, complete, compgen, ulimit.
  • Output process substitution: >(cmd) (the <(cmd) direction works).
  • Builtin options with no process backing: xargs -I/-P (exit 1) and timeout -s/-k/--preserve-status (exit 125) return an unsupported option error: commands run as coroutines inside the workspace, so there is no process to signal or parallelize.
Each returns exit_code 2 with stderr mirage: unsupported builtin: <name> or mirage: unsupported: process substitution >(...), except the builtin options above, which use the listed GNU-shaped exit codes.

Syntax errors

Commands the parser cannot make sense of return exit_code 2 with stderr mirage: syntax error near '<token>'. Earlier versions silently ran whatever fragment did parse; that no longer happens.

What --background is and isn’t

The daemon’s --background flag detaches a job and returns a job id. It is not the same as the bash & operator, which the shell does support inline (sleep 30 &). Use & for in-shell job parallelism, --background (or mirage job) for long-lived work that should outlive the request.

Per-session mount modes

A session can be created with its own per-mount modes, like a container that mounts the same volume ro while another mounts it rw. Each listed prefix carries a mode ceiling on the read < write < exec ladder, written as the words read/write/exec or the cumulative filesystem aliases r/rw/rwx (exec implies write implies read, so bit-style forms like a bare w are rejected). A command touching a mount the session was not given is rejected with mirage: session 'agent' not allowed to access mount '/X' and exit code 1; a command exceeding the session’s mode fails exactly like it would on a read-only mount. If a session is created without mounts, it is unrestricted: every mount behaves per its own configured mode. A list of prefixes (instead of a mapping) restricts the session to those mounts but keeps each at its own mode, and a bare -m /data on the CLI does the same for one mount. A session’s mode can only narrow, never widen: the effective permission is the weaker of the mount’s own mode and the session’s mode, so rw on a READ mount is still read-only. This is a soft boundary, enforced inside the daemon process, not an OS or process-level isolation. Use it to shrink the blast radius of prompt-injection in multi-agent workspaces: a Slack-only agent cannot pivot to read /linear, /github, or any other mount it was not given. Note that FUSE mounts are not scoped by session modes: a FUSE mount is a host-level surface served under the default unrestricted view (mount modes still apply, session narrowing does not). The check fires for every code path that reaches a mount: shell commands (cat, ls, …), redirects (>, <), cross-mount cp/mv, wget -O, curl -o, command substitution $(...), subshells (...), pipes, &&/|| chains, background jobs, and the programmatic ws.ops.read/write/... API. Infrastructure prefixes are always accessible: the history view (/.bash_history, which the history builtin and the GNU histfile render from) and the implicit scratch root (/, where stateless text-processing commands like wc resolve when given no path). A user-defined / mount is not infrastructure; sessions must be given / explicitly to touch it.
The modes are a property of the session, so they cover every command issued under that session_id, including subshells, pipelines, and recursive bash -c '...'. They do not change the mount’s own MountMode: a write to a session-writable mount is still rejected if the mount itself is READ. The two checks compose.

Agent Pattern

Agent harnesses commonly fan out tool calls in parallel, each with its own cwd/env/cancel. The clone semantics make this race-free without per-call boilerplate. From the CLI, a subshell per call gives the same isolation.