Skip to main content

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:
A command’s limit comes from the first place that names it: the session’s profile, the mount the command runs on, the workspace, then the built-in default. A command that spans several mounts takes the tightest of their limits. An entry replaces that command’s whole limit, so restate timeout_seconds if you still want a deadline; commands it does not name keep their defaults.
  • max_lines / max_bytes cap the output; null means no cap.
  • timeout_seconds is a deadline; a command that runs past it exits 124.
  • on_exceed: truncate (default) keeps the capped output, adds a stderr notice, and leaves the exit code alone. on_exceed: error drops the output and exits 1, so && and || see the failure.
Caps apply to what each command prints, one command at a time: 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 local lancedb). A backend that caches listings but no bytes (disk, chroma, qdrant, airtable, wandb) accepts fresh, because its listings are checked, and so does any mount whose index: (its own or the workspace’s) keeps listings for a nonzero ttl;
  • or it caches reads but stamps no token the check can compare, because its stat and its read return different kinds of value, or its read stamps nothing.
The VFS matrix lists which backends accept fresh today.

The bound

A bare ttl: 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:
  1. the running command wrote it;
  2. 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);
  3. 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.
Otherwise the folder is listed again, and that re-list is trusted for the rest of the command. A read that belongs to no command (FUSE, a programmatic op such as 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:
Existing listings in a persisted Redis index retain their stored expiry until rewritten.

A mount’s own index

A mount’s index: 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.
The mount’s 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 no read: — the bound pins nothing.
  • read: bounded written out with no ttl: — an explicit policy with an implicit bound.
An unset 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 through mounts= 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, so ttl: 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.
Making the reader’s bound authoritative would mean recording when each entry was written somewhere every store can read it back — Redis keeps no such timestamp today. Until then, treat a shared cache as a place where 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

The slack, 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.
Chat selects messages by creation time; attachments follow those messages. Calendar selects events overlapping the interval and retains their original payloads, including events that cross a bound. Only day directories intersecting the interval are exposed. Calendar day names use the mount’s 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.
Without either bound, Slack and Discord retain their recent listing defaults (90 and 30 days respectively). Configuring a bound makes the selected interval discoverable. Bounds are enforced on reads, metadata, listings, and VFS searches; separately installed account CLIs retain their own access. Read-only does not freeze upstream changes. Change bounds by rebuilding/remounting the VFS, which clears its cached state.