Filesystem
nano.fs is a typed view of the VM's in-memory POSIX filesystem. Reads and single-file writes are synchronous; directory mutations run BusyBox under the VM lock and are async; archives and lazy files cover bulk loading.
Every runner sees the same filesystem: files you write here are visible to BusyBox, Node.js, wasm apps and sandboxed scripts. Over the worker transport the same methods exist but every one returns a promise.
Fast path (sync)
| Option | Type | Default | Description |
|---|---|---|---|
writeFile(path, content, mode?) | void | mode 0o644 | Create or overwrite a file; parent directories are created. content is a string or Uint8Array. |
readText(path) | string | null | — | UTF-8 text, or null when missing. |
readFile(path) | Uint8Array | null | — | Raw bytes, or null. |
list(path) | DirEntry[] | null | — | Directory entries { name, type: "file" | "dir" | "symlink", size }. |
exists(path) | boolean | — | True for an existing file or directory. |
walk(root?) | string[] | "/" | Every file path under root, recursively. |
Sync reads and writes
No VM step and no await: ideal for seeding inputs before a run and reading results after.
nano.fs.writeFile("/work/src/index.js", "export const x = 1;\n");
nano.fs.writeFile("/work/data.bin", new Uint8Array([1, 2, 3]));
print(nano.fs.readText("/work/src/index.js").trim());
print(nano.fs.readFile("/work/data.bin"));
print(nano.fs.exists("/work/src"), nano.fs.exists("/work/missing"));
print(JSON.stringify(nano.fs.list("/work/src")));export const x = 1;
Uint8Array(3)
true false
[{"name":"index.js","type":"file","size":20}]
── /work ──
/work/data.bin
/work/src/index.jsFile modes
The guest enforces the execute bit, so pass a mode such as 0o755 for scripts and binaries you want to run. Catalog installs do this for you.
writeFile with mode 0o755
nano.fs.writeFile("/work/hello.sh", "#!/bin/sh\necho running $0\n", 0o755);
nano.fs.writeFile("/work/plain.sh", "#!/bin/sh\necho never\n");
print((await nano.shExec("ls -l /work/hello.sh /work/plain.sh | cut -c1-10; /work/hello.sh; /work/plain.sh")).stdout.trim());-rwxr-xr-x -rw-r--r-- running /work/hello.sh /tmp/.nano-exec-0.sh: line 1: /work/plain.sh: Permission denied ── /work ── /work/data.bin /work/hello.sh /work/plain.sh /work/src/index.js
Mutations (async)
Directory operations run the real BusyBox tool, so they have shell semantics (recursive delete, attribute-preserving copy, rename over). They take the VM lock and throw nano-sdk: fs op failed (exit N): <command> with the tool's message on failure.
| Option | Type | Default | Description |
|---|---|---|---|
mkdir(path) | Promise<void> | — | mkdir -p. |
remove(path) | Promise<void> | — | rm -rf. |
move(from, to) | Promise<void> | — | mv. |
copy(from, to) | Promise<void> | — | cp -a. |
mkdir, copy, move, remove
await nano.fs.mkdir("/work/a/b/c");
nano.fs.writeFile("/work/a/b/c/note.txt", "hi");
await nano.fs.copy("/work/a", "/work/a-copy");
await nano.fs.move("/work/a-copy/b", "/work/moved");
await nano.fs.remove("/work/a");
try {
await nano.fs.move("/work/nope", "/work/x");
} catch (e) {
print(e.message.split("\n")[0]);
}nano-sdk: fs op failed (exit 1): mv '/work/nope' '/work/x' ── /work ── /work/data.bin /work/hello.sh /work/plain.sh /work/moved/c/note.txt /work/src/index.js
Archives
loadTarGz(bytes) unpacks a gzipped tar archive into the filesystem at the paths it contains. It is how overlays are applied at boot (see image.overlays) and works on npm package tarballs.
Unpack an npm tarball
Fetches left-pad-1.3.0.tgz from the npm registry (CORS-enabled) and unpacks it. npm tarballs put everything under package/.
const res = await fetch("https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz");
await nano.fs.loadTarGz(new Uint8Array(await res.arrayBuffer()));
print(nano.fs.walk("/package").filter((f) => !f.includes("/perf/")).sort().join("\n"));
print((await nano.node(["-e", "console.log(require('/package/index.js')('7', 3, '0'))"])).stdout.trim());/package/.travis.yml /package/COPYING /package/README.md /package/index.d.ts /package/index.js /package/package.json /package/test.js 007 ── /work ── /work/data.bin /work/hello.sh /work/plain.sh /work/moved/c/note.txt /work/src/index.js
Lazy files
registerLazyFile(path, { size, mode, resolve }) records a file whose bytes are produced by resolve() the first time a guest program opens it. The catalog's lazy: true installs use this. Until it is materialized, exists() reports false.
registerLazyFile
let calls = 0;
nano.fs.registerLazyFile("/work/lazy.txt", {
size: 23,
mode: "0644",
resolve: async () => {
calls++;
return new TextEncoder().encode("materialized on demand\n");
},
});
print("resolve() calls before:", calls);
print((await nano.shExec("cat /work/lazy.txt; cat /work/lazy.txt")).stdout.trim());
print("resolve() calls after:", calls);resolve() calls before: 0 materialized on demand materialized on demand resolve() calls after: 1
The cli-toolkit example seeds files and runs catalog tools over them.
ln -s inside the guest doesn't create symlinks (there is no symlinkat syscall yet; ln reports "Function not implemented"), so list() reports symlink entries only for links created by the image, such as the BusyBox applets in /bin. See Limitations.