File System Access

When Elide runs your code, that code is a guest of the runtime. The runtime decides what the guest can touch -- including the host file system. This page documents how that gate actually works: the isolation model, where the policy is decided, and how to change it. It is about I/O isolation, not the node:fs module surface. For the list of fs functions Elide implements, see the Node Filesystem API.

The isolation model

Every guest execution runs inside an Elide runtime context. File-system reachability is a property of that context, controlled by an I/O policy and a backing virtual file system. Elide builds each context in two stages, and the second stage has the final say.

Stage 1: the runtime builds a locked-down context

When the runtime creates a context, it starts from a deliberately closed posture. In ElideRuntime.newContext() the builder is configured with:

  • allowIO(cachedIoAccess) -- where cachedIoAccess is built with allowHostFileAccess(false) and allowHostSocketAccess(false).
  • allowCreateThread(false), allowNativeAccess(false), allowHostClassLoading(false), allowPolyglotAccess(PolyglotAccess.NONE).

Process creation is not part of this lockdown -- the base context is built with allowCreateProcess(true).

The backing file system itself is chosen by the embedding builder. ElideRuntimeBuilder exposes a FileSystemOptions.FileSystemMode with three values and a default of OFF:
ModeBacking file system
OFFA deny-I/O file system (newDenyIOFileSystem())
READ_ONLYThe default file system wrapped read-only
READ_WRITEThe default file system, unrestricted
In every mode the runtime additionally wraps the file system with allowInternalResourceAccess(...) so the engine can always load its own embedded resources (language runtimes, polyfills) regardless of guest policy.

Stage 2: runtime components configure the context

After the base context is built, Elide applies its runtime components in order. Components are hard-wired in HardWiredComponentResolver, and one of them -- IOAccessComponent -- decides the final I/O posture from the sandbox policy:

  • No sandbox configured (the default): the component grants allowIO(IOAccess.ALL). Because allowIO is last-write-wins on the runtime context builder, this overrides the Stage 1 posture, so in a plain elide run / elide serve invocation guest code has host file-system access.
  • A sandbox policy is configured: instead of allow-all, the component installs a capability-gated file system (ElideGuestFileSystem) plus a shared FsPolicy. The same FsPolicy gates both node:fs and the polyglot guest file system, so one policy governs every language.
File-system access is controlled by CLI flags. —sandbox switches the guest to deny-by-default; —allow-read[=PATHS] and —allow-write[=PATHS] grant read/write access (repeatable and comma-scoped — the bare form grants everything, =PATHS scopes it to specific paths), and both imply —sandbox; —fs-audit logs each access decision. Without any of these flags the runtime keeps the historical allow-all posture. See the Filesystem Sandbox guide for details. The embedding FileSystemMode knob (ElideRuntimeBuilder.fileSystem { ... }) still exists for programmatic embedders.

Debug mode

When the debugger is active (debugger settings present and enabled), DebuggerComponent widens the context further still -- allowAllAccess(true), allowCreateProcess(true), allowNativeAccess(true), allowIO(IOAccess.ALL), and allowEnvironmentAccess(INHERIT). This is intentional: an attached debugger is already a fully trusted principal. These grants do not apply to normal, non-debug runs.

What this means for your code

Reading and writing files works the way you would expect from Node:

javascript
import { readFileSync, writeFileSync } from "node:fs";

const text = readFileSync("hello.txt", { encoding: "utf-8" });
writeFileSync("out.txt", text.toUpperCase());

The same files are reachable from every guest language layered on the same runtime, because the IOAccess policy and the backing file system are properties of the context, not of any one language. Paths resolve against the host file system; relative paths resolve against the process working directory.

Internal engine resources are always reachable independent of the I/O policy -- that is what allowInternalResourceAccess guarantees -- so module loading and language bootstrap never depend on host-file permissions.

Embedding: choosing a stricter posture

If you embed Elide's runtime rather than invoking the CLI, you control the file-system posture through the builder:

kotlin
val runtime = ElideRuntime.newBuilder(languages)
  .fileSystem { mode = FileSystemOptions.FileSystemMode.READ_ONLY }
  // ...components that do NOT re-open I/O...
  .build()

Two things must both hold for a restricted posture to survive to runtime:

1. Select the FileSystemMode you want (OFF or READ_ONLY). 2. Ensure no later component re-grants IOAccess.ALL. The default HardWiredComponentResolver includes IOAccessComponent, which does exactly that; supply your own component set via componentsFrom(...) if you need the lockdown to hold.

Summary

  • The runtime core defaults to closed: no host file access, no host sockets, no process creation, default FileSystemMode.OFF.
  • With no sandbox configured, the shipped CLI re-opens full I/O via the hard-wired IOAccessComponent (allowIO(IOAccess.ALL)), so day-to-day elide run/elide serve code can read and write files normally.
  • File-system access is gated by the --sandbox, --allow-read, --allow-write, and --fs-audit CLI flags; without them the shipped CLI keeps the allow-all posture. Embedders can also set the posture through the builder and the active component set.
  • The node:fs module is the surface you actually call -- see the Node Filesystem API for the implemented function set.

What's next