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 + env
lifo snapshot save <id> --output box.tar.gz
lifo snapshot restore box.tar.gz

What'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 filesystem

lifo-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 ./work

The 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.