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)-- wherecachedIoAccessis built withallowHostFileAccess(false)andallowHostSocketAccess(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).
ElideRuntimeBuilder
exposes a FileSystemOptions.FileSystemMode with three values and a default of OFF:
| Mode | Backing file system |
|---|---|
OFF | A deny-I/O file system (newDenyIOFileSystem()) |
READ_ONLY | The default file system wrapped read-only |
READ_WRITE | The default file system, unrestricted |
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). BecauseallowIOis last-write-wins on the runtime context builder, this overrides the Stage 1 posture, so in a plainelide run/elide serveinvocation 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 sharedFsPolicy. The sameFsPolicygates bothnode:fsand the polyglot guest file system, so one policy governs every language.
—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:
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:
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-dayelide run/elide servecode can read and write files normally. - File-system access is gated by the
--sandbox,--allow-read,--allow-write, and--fs-auditCLI 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:fsmodule is the surface you actually call -- see the Node Filesystem API for the implemented function set.
What's next
- Node Filesystem API -- the
node:fsmodule surface - Filesystem primitives (host-internal) -- the NIO-backed layer that
node:fsand the WHATWG file APIs are built on - Environment Variables -- the companion host-exposure model for env