Skip to main content
MIRAGE ships S3VFS in two runtimes:
  • @struktoai/mirage-node, signs requests server-side using @aws-sdk/client-s3.
  • @struktoai/mirage-browser, stays secret-free: your backend hands out presigned URLs and the browser fetches them with fetch().
Credentials (IAM user, access keys) are created the same way in both runtimes, see AWS S3 Credentials.

Node (server-side)

If ~/.aws/credentials is configured, you can omit accessKeyId/secretAccessKey and the AWS SDK picks up the default profile. Under write: conditional a write carries the version of the file mirage read, and S3 refuses it when the file has changed since. The endpoint decides which writes can be conditioned: AWS’s conditions every write, copy and delete; any other endpoint (or AWS_ENDPOINT_URL_S3/AWS_ENDPOINT_URL) only a write, so mirage refuses what would copy or delete there. An endpoint set only in an AWS profile is not read: give a non-AWS server its endpoint. A browser mount signed by presigned URLs cannot carry a condition, so it cannot declare write: conditional. If the session last saw no file at the path, the write sends If-None-Match: * and fails if someone created the file in the meantime. A conditional single-file move attempts rollback when source deletion is refused. The move still fails and warns if the destination may remain. Cleanup uses the copy’s S3 version or, for a new destination, its ETag. Unversioned overwrites, same-ETag replacements, retried versioned copies, and uncertain delete outcomes limit recovery; inspect both paths after a failure. An absent destination must remain absent until copying. This guard may add one metadata request; cleanup adds one delete and may require s3:DeleteObjectVersion. Directory and cross-mount moves do not use rollback. A directory mv, rm -r or rmdir looks at the directory again when it is done: a file written into it that the command never listed stays, and the command fails with Directory not empty. Under write: conditional, a directory mv that finds a file changed before it could copy it removes nothing from the source; the copies it made stay at the destination. An append or another rewrite that observes a missing file uses create-only commit under either write policy. S3 checks If-None-Match: * at PUT or final multipart completion, so a delayed creation cannot replace a competing writer’s bytes. A lost condition returns an error without automatic retry. Existing-file appends under unconditional retain last-write-wins behavior.

Config

Fields marked secret are redacted in snapshots.

Browser (presigned URLs)

The browser S3VFS never sees AWS credentials. You implement one function, presignedUrlProvider, that your frontend calls to get a signed URL for each S3 operation. The actual signing happens on your server, using any S3-signing SDK you prefer.

Config

Fields marked secret are redacted in snapshots. The browser S3VFS can also sign requests itself, without presignedUrlProvider: it then takes the credential fields of the Node config, which puts the keys in the browser.

1. Server: sign URLs on demand

Using @aws-sdk/s3-request-presigner:
Scope the presigner to the minimal operations your app needs. Read-only viewers only need GET/HEAD/LIST; a file manager needs the full set.

2. Browser: wire it up

Customize a builtin command

The generic command definitions are available before a workspace is created. Copy the definition with the behavior you want to change, then register that copy on one mount:
Use the same generic export from @struktoai/mirage-node for a Node workspace. The builtin definition and other mounts stay unchanged. Registration is a host application action; an agent can discover and run the resulting cat command, but cannot alter the command table.

3. Configure CORS on the bucket

Presigned URLs reach the S3 host directly from the browser. Your bucket must allow cross-origin requests from your app’s origin, otherwise every fetch() fails with TypeError: Failed to fetch (the browser drops the response at the CORS preflight).
Or use the helper in the browser example, it signs the PutBucketCors call with your existing env credentials and applies the same policy to all configured buckets in one shot:
CORS is origin-exact. http://localhost:5173 ≠ http://localhost:5174 ≠ https://app.example.com. Include every origin you’ll serve the page from.
See S3 Setup for credential setup.

Refused keys

When credentials cannot read an S3 object (S3 returns AccessDenied, or a bodiless HTTP 403 response on a HEAD request), Mirage reports EACCES (Permission denied) for that file operand. Shell commands follow standard POSIX conventions:
  • cat a b c prints the contents of a and c, writes a permission error for b to stderr, and exits with a non-zero status.
  • grep -r logs an error on inaccessible files and exits with status 2.
  • cp b x || fallback triggers the fallback branch.
  • test -e b evaluates to false.

IAM permissions and existence checks

  • Scoped grants: When access is granted below a specific prefix (for example, s3:GetObject on bucket/dir/* alongside s3:ListBucket on the bucket), Mirage treats dir as a directory during stat.
  • Missing ListBucket: Without s3:ListBucket permission, S3 returns HTTP 403 even for non-existent keys to avoid leaking existence. As a result, non-existent files report Permission denied (EACCES) instead of No such file or directory (ENOENT).
  • Listed but unreadable keys: If ListObjectsV2 includes an object that your credentials cannot read, stat and test -e report that the file exists, while read attempts (cat) fail with Permission denied.
  • Browser presigned URLs: Mirage does not parse the HTTP error body when reading through presigned URLs in the browser. Any HTTP 403 status (including expired or misconfigured URLs) is handled as a permission refusal.

Scoping a VFS to a key prefix

Pass keyPrefix to scope every operation to a subpath of the bucket:
When set, every read/write/list/stat/copy/rename/delete operation is transparently scoped to that bucket subpath. Agents see clean paths like /data/notes.md; the underlying bucket key is users/${userId}/data/notes.md. Useful for multi-tenant systems. Pairs naturally with STS AssumeRole session policies for AWS-side enforcement. Unset behavior is unchanged. Normalization: leading slashes are stripped and a trailing slash is added automatically. Both undefined and an empty string are treated as “no prefix.” In YAML-based configs the snake-case spelling key_prefix is also accepted and is automatically mapped to keyPrefix at construction.
Browser variant — security noteFor mirage-browser’s S3VFS (which uses presignedUrlProvider), keyPrefix flows through the same core code path, but a client-controlled prefix is not a security boundary — a malicious client can ignore or alter it before signing. For real isolation, enforce the prefix inside your server-side presignedUrlProvider implementation (and in the IAM/STS session policy backing those credentials). Treat the client-side keyPrefix as ergonomics only.

VFS.md

The VFS.md an agent reads with S3 mounted at /<path>.

Virtual Filesystem

Supported Unix Commands for VFS

Unix commands work on each path as on a local filesystem, as far as the path supports them:

Limitations of Unix Commands for VFS

Where a path differs from a local filesystem:Run man <cmd> or <cmd> --help for one command’s usage.

Integration

What S3 answers through its own API, so fewer files are read. Integration explains each command.