Skip to main content
You can create a custom VirtualFileSystem to back the sandbox with any storage layer — a database, S3, a zip archive, or anything else. Sandboxed code uses fs, require, and other Node APIs as normal, and your implementation handles the actual I/O.

The interface

Your class must implement VirtualFileSystem from secure-exec-core:
import type { VirtualFileSystem, VirtualStat, VirtualDirEntry } from "secure-exec";

class MyFileSystem implements VirtualFileSystem {
  async readFile(path: string): Promise<Uint8Array> { /* ... */ }
  async readTextFile(path: string): Promise<string> { /* ... */ }
  async writeFile(path: string, content: string | Uint8Array): Promise<void> { /* ... */ }
  async readDir(path: string): Promise<string[]> { /* ... */ }
  async readDirWithTypes(path: string): Promise<VirtualDirEntry[]> { /* ... */ }
  async createDir(path: string): Promise<void> { /* ... */ }
  async mkdir(path: string): Promise<void> { /* ... */ }
  async exists(path: string): Promise<boolean> { /* ... */ }
  async stat(path: string): Promise<VirtualStat> { /* ... */ }
  async removeFile(path: string): Promise<void> { /* ... */ }
  async removeDir(path: string): Promise<void> { /* ... */ }
  async rename(oldPath: string, newPath: string): Promise<void> { /* ... */ }
  async symlink(target: string, linkPath: string): Promise<void> { /* ... */ }
  async readlink(path: string): Promise<string> { /* ... */ }
  async lstat(path: string): Promise<VirtualStat> { /* ... */ }
  async link(oldPath: string, newPath: string): Promise<void> { /* ... */ }
  async chmod(path: string, mode: number): Promise<void> { /* ... */ }
  async chown(path: string, uid: number, gid: number): Promise<void> { /* ... */ }
  async utimes(path: string, atime: number, mtime: number): Promise<void> { /* ... */ }
  async truncate(path: string, length: number): Promise<void> { /* ... */ }
}

Example: read-only Map filesystem

A minimal filesystem backed by a Map<string, string>. Useful when you have a fixed set of files (e.g. loaded from a database) and want to make them available to sandboxed code.
import type { VirtualFileSystem, VirtualStat, VirtualDirEntry } from "secure-exec";
import {
  NodeRuntime,
  allowAllFs,
  createNodeDriver,
  createNodeRuntimeDriverFactory,
} from "secure-exec";

class ReadOnlyMapFS implements VirtualFileSystem {
  private files: Map<string, string>;

  constructor(files: Record<string, string>) {
    this.files = new Map(Object.entries(files));
  }

  async readFile(path: string) {
    const content = this.files.get(path);
    if (content === undefined) throw new Error(`ENOENT: ${path}`);
    return new TextEncoder().encode(content);
  }

  async readTextFile(path: string) {
    const content = this.files.get(path);
    if (content === undefined) throw new Error(`ENOENT: ${path}`);
    return content;
  }

  async exists(path: string) {
    return this.files.has(path) || this.#isDir(path);
  }

  async stat(path: string): Promise<VirtualStat> {
    const now = Date.now();
    if (this.files.has(path)) {
      return {
        mode: 0o444,
        size: new TextEncoder().encode(this.files.get(path)!).byteLength,
        isDirectory: false,
        atimeMs: now, mtimeMs: now, ctimeMs: now, birthtimeMs: now,
      };
    }
    if (this.#isDir(path)) {
      return {
        mode: 0o555,
        size: 0,
        isDirectory: true,
        atimeMs: now, mtimeMs: now, ctimeMs: now, birthtimeMs: now,
      };
    }
    throw new Error(`ENOENT: ${path}`);
  }

  async lstat(path: string) { return this.stat(path); }

  async readDir(path: string) {
    const prefix = path === "/" ? "/" : path + "/";
    const entries = new Set<string>();
    for (const key of this.files.keys()) {
      if (key.startsWith(prefix)) {
        const rest = key.slice(prefix.length);
        entries.add(rest.split("/")[0]);
      }
    }
    if (entries.size === 0) throw new Error(`ENOENT: ${path}`);
    return [...entries];
  }

  async readDirWithTypes(path: string): Promise<VirtualDirEntry[]> {
    const names = await this.readDir(path);
    const prefix = path === "/" ? "/" : path + "/";
    return names.map((name) => ({
      name,
      isDirectory: this.#isDir(prefix + name),
    }));
  }

  // Write operations throw — this filesystem is read-only
  async writeFile() { throw new Error("EROFS: read-only filesystem"); }
  async createDir() { throw new Error("EROFS: read-only filesystem"); }
  async mkdir() { throw new Error("EROFS: read-only filesystem"); }
  async removeFile() { throw new Error("EROFS: read-only filesystem"); }
  async removeDir() { throw new Error("EROFS: read-only filesystem"); }
  async rename() { throw new Error("EROFS: read-only filesystem"); }
  async symlink() { throw new Error("EROFS: read-only filesystem"); }
  async readlink() { throw new Error("ENOSYS: no symlinks"); }
  async link() { throw new Error("EROFS: read-only filesystem"); }
  async chmod() { throw new Error("EROFS: read-only filesystem"); }
  async chown() { throw new Error("EROFS: read-only filesystem"); }
  async utimes() { throw new Error("EROFS: read-only filesystem"); }
  async truncate() { throw new Error("EROFS: read-only filesystem"); }

  #isDir(path: string) {
    const prefix = path === "/" ? "/" : path + "/";
    for (const key of this.files.keys()) {
      if (key.startsWith(prefix)) return true;
    }
    return false;
  }
}

Using it

const fs = new ReadOnlyMapFS({
  "/config.json": JSON.stringify({ greeting: "hello" }),
  "/src/index.js": `
    const config = JSON.parse(require("fs").readFileSync("/config.json", "utf8"));
    console.log(config.greeting);
  `,
});

const runtime = new NodeRuntime({
  systemDriver: createNodeDriver({
    filesystem: fs,
    permissions: { ...allowAllFs },
  }),
  runtimeDriverFactory: createNodeRuntimeDriverFactory(),
});

const result = await runtime.exec(`
  const config = JSON.parse(require("fs").readFileSync("/config.json", "utf8"));
  console.log(config.greeting);
`);

console.log(result.stdout); // "hello\n"
runtime.dispose();

Tips

  • Throw Node-style errors. Sandboxed code expects errors like ENOENT, EACCES, and EISDIR. Match the error message prefix so fs error handling works naturally.
  • Normalize paths. All paths are absolute POSIX paths (forward slashes, rooted at /). Normalize before lookup to avoid mismatches.
  • No-op what you don’t need. Operations like chmod, chown, and utimes can be no-ops if your storage layer doesn’t support them. Throw ENOSYS for operations that genuinely can’t work (like symlinks on a flat key-value store).
  • Use createInMemoryFileSystem() as a reference. The built-in in-memory filesystem is a complete implementation you can study or extend.