What It Does
A workspace can be described entirely in YAML and loaded with one call —load_config in Python, loadWorkspaceConfigFile in TypeScript. This page is the key-by-key reference. Both languages read the same document and refuse the same things: an unknown key is an error, not a warning, so a typo fails loudly at load rather than silently doing nothing.
Top level
Mount block
Command limits
cat, grep, rg, head, and tail cap their output at 2000 lines, and
every command stops after 600 seconds. Change that with command_limits in
three places: at the top level for every session, on a mount for commands
that run on it, and on a profile for sessions created with it:
timeout_seconds if you still want a deadline; commands it does not name keep
their defaults.
max_lines/max_bytescap the output;nullmeans no cap.timeout_secondsis a deadline; a command that runs past it exits124.on_exceed: truncate(default) keeps the capped output, adds a stderr notice, and leaves the exit code alone.on_exceed: errordrops the output and exits1, so&&and||see the failure.
cat big.txt; echo end still prints end. Data going into a pipe, a
redirect, or $(...) is never cut, so cat big.txt | wc -l counts every
line. See Output Limits for the SDK form.
The read policy
read: bounded serves cached bytes and cached directory listings without asking the backend, for as long as ttl: allows. read: fresh revalidates against the backend’s content token before serving a cached copy, at the cost of one backend stat per file read, and checks a cached listing before serving it: against the listing’s stored version where the backend has one, otherwise by re-listing the folder once per command. See listings under fresh.
fresh is refused at mount time on a backend that cannot honour it, rather than quietly behaving as bounded:
- the backend caches neither reads nor listings, so the check has nothing to run against (
ram,redis,postgres,mongodb, a locallancedb). A backend that caches listings but no bytes (disk,chroma,qdrant,airtable,wandb) acceptsfresh, because its listings are checked, and so does any mount whoseindex:(its own or the workspace’s) keeps listings for a nonzerottl; - or it caches reads but stamps no token the check can compare, because its
statand its read return different kinds of value, or its read stamps nothing.
fresh today.
The bound
A barettl: lives only in a mount block. At the top level it would sit beside index: {ttl: ...} and mean a different thing, so the workspace-level default is read: alone and a workspace-level bounded takes 600 seconds. Inside a mount the two are told apart by nesting: ttl: is the bound, and a mount’s own index: {ttl: ...} is how long its listing store keeps a listing, never longer than that bound.
Each listing write is capped by the mount’s ttl, under both policies. Backends may expire listings sooner, and mounts without a listing cache still keep none. fresh also checks listings, so a file added or removed outside mirage shows in the next ls, find or glob. A cached listing is served when:
- the running command wrote it;
- the mount is a GitHub mount pinned to a full commit sha and the listing was fetched at it, with no request (a Hugging Face Hub mount never pins);
- its stored version still matches the backend’s: one check per command for a GitHub or Hub mount, one local stat per folder on disk.
ws.vfs, an agent’s file tools) trusts a listing written, or a version check sent, in the last second, so calls in a short ls -l burst over FUSE can share one re-list or one check; a burst lasting beyond the window can re-list again, and an outside change can stay hidden from those reads for up to that second.
Disk versions each folder by its change time, which assumes a local POSIX filesystem. For a network or FUSE root (NFS, SMB, rclone, s3fs, mirage’s own FUSE), whose folder change time may not move when its entries do, turn the versions off in the mount’s config:; that mount then re-lists once per command:
A mount’s own index
A mount’sindex: block takes the top-level index: keys and replaces that block for this mount, whole: nothing is inherited from it, so type: is required, a missing ttl: is 600 seconds, and a Redis store’s url: and key_prefix: take the block’s own defaults, not the top-level ones. A mount without one takes the top-level index:, and with neither, the backend’s own listing lifetime (Index TTL). index: null is the same as leaving it out. The file cache has no per-mount block; a mount’s cache: is refused.
ttl: still caps every listing write, so a mount index: {ttl: ...} can only shorten how long a listing lives; /drive keeps listings for a day because its ttl: is raised to match. fresh is judged on the mount’s own index: a zero ttl there takes listing-only fresh away from disk even under a nonzero top-level index:.
Two shapes are refused, because a read/ttl pair that disagrees is almost always a typo:
ttl:with noread:— the bound pins nothing.read: boundedwritten out with nottl:— an explicit policy with an implicit bound.
read: is a different thing and is fine: it takes the default.
Across a snapshot
A snapshot records each mount’s policy and bound, and a restore keeps them — but only for a mount the loader rebuilds from the saved state itself. A mount handed back throughmounts= takes the default (bounded, 600s) instead.
That is deliberate: a mount saved with redacted credentials has to be handed
back, and what comes back may be a different backend entirely. Replaying a saved
fresh onto one that cannot revalidate would refuse a restore that had nothing
wrong with it. The saved policy still has to be a policy mirage knows — a
snapshot naming an unknown one is refused whether or not the mount is overridden.
Declare read: again on an overridden mount if you want it back.
What the bound promises
The bound is stamped on a cache entry when that entry is written, and the store expires it from there. So the bound that applies to an entry is the one the mount that wrote it declared, not the one the mount reading it declares. Within a workspace those are the same mount, sottl: means what it says.
They can differ in three situations, and in each the older bound wins until the
entry expires on its own:
- two workspaces sharing one Redis cache declare different
ttl:for the same prefix; ttl:is lowered and the process restarts against a surviving shared cache;- a snapshot is restored into a mount whose
ttl:differs from the one that took the snapshot.
ttl: should agree
across the workspaces that use it.
Not yet accepted
read: pinned is refused, naming the layer it needs: pinning reads to the content a commit records requires a version layer mirage does not have. There is no write: key yet. Both are reserved rather than silently ignored, so a config that names one fails at load instead of appearing to work.
Time-scoped chat and calendar mounts
Theslack, discord, and gcal backend config blocks accept start_time
and end_time: RFC3339 timestamps with a timezone and at most millisecond
precision. Start is inclusive and end is exclusive; either may be omitted or
null. An empty or reversed interval is refused. The same snake_case fields
work through build_vfs in Python and buildVfs in TypeScript.
time_zone (or its
primary calendar’s timezone); chat day names use UTC. Context such as users,
channels, and calendar.json remains available.
Calendar’s bare listing includes all past event days. Without end_time, future
discovery stops at the end of today +90 days; explicit date paths/globs can reach
further within the configured scope. With end_time, that explicit endpoint
replaces the default horizon. Calendar refuses a listing still incomplete after
50 API pages; narrow the range instead of receiving a silently truncated result.
A gcal block also takes bucket_days: 1 (the default), 7 or 30, the days
each date directory covers. With 7 or 30 a directory is named by its first
and last day, 2026-08-10--2026-08-16/, on a fixed grid where 7-day buckets run
Monday to Sunday, and each event file name carries its day
(<eventId>__2026-08-11_0900-1030_<Title>.gcal.json). One listing then answers a
week or a month of events. Other values are refused.