BoxVFS in two runtimes:
@struktoai/mirage-node, usesclient_id+client_secret+ refresh token (rotated on each use)@struktoai/mirage-browser, supports the same refresh token via PKCE (no client secret in the bundle)
/folders/{id}/items, /files/{id}/content, /search, plus multipart upload, folder
create, delete, rename/move, and copy for writes). Credentials are obtained the same way
in both runtimes, see Box Credentials.
Both READ and WRITE modes are supported (mount with { mode: MountMode.WRITE } to
enable tee/cp/mv/rm/mkdir/touch). Pass rootFolderId to mount a single Box
folder as the workspace root instead of the whole account; folder ids are stable across
renames and moves. The Python package ships the same backend, see
Box (Python).
Quick start: developer token
For exploration, skip the OAuth flow and use a developer token (one-button-click in the Box app console, 60-minute lifetime). TheBoxVFS accepts an accessToken field that
short-circuits the refresh logic:
invalid_token), regenerate it in the console and
re-run. See Box Credentials -> Quick Start.
Service account (client credentials, no refresh token)
For headless server auth, skip OAuth and refresh tokens entirely. Create a Box app with the Server Authentication (Client Credentials Grant) method, authorize it once in the Box admin console (Apps -> Custom Apps Manager), and pass the enterprise ID:Node (server-side, long-running)
BoxTokenManager caches the access token in memory (5-minute safety buffer before expiry)
and rotates the refresh token on every refresh. Without onRefreshTokenRotated, the rotation
is in-memory only and a process restart needs a fresh refresh token.
Config
Fields marked secret are redacted in snapshots.
Browser (PKCE, no client secret)
examples/browser/vfs/box/box_pkce.ts
runs the full PKCE dance and persists the rotated refresh token to localStorage.
VFS mode (patchNodeFs)
fs.promises.* and the callbacks) are routed; a sync form like fs.statSync refuses a mounted path.
FUSE mode
Path → ID resolution
Box uses numeric folder/file IDs internally (root folder is id0). Mirage caches the path → ID
mapping in the mount’s index store (RAM unless an index: is configured). The first time you ls /box/foo/bar/,
it walks the tree top-down (one API call per level) and caches each entry’s ID. Subsequent reads
of /box/foo/bar/anyfile.json hit the cache instead of re-walking.
With no index: configured, listings live for the mount’s ttl (600 s by default), at most 24 hours; a configured index: uses its own ttl, still capped by the mount’s ttl:. To force re-resolution, unmount the prefix and mount a new BoxVFS there.
Writes
A write records thesha1 its upload reply names, so a fresh read
right after a tee or sed -i downloads nothing.
Conditional writes
A Box mount acceptswrite: conditional. A
write or a file delete compares the sha1 mirage holds with the one the
folder listing it walks anyway shows, and refuses a file changed since it
was read before sending anything; the request then carries the file’s
etag as If-Match, so Box refuses (412) a change that lands in between.
A conditional write costs no request more than a plain one. rm -r walks
the folder file by file, and a mv within the mount is Box’s own move,
which is not refused when its source changed (a mv to another mount is a
copy and a conditioned delete, so there a changed source stays and mv
reports the failed removal).
Reads under fresh
A Box mount accepts read: fresh. The token is the file’s sha1, which a
folder listing and GET /files/{id} both carry; a download names no version,
so a whole read stamps the listed sha1 only when the downloaded bytes hash
to it, and a ranged read stamps nothing. On a
write: conditional mount a whole read stamps
the sha1 of the bytes it downloaded, so a listing another client has since
outdated cannot leave the next write without the version the agent read. modified_at is never the token:
two same-size edits in one second share it.
Checking a cached file costs one request, GET /files/{id} by the id the
mount last listed, while the mount still holds that file’s listing row. The
answer is trusted only when Box places the same active file at the same path
from the mount root; a file that was moved, renamed, trashed or recreated
falls back to listing each folder from the mount root, one listing per level
(up to two per level when the file is gone). When that fallback confirms
byte-identical content, the built-in RAM and Redis indexes retain the newly
resolved file row if the original hint is still current. A later check then
uses the recovered id without refreshing the parent listing. Custom indexes
must implement replaceIfUnchanged to retain this row; otherwise later checks
repeat the fallback walk. A persistent 403
still requires the failed point request and a listing walk on each check.
Any stale, unknown or gone verdict on the mount
drops its cached listings, so the checks after one walk until a cold read or
an ls lists the folders again. A file Box reports without a sha1 cannot
be verified: each fresh read of it downloads it again and drops the mount’s
cached listings.
A folder’s etag does not move when its children change, so under fresh
listings are re-fetched once per command. Repeated uses within that command
share the result; outside commands, the existing one-second listing trust
window applies. Before fetching a nested listing, mirage resolves its folder through current
parent listings up to the configured mount root. A folder moved away and
replaced under the same name is therefore resolved to the replacement on
the next fresh command. This costs one listing per ancestor level; a warm
file check still uses one metadata request.
Box-native file types
Every Box item is served as its raw bytes, keeping its real name (no.json suffix).
Box has no API to edit these formats from a structured payload, so Mirage does not render
them, it hands back exactly what Box stores:
.boxnote/.boxcanvas, Box’s native Notes and Canvas, stored as ProseMirror-style JSON. They are text, socat foo.boxnote | jq .works if you want to poke at the structure..gdoc/.gsheet/.gslides, Box’s V2 Google-format files, stored as Office Open XML zips (.docx/.xlsx/.pptx). These are opaque binary, like any other Office document on a Mirage mount.
gws docs ...
commands operate on Google file IDs and do not apply to Box.
VFS.md
The VFS.md an agent reads with Box mounted at/<path>.
- Rendered
- Raw
Virtual Filesystem
/<path> layout
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 to list every command, and man <cmd> or <cmd> --help for one command’s usage.Integration
What Box answers through its own API, so fewer files are read. Integration explains each command.
The Python Box docs list when each search answers and what it can miss.
Available commands
BoxVFS ships the same shell command set as GoogleDriveVFS and DropboxVFS:
- Filesystem:
ls,cat,head,tail,nl,wc,stat,find,tree,du,file,realpath,basename,dirname - Writes (in
WRITEmode):tee,cp,mv,rm(-r),mkdir(-p),touch,truncate - Search/text:
grep,rg,awk,sed,sort,uniq,cut,diff,cmp,jq - Encoding:
base64
Examples
End-to-end runnable scripts are inexamples/typescript/vfs/box/:
box.ts,Workspace.shell('ls/stat/cat/tree …')shell demobox_vfs.ts,patchNodeFs+ nativefs.promises.*callsbox_fuse.ts, FUSE mount with shell access in another terminal