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.
Python
TypeScript
- CLI
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).
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.
Python
TypeScript
- CLI
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 -nprintshi -n),read -r,xargs -n/-0/-d/-r/--(batching, GNU exit codes:123when an invocation fails,126/127stop the run),timeout DURATIONwiths/m/h/dsuffixes (kills at the deadline with exit124, usage errors exit125).shiftandreturnreport bash’snumeric argument requirederrors. - Globs:
*,?,[...]classes and[!...]negation (Pythonfnmatchsemantics 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,pswork; use the--backgroundflag andmirage jobCLI 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(exit1) andtimeout -s/-k/--preserve-status(exit125) return anunsupported optionerror: commands run as coroutines inside the workspace, so there is no process to signal or parallelize.
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 returnexit_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 volumero 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.
Python
- CLI
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 owncwd/env/cancel. The clone semantics make this race-free without per-call boilerplate. From the CLI, a subshell per call gives the same isolation.
Python
TypeScript
- CLI