Snapshots
Save a box as a portable .tar.gz and restore it anywhere — the same file works in the browser, in Node and through the CLI.
A snapshot is the whole box in one file: the filesystem, plus the working directory and environment it had. One format everywhere — a snapshot taken by the CLI restores in the browser, and vice versa.
const bytes = await sandbox.exportSnapshot(); // Uint8Array (.tar.gz)
await other.importSnapshot(bytes); // files + cwd + envlifo snapshot save <id> --output box.tar.gz
lifo snapshot restore box.tar.gzWhat's inside
It's an ordinary gzipped tar, so you can look:
tar tzf box.tar.gz | head
# lifo-snapshot.json ← the manifest
# /home/user/… ← the filesystemlifo-snapshot.json carries the session state:
{
"version": 1,
"savedAt": "2026-07-28T12:00:00.000Z",
"cwd": "/home/user/project",
"env": { "MY_VAR": "value" },
"mountPath": "/Users/me/project"
}Every VFS path starts with /, so the manifest's relative name can't collide
with a real file — and it is never restored as a file.
Note
Earlier versions had two formats: core wrote this .tar.gz while the CLI
wrote a zip wrapping a JSON-serialized filesystem, because the tar had nowhere to
keep cwd/env. Neither could read the other's snapshots. The manifest removes
the reason for the split. Zip snapshots from lifo ≤ 0.9.0 are not readable — save
a fresh one from a running session.
Excluding node_modules
node_modules usually dominates the size. Skip it and reinstall on the other side:
const bytes = await sandbox.exportSnapshot({ exclude: ['node_modules', '.git'] });A restored box then needs npm install before it can run. For an Expo project,
pruneExpoModules is the middle ground — it keeps only what
Metro actually reads, so the box still runs without a reinstall.
Files only
To leave session state out — publishing a template, say, where the author's cwd
and environment are noise:
const bytes = await sandbox.exportSnapshot({ metadata: false });importSnapshot() returns null for such an archive and leaves cwd/env
alone. That's also exactly how a pre-manifest snapshot behaves, which is why old
.tar.gz files still restore.
Reading the manifest on its own
Sometimes a decision depends on the metadata before you have anywhere to restore
into — the CLI picks which host directory to mount based on mountPath:
import { readSnapshotMetadata } from "@lifo-sh/core";
const manifest = await readSnapshotMetadata(bytes);
console.log(manifest?.cwd, manifest?.mountPath);In the browser
The playground's box menu exports a snapshot as a download and restores one from a
file picker; it's the same sandbox.exportSnapshot() underneath. Export yields a
Uint8Array, so hand it to a Blob for downloading and read a File back with
arrayBuffer():
const bytes = await sandbox.exportSnapshot({ exclude: ["node_modules"] });
const url = URL.createObjectURL(new Blob([bytes], { type: "application/gzip" }));
// …offer `url` as a download, then URL.revokeObjectURL(url)
// restoring a picked file
await sandbox.importSnapshot(new Uint8Array(await file.arrayBuffer()));Large snapshots are exported without blocking the UI — the walk yields to the event loop periodically, so a big tree doesn't freeze the tab.
In the CLI
lifo list # find the session id
lifo snapshot save 87f5e4 # → ~/.lifo/snapshots/87f5e4-<ts>.tar.gz
lifo snapshot save 87f5e4 -o box.tar.gz
lifo snapshot restore box.tar.gz # boots a session and attaches
lifo snapshot restore box.tar.gz --mount ./workThe archive is built by the daemon, which owns the filesystem, and handed back
over its socket. On restore, mountPath from the manifest is reused when that
directory still exists; otherwise a temp directory is created. --mount always
wins.