Log inGet started

vfs

Updated 6 September 2026

The vfs namespace — 76 functions.

globals/vfs/clearPlayShadow

vfs.clearPlayShadow() -> boolean

Forget the entire play-shadow set after a bulk promote or discard. Tracking only — never touches the bytes.

Returns boolean — Always true.

vfs.clearPlayShadow()

globals/vfs/copy

vfs.copy(src: string, dst: string) -> (boolean, string?)

Copy a file OR directory from src to dst, cp -r style. A directory recurses — every descendant is replicated at the same relative path under dst, .refs sidecars included. .meta sidecars are minted fresh, so a copy is a distinct asset with its own identity. Both paths are absolute. The source may live in any layer (writable, library mount, builtin, runtime-generated); the destination must be a writable route.

Parameters

  • src string — Source absolute VFS path (file or directory).
  • dst string — Destination absolute VFS path.

Returns (boolean, string?) — True on success; (false, errmsg) on failure.

vfs.copy("/zero/runtime/recordings/take1.mp4", "/zero/source/clips/take1.mp4")

globals/vfs/currentAuthor

vfs.currentAuthor() -> { id: string, name: string? }?

The agent this call is attributed to — the author a /source write made right now would be recorded under in the play shadow. Nil when the call carries no actor identity, which is the case for engine-authored work and for a caller that presented no token. Compare its id against vfs.playShadowAuthors() to separate your own pending edits from a co-author's.

Returns { id: string, name: string? }?{ id, name } for the acting agent, or nil when unattributed.

local me = vfs.currentAuthor()
print(if me ~= nil then me.id else "unattributed")

globals/vfs/evict

vfs.evict(path: string, opts: VfsOpts?) -> boolean

Drop the in-memory bytes for path from the writable MemFs layer without removing the asset. Use after processing large binaries to reclaim RAM.

Parameters

  • path string — VFS path whose bytes should be evicted.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns boolean — True if MemFs bytes were dropped; false otherwise.

vfs.evict("/zero/source/textures/imported_big.png")

globals/vfs/exists

vfs.exists(path: string, opts: VfsOpts?) -> boolean

Is the path known to the VFS? Checks the Stage-1 metadata (.meta sidecar / ManifestView) — NOT "are the bytes locally cached?". Use vfs.read(path) ~= nil to confirm bytes are reachable.

Parameters

  • path string — VFS path to check.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns boolean — True if the path is known regardless of byte cache state.

assert(vfs.exists("@builtin/models/Cube"))

globals/vfs/isDirectory

vfs.isDirectory(path: string, opts: VfsOpts?) -> boolean

Is ONE path a directory? Answers from reality — the writable layer's children, a resolver-served folder listing, an explicit empty-directory marker — so a loose file whose extension collides with an assetType name (notes.json) reads as the file it is while a real <name>.<type>/ folder reads as a folder. Costs the same whatever the containing folder holds; use vfs.list when you want every entry's kind, this when you hold one path.

Parameters

  • path string — VFS path to classify.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns boolean — True if the path is a directory.

if vfs.isDirectory("/zero/source/Goblin.dynamicAsset") then print("folder asset") end

globals/vfs/isSaveExcluded

vfs.isSaveExcluded(path: string, opts: VfsOpts?) -> boolean

Does this path hold content the machine keeps to itself? /source/tmp/ is session scratch and /source/local/ is this machine's own durable content — each directory itself included, and everything under it. Both are writable, hot-reloadable and enumerable like the rest of /source/; what separates them is where they stop. The engine filters them out of every world save and every peer broadcast, so they reach no world, carry no manifest row there, and a staging verb handed one refuses it by name. The match reads a whole path segment, so /source/tmpfoo/ is ordinary content. Ask here whenever your code has to agree with what a world can hold.

Parameters

  • path string — VFS path to classify.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns boolean — True when the path is held back from world saves and sync.

if not vfs.isSaveExcluded(p) then table.insert(publishable, p) end

globals/vfs/list

vfs.list(path: string?) -> { VfsListEntry }

List entries in a VFS directory.

Parameters

  • path string (optional) — Directory path (defaults to /zero).

Returns { VfsListEntry } — Array of { name, isDirectory } tables.

for _, e in ipairs(vfs.list("/zero/source")) do print(e.name) end

globals/vfs/memResident

vfs.memResident() -> { { path: string, bytes: number, kind: string } }

List the MemFs entries that are NOT resident-by-default — the writable in-memory layer's binary blobs and its large text files (text at or above the inline-text size threshold). These are the bytes vfs.evict can reclaim: the ones kept in RAM rather than left to fall through to the on-disk BlobStore cache. Small text (resident by default) is omitted. The audit counterpart to vfs.evict and to reading with { keep = true } — use it to see what encoded bytes are held in RAM, and why.

Returns { { path: string, bytes: number, kind: string } } — Array of { path, bytes, kind }; kind is "binary" for non-text content and "large-text" for oversized text. Empty when the VFS isn't up.

for _, e in ipairs(vfs.memResident()) do print(e.path, e.bytes, e.kind) end

globals/vfs/mkdir

vfs.mkdir(path: string, opts: VfsOpts?) -> boolean

Create a directory. mkdir -p semantics — idempotent. Errors if a file already exists at the same path. While play is running an authored /source directory waits for the lock to lift and the refusal RAISES with the reason; writing a file under the path creates it as part of that write, and scratch under /source/tmp/ creates as in edit mode.

Parameters

  • path string — Directory path.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns boolean — True if the directory exists after the call. A creation the running play session refuses raises with the whole reason.

vfs.mkdir("/zero/source/scenes/")

globals/vfs/move

vfs.move(src: string, dst: string, opts: { quiet: boolean? }?) -> (boolean, string?)

Move a file from src to dst. By default fires the destination's write side effects; pass opts.quiet = true to suppress them. While play is running a move takes the source away, so authored /source content that predates play is refused and the refusal RAISES with the reason; content this play session created moves and stays tracked on the play shadow.

Parameters

  • src string — Source absolute VFS path.
  • dst string — Destination absolute VFS path.
  • opts { quiet: boolean? } (optional) — Optional { quiet: boolean? }.

Returns (boolean, string?) — True on success; (false, errmsg) when the paths themselves refuse it. A move the running play session refuses raises with the whole reason instead.

vfs.move("/zero/source/a.luau", "/zero/source/b.luau")

globals/vfs/mutationSeq

vfs.mutationSeq() -> number

Lifetime count of VFS mutations the engine has APPLIED — the drain's clock. A write queues its side effects (an asset's content reload, the assetType's onChange, a component or scene registration) and a later frame runs them; this number advances as each one completes. Read it, write, then poll for a larger value to learn the queue has moved past the point you wrote at — instead of waiting a guessed number of frames. It counts every mutation kind, so it answers about the pipeline rather than about one file; asset.reloadSeq(ref) is the per-asset reading.

Returns number — Count of applied VFS mutations this session. Monotonic.

local at = vfs.mutationSeq()
vfs.write("/zero/source/tmp/note.txt", "hi")
repeat task.wait() until vfs.mutationSeq() > at

globals/vfs/pendingWrites

vfs.pendingWrites() -> { string }

List the /source paths with an in-flight local write the synced manifest has not reflected yet — the read-your-writes frontier. A just-written file appears here until its upload round-trips and the synced dirty state catches up; world.vcsStatus unions these so a fresh edit reads back as dirty immediately. Empty when fully synced.

Returns { string } — Array of VFS paths with pending (unconfirmed) local writes.

for _, p in ipairs(vfs.pendingWrites()) do print(p) end

globals/vfs/playShadowAuthors

vfs.playShadowAuthors() -> { [string]: { id: string, name: string? } }

The agent behind each currently-shadowed /source path: the ZeroMind user id the write was attributed to, and the username to show for it. Several agents drive one engine at once and every one of their in-play source edits sits in the same shadow set, so this is how a review, a refusal or a verdict tells one agent's pending work from another's. A path written with no actor identity carries no entry — it belongs to no agent in particular, and stays settleable by any of them.

Returns { [string]: { id: string, name: string? } } — Map of shadowed path to { id, name }.

local mine = vfs.currentAuthor()
for path, who in pairs(vfs.playShadowAuthors()) do
if mine == nil or who.id ~= mine.id then print(path, "belongs to", who.name) end
end

globals/vfs/playShadowPaths

vfs.playShadowPaths() -> { string }

List the /source paths edited during running play that are currently held as copy-on-write SHADOWS (MemFs-only, on-disk original untouched) — the universal play shadow-copy set. These are the in-play edits persist promotes over the originals on confirm, or drops on a guarded discard. Empty outside play or when nothing was edited.

Returns { string } — Array of normalized VFS paths currently shadowed.

for _, p in ipairs(vfs.playShadowPaths()) do print(p) end

globals/vfs/promotePlayShadow

vfs.promotePlayShadow(path: string) -> string

Promote a single play-shadow edit into a canonical write. Re-asserts the live overlay bytes through the full write pipeline with the play write lock released, then unmarks the path. The bytes stay in the engine end to end, so binary content promotes exactly. Takes ONE file path, and needs the write lock released, so run it inside a pause you take and hand back. A promotion that cannot happen raises with the reason: the path is not shadowed, the path is a folder covering shadowed edits, play is running, the workspace is read-only, or the write-through failed. A shadow entry whose bytes are gone is dropped as promoted, so the path comes back with nothing written for it.

Parameters

  • path string — Shadowed VFS path to promote (one of vfs.playShadowPaths()).

Returns string — The path the call settled — it is no longer shadowed.

engine.paused = true
local promoted = vfs.promotePlayShadow("/zero/source/cover.jpg")
engine.paused = false

globals/vfs/read

vfs.read(path: string, opts: VfsOpts?) -> string?

Read a file from the virtual filesystem. Binary-safe. Returns file contents as a string, or nil if the file is not known. Relative paths resolve under opts.root (default /source/). When called from a coroutine and the bytes aren't locally cached, transparently yields the coroutine while the lazy fetch runs.

Parameters

  • path string — VFS path.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns string? — File contents (binary-safe), or nil.

local src = vfs.read("@builtin/components/Camera.luau")

globals/vfs/readAsync

vfs.readAsync(path: string, opts: VfsOpts?) -> string

Asynchronous binary-safe read. Returns a promise ID that resolves to the file contents. Useful for reading render textures from the main thread without blocking.

Parameters

  • path string — VFS path.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns string — Promise ID — pass to task.await().

local data = task.await(vfs.readAsync("/zero/runtime/screenshots/last.png"))

globals/vfs/reload

vfs.reload(modulePath: string?) -> boolean

Clear entries from the require() cache so the next require(name) re-runs the module's source. Pass a single module identity to drop only that entry; call with no arguments to drop every cached module.

Parameters

  • modulePath string (optional) — Module identity to reload (omit to reload all).

Returns boolean — For a single identity, whether a module was cached under that name and has now been dropped — false says the name matched nothing. The no-arg form returns true.

vfs.reload("@mylib/utils.helpers")

globals/vfs/remove

vfs.remove(path: string, opts: VfsOpts?) -> (boolean, string?)

Remove a file. Refuses to remove directories unless opts.recursive = true. Refuses protected system roots. While play is running, authored /source content that predates play is refused and the refusal RAISES with the reason; content this play session created is removable, a folder included.

Parameters

  • path string — VFS path to remove.
  • opts VfsOpts (optional){ root = "/source/", recursive = false }.

Returns (boolean, string?) — True on success; (false, errmsg) when the path itself refuses the removal. A removal the running play session refuses raises with the whole reason instead, so a pcall around the call reads it — and the lock answers ahead of whether the path is there, so a locked /source path raises whether or not it holds anything.

vfs.remove("/zero/source/scratch.luau")

globals/vfs/revertPlayShadow

vfs.revertPlayShadow(path: string) -> string

Revert a single play-shadow edit: restore the pre-play copy captured at the first play-mode write (the last edit-mode state, unstaged edits included) into the live slot — or remove the file when it did not exist at that moment — then unmark the path. Hot-reload picks the original back up, so the running session actually reverts. Takes ONE file path, and needs the write lock released, so run it inside a pause you take and hand back. A revert that cannot happen raises with the reason, on the same terms as vfs.promotePlayShadow.

Parameters

  • path string — Shadowed VFS path to revert (one of vfs.playShadowPaths()).

Returns string — The path that is now reverted and no longer shadowed.

engine.paused = true
local reverted = vfs.revertPlayShadow("/zero/source/Foo.component/init.luau")
engine.paused = false

globals/vfs/unmarkPlayShadow

vfs.unmarkPlayShadow(path: string) -> boolean

Forget a single play-shadow path after it has been promoted (saved over source) or discarded. Tracking only — never touches the bytes.

Parameters

  • path string — VFS path to unmark.

Returns boolean — Always true.

vfs.unmarkPlayShadow("/zero/source/Foo.component/init.luau")

globals/vfs/unwatch

vfs.unwatch(watcherId: number) -> boolean

Remove a previously registered VFS watcher.

Parameters

  • watcherId number — Watcher id returned by vfs.watch.

Returns boolean — True if the watcher was found and removed.

vfs.unwatch(id)

globals/vfs/watch

vfs.watch(path: string, callback: (string, string) -> ()) -> number

Register a callback that fires when a VFS path is written or removed. Two match modes: exact, or folder/prefix (key ends with /, and fires for any descendant). The callback runs in the VM that registered it. Returns a watcher id for vfs.unwatch.

Parameters

  • path string — Exact path, or folder path ending in /.
  • callback (string, string) -> ()(mutated_path, kind) -> (), kind "write" or "remove".

Returns number — Watcher id.

local id = vfs.watch("/zero/source/", function(path, kind) print(kind, path) end)

globals/vfs/write

vfs.write(path: string, content: string, opts: VfsOpts?) -> (boolean, string?)

Write content to a file. Binary-safe. Overwrites existing files by default — pass opts.overwrite = false to refuse to clobber. While play is running a /source write lands on the play shadow: it succeeds and reads back, live in the session with disk source untouched, and is discarded on a guarded play-exit unless accepted. Scratch under /source/tmp/ writes through untouched. Pass opts.durable = true to say these bytes ARE the source: the write reaches canonical /source with play still running and the session still in play, hot-reloading the modules and components that read it, so the edit is observed running in the same play session with nothing left to promote. A durable write RAISES with the reason when the bytes cannot become canonical source.

Parameters

  • path string — VFS path to write to.
  • content string — File content (binary-safe).
  • opts VfsOpts (optional){ root = "/source/", overwrite = true, quiet = false, durable = false }.

Returns (boolean, string?) — True on success; on failure returns false + error message. A durable write raises instead of returning false.

vfs.write("/zero/source/notes.md", body)
vfs.write("/zero/source/game/Vent.component/init.luau", src, { durable = true })

modules/vfs/README

require("@builtin/modules/api/engine/vfs") -- vfs (also available as global 'vfs')

Virtual filesystem — read, write, list, watch. Public Luau surface over the __vfs Internal FFI namespace.

Usage: local vfs = require("@builtin/modules/api/engine/vfs") Also available as global: vfs

modules/vfs/clearPlayShadow

clearPlayShadow(): boolean

Forget the entire play-shadow set after a bulk promote or discard. Tracking only — never touches the bytes.

vfs.clearPlayShadow()

modules/vfs/copy

copy(src: string, dst: string): (boolean, string?)

Copy a file OR directory from src to dst, cp -r style. A directory recurses — every descendant is replicated at the same relative path under dst, .refs sidecars included. .meta sidecars are minted fresh, so a copy is a distinct asset with its own identity. Both paths are absolute. The source may live in any layer (writable, library mount, builtin, runtime-generated); the destination must be a writable route.

Parameters

  • src string — Source absolute VFS path (file or directory).
  • dst string — Destination absolute VFS path.
vfs.copy("/zero/runtime/recordings/take1.mp4", "/zero/source/clips/take1.mp4")

modules/vfs/currentAuthor

currentAuthor(): { id: string, name: string? }?

The agent this call is attributed to — the author a /source write made right now would be recorded under in the play shadow. Nil when the call carries no actor identity, which is the case for engine-authored work and for a caller that presented no token. Compare its id against vfs.playShadowAuthors() to separate your own pending edits from a co-author's.

local me = vfs.currentAuthor()
print(if me ~= nil then me.id else "unattributed")

modules/vfs/evict

evict(path: string, opts: VfsOpts?): boolean

Drop the in-memory bytes for path from the writable MemFs layer without removing the asset. Use after processing large binaries to reclaim RAM.

Parameters

  • path string — VFS path whose bytes should be evicted.
  • opts VfsOpts? (optional){ root = "/source/" }.
vfs.evict("/zero/source/textures/imported_big.png")

modules/vfs/exists

exists(path: string, opts: VfsOpts?): boolean

Is the path known to the VFS? Checks the Stage-1 metadata (.meta sidecar / ManifestView) — NOT "are the bytes locally cached?". Use vfs.read(path) ~= nil to confirm bytes are reachable.

Parameters

  • path string — VFS path to check.
  • opts VfsOpts? (optional){ root = "/source/" }.
assert(vfs.exists("@builtin/models/Cube"))

modules/vfs/isDirectory

isDirectory(path: string, opts: VfsOpts?): boolean

Is ONE path a directory? Answers from reality — the writable layer's children, a resolver-served folder listing, an explicit empty-directory marker — so a loose file whose extension collides with an assetType name (notes.json) reads as the file it is while a real <name>.<type>/ folder reads as a folder. Costs the same whatever the containing folder holds; use vfs.list when you want every entry's kind, this when you hold one path.

Parameters

  • path string — VFS path to classify.
  • opts VfsOpts? (optional){ root = "/source/" }.
if vfs.isDirectory("/zero/source/Goblin.dynamicAsset") then print("folder asset") end

modules/vfs/isSaveExcluded

isSaveExcluded(path: string, opts: VfsOpts?): boolean

Does this path hold content the machine keeps to itself? /source/tmp/ is session scratch and /source/local/ is this machine's own durable content — each directory itself included, and everything under it. Both are writable, hot-reloadable and enumerable like the rest of /source/; what separates them is where they stop. The engine filters them out of every world save and every peer broadcast, so they reach no world, carry no manifest row there, and a staging verb handed one refuses it by name. The match reads a whole path segment, so /source/tmpfoo/ is ordinary content. Ask here whenever your code has to agree with what a world can hold.

Parameters

  • path string — VFS path to classify.
  • opts VfsOpts? (optional){ root = "/source/" }.
if not vfs.isSaveExcluded(p) then table.insert(publishable, p) end

modules/vfs/list

list(path: string?): { VfsListEntry }

List entries in a VFS directory.

Parameters

  • path string? (optional) — Directory path (defaults to /zero).
for _, e in ipairs(vfs.list("/zero/source")) do print(e.name) end

modules/vfs/memResident

memResident(): { { path: string, bytes: number, kind: string } }

List the MemFs entries that are NOT resident-by-default — the writable in-memory layer's binary blobs and its large text files (text at or above the inline-text size threshold). These are the bytes vfs.evict can reclaim: the ones kept in RAM rather than left to fall through to the on-disk BlobStore cache. Small text (resident by default) is omitted. The audit counterpart to vfs.evict and to reading with { keep = true } — use it to see what encoded bytes are held in RAM, and why.

for _, e in ipairs(vfs.memResident()) do print(e.path, e.bytes, e.kind) end

modules/vfs/mkdir

mkdir(path: string, opts: VfsOpts?): boolean

Create a directory. mkdir -p semantics — idempotent. Errors if a file already exists at the same path. While play is running an authored /source directory waits for the lock to lift and the refusal RAISES with the reason; writing a file under the path creates it as part of that write, and scratch under /source/tmp/ creates as in edit mode.

Parameters

  • path string — Directory path.
  • opts VfsOpts? (optional){ root = "/source/" }.
vfs.mkdir("/zero/source/scenes/")

modules/vfs/move

move(src: string, dst: string, opts: { quiet: boolean? }?): (boolean, string?)

Move a file from src to dst. By default fires the destination's write side effects; pass opts.quiet = true to suppress them. While play is running a move takes the source away, so authored /source content that predates play is refused and the refusal RAISES with the reason; content this play session created moves and stays tracked on the play shadow.

Parameters

  • src string — Source absolute VFS path.
  • dst string — Destination absolute VFS path.
  • opts { quiet: boolean? }? (optional) — Optional { quiet: boolean? }.
vfs.move("/zero/source/a.luau", "/zero/source/b.luau")

modules/vfs/mutationSeq

mutationSeq(): number

Lifetime count of VFS mutations the engine has APPLIED — the drain's clock. A write queues its side effects (an asset's content reload, the assetType's onChange, a component or scene registration) and a later frame runs them; this number advances as each one completes. Read it, write, then poll for a larger value to learn the queue has moved past the point you wrote at — instead of waiting a guessed number of frames. It counts every mutation kind, so it answers about the pipeline rather than about one file; asset.reloadSeq(ref) is the per-asset reading.

local at = vfs.mutationSeq()
vfs.write("/zero/source/tmp/note.txt", "hi")
repeat task.wait() until vfs.mutationSeq() > at

modules/vfs/pendingWrites

pendingWrites(): { string }

List the /source paths with an in-flight local write the synced manifest has not reflected yet — the read-your-writes frontier. A just-written file appears here until its upload round-trips and the synced dirty state catches up; world.vcsStatus unions these so a fresh edit reads back as dirty immediately. Empty when fully synced.

for _, p in ipairs(vfs.pendingWrites()) do print(p) end

modules/vfs/playShadowAuthors

playShadowAuthors(): { [string]: { id: string, name: string? } }

The agent behind each currently-shadowed /source path: the ZeroMind user id the write was attributed to, and the username to show for it. Several agents drive one engine at once and every one of their in-play source edits sits in the same shadow set, so this is how a review, a refusal or a verdict tells one agent's pending work from another's. A path written with no actor identity carries no entry — it belongs to no agent in particular, and stays settleable by any of them.

local mine = vfs.currentAuthor()
for path, who in pairs(vfs.playShadowAuthors()) do
if mine == nil or who.id ~= mine.id then print(path, "belongs to", who.name) end
end

modules/vfs/playShadowPaths

playShadowPaths(): { string }

List the /source paths edited during running play that are currently held as copy-on-write SHADOWS (MemFs-only, on-disk original untouched) — the universal play shadow-copy set. These are the in-play edits persist promotes over the originals on confirm, or drops on a guarded discard. Empty outside play or when nothing was edited.

for _, p in ipairs(vfs.playShadowPaths()) do print(p) end

modules/vfs/promotePlayShadow

promotePlayShadow(path: string): string

Promote a single play-shadow edit into a canonical write. Re-asserts the live overlay bytes through the full write pipeline with the play write lock released, then unmarks the path. The bytes stay in the engine end to end, so binary content promotes exactly. Takes ONE file path, and needs the write lock released, so run it inside a pause you take and hand back. A promotion that cannot happen raises with the reason: the path is not shadowed, the path is a folder covering shadowed edits, play is running, the workspace is read-only, or the write-through failed. A shadow entry whose bytes are gone is dropped as promoted, so the path comes back with nothing written for it.

Parameters

  • path string — Shadowed VFS path to promote (one of vfs.playShadowPaths()).
engine.paused = true
local promoted = vfs.promotePlayShadow("/zero/source/cover.jpg")
engine.paused = false

modules/vfs/read

read(path: string, opts: VfsOpts?): string?

Read a file from the virtual filesystem. Binary-safe. Returns file contents as a string, or nil if the file is not known. Relative paths resolve under opts.root (default /source/). When called from a coroutine and the bytes aren't locally cached, transparently yields the coroutine while the lazy fetch runs.

Parameters

  • path string — VFS path.
  • opts VfsOpts? (optional){ root = "/source/" }.
local src = vfs.read("@builtin/components/Camera.luau")

modules/vfs/readAsync

readAsync(path: string, opts: VfsOpts?): string

Asynchronous binary-safe read. Returns a promise ID that resolves to the file contents. Useful for reading render textures from the main thread without blocking.

Parameters

  • path string — VFS path.
  • opts VfsOpts? (optional){ root = "/source/" }.
local data = task.await(vfs.readAsync("/zero/runtime/screenshots/last.png"))

modules/vfs/reload

reload(modulePath: string?): boolean

Clear entries from the require() cache so the next require(name) re-runs the module's source. Pass a single module identity to drop only that entry; call with no arguments to drop every cached module.

Parameters

  • modulePath string? (optional) — Module identity to reload (omit to reload all).
vfs.reload("@mylib/utils.helpers")

modules/vfs/remove

remove(path: string, opts: VfsOpts?): (boolean, string?)

Remove a file. Refuses to remove directories unless opts.recursive = true. Refuses protected system roots. While play is running, authored /source content that predates play is refused and the refusal RAISES with the reason; content this play session created is removable, a folder included.

Parameters

  • path string — VFS path to remove.
  • opts VfsOpts? (optional){ root = "/source/", recursive = false }.
vfs.remove("/zero/source/scratch.luau")

modules/vfs/revertPlayShadow

revertPlayShadow(path: string): string

Revert a single play-shadow edit: restore the pre-play copy captured at the first play-mode write (the last edit-mode state, unstaged edits included) into the live slot — or remove the file when it did not exist at that moment — then unmark the path. Hot-reload picks the original back up, so the running session actually reverts. Takes ONE file path, and needs the write lock released, so run it inside a pause you take and hand back. A revert that cannot happen raises with the reason, on the same terms as vfs.promotePlayShadow.

Parameters

  • path string — Shadowed VFS path to revert (one of vfs.playShadowPaths()).
engine.paused = true
local reverted = vfs.revertPlayShadow("/zero/source/Foo.component/init.luau")
engine.paused = false

modules/vfs/unmarkPlayShadow

unmarkPlayShadow(path: string): boolean

Forget a single play-shadow path after it has been promoted (saved over source) or discarded. Tracking only — never touches the bytes.

Parameters

  • path string — VFS path to unmark.
vfs.unmarkPlayShadow("/zero/source/Foo.component/init.luau")

modules/vfs/unwatch

unwatch(watcherId: number): boolean

Remove a previously registered VFS watcher.

Parameters

  • watcherId number — Watcher id returned by vfs.watch.
vfs.unwatch(id)

modules/vfs/watch

watch(path: string, callback: (string, string) -> ()): number

Register a callback that fires when a VFS path is written or removed. Two match modes: exact, or folder/prefix (key ends with /, and fires for any descendant). The callback runs in the VM that registered it. Returns a watcher id for vfs.unwatch.

Parameters

  • path string — Exact path, or folder path ending in /.
  • callback (string, string) -> ()(mutated_path, kind) -> (), kind "write" or "remove".
local id = vfs.watch("/zero/source/", function(path, kind) print(kind, path) end)

modules/vfs/write

write(path: string, content: string, opts: VfsOpts?): (boolean, string?)

Write content to a file. Binary-safe. Overwrites existing files by default — pass opts.overwrite = false to refuse to clobber. While play is running a /source write lands on the play shadow: it succeeds and reads back, live in the session with disk source untouched, and is discarded on a guarded play-exit unless accepted. Scratch under /source/tmp/ writes through untouched. Pass opts.durable = true to say these bytes ARE the source: the write reaches canonical /source with play still running and the session still in play, hot-reloading the modules and components that read it, so the edit is observed running in the same play session with nothing left to promote. A durable write RAISES with the reason when the bytes cannot become canonical source.

Parameters

  • path string — VFS path to write to.
  • content string — File content (binary-safe).
  • opts VfsOpts? (optional){ root = "/source/", overwrite = true, quiet = false, durable = false }.
vfs.write("/zero/source/notes.md", body)
vfs.write("/zero/source/game/Vent.component/init.luau", src, { durable = true })

typed/builtin//modules/api/engine/vfs/vfs/clearPlayShadow

vfs.clearPlayShadow() -> boolean

Forget the entire play-shadow set after a bulk promote or discard. Tracking only — never touches the bytes.

Returns boolean — Always true.

vfs.clearPlayShadow()

typed/builtin//modules/api/engine/vfs/vfs/copy

vfs.copy(src: string, dst: string) -> (boolean, string?)

Copy a file OR directory from src to dst, cp -r style. A directory recurses — every descendant is replicated at the same relative path under dst, .refs sidecars included. .meta sidecars are minted fresh, so a copy is a distinct asset with its own identity. Both paths are absolute. The source may live in any layer (writable, library mount, builtin, runtime-generated); the destination must be a writable route.

Parameters

  • src string — Source absolute VFS path (file or directory).
  • dst string — Destination absolute VFS path.

Returns (boolean, string?) — True on success; (false, errmsg) on failure.

vfs.copy("/zero/runtime/recordings/take1.mp4", "/zero/source/clips/take1.mp4")

typed/builtin//modules/api/engine/vfs/vfs/currentAuthor

vfs.currentAuthor() -> { id: string, name: string? }?

The agent this call is attributed to — the author a /source write made right now would be recorded under in the play shadow. Nil when the call carries no actor identity, which is the case for engine-authored work and for a caller that presented no token. Compare its id against vfs.playShadowAuthors() to separate your own pending edits from a co-author's.

Returns { id: string, name: string? }?{ id, name } for the acting agent, or nil when unattributed.

local me = vfs.currentAuthor()
print(if me ~= nil then me.id else "unattributed")

typed/builtin//modules/api/engine/vfs/vfs/evict

vfs.evict(path: string, opts: VfsOpts?) -> boolean

Drop the in-memory bytes for path from the writable MemFs layer without removing the asset. Use after processing large binaries to reclaim RAM.

Parameters

  • path string — VFS path whose bytes should be evicted.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns boolean — True if MemFs bytes were dropped; false otherwise.

vfs.evict("/zero/source/textures/imported_big.png")

typed/builtin//modules/api/engine/vfs/vfs/exists

vfs.exists(path: string, opts: VfsOpts?) -> boolean

Is the path known to the VFS? Checks the Stage-1 metadata (.meta sidecar / ManifestView) — NOT "are the bytes locally cached?". Use vfs.read(path) ~= nil to confirm bytes are reachable.

Parameters

  • path string — VFS path to check.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns boolean — True if the path is known regardless of byte cache state.

assert(vfs.exists("@builtin/models/Cube"))

typed/builtin//modules/api/engine/vfs/vfs/isDirectory

vfs.isDirectory(path: string, opts: VfsOpts?) -> boolean

Is ONE path a directory? Answers from reality — the writable layer's children, a resolver-served folder listing, an explicit empty-directory marker — so a loose file whose extension collides with an assetType name (notes.json) reads as the file it is while a real <name>.<type>/ folder reads as a folder. Costs the same whatever the containing folder holds; use vfs.list when you want every entry's kind, this when you hold one path.

Parameters

  • path string — VFS path to classify.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns boolean — True if the path is a directory.

if vfs.isDirectory("/zero/source/Goblin.dynamicAsset") then print("folder asset") end

typed/builtin//modules/api/engine/vfs/vfs/isSaveExcluded

vfs.isSaveExcluded(path: string, opts: VfsOpts?) -> boolean

Does this path hold content the machine keeps to itself? /source/tmp/ is session scratch and /source/local/ is this machine's own durable content — each directory itself included, and everything under it. Both are writable, hot-reloadable and enumerable like the rest of /source/; what separates them is where they stop. The engine filters them out of every world save and every peer broadcast, so they reach no world, carry no manifest row there, and a staging verb handed one refuses it by name. The match reads a whole path segment, so /source/tmpfoo/ is ordinary content. Ask here whenever your code has to agree with what a world can hold.

Parameters

  • path string — VFS path to classify.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns boolean — True when the path is held back from world saves and sync.

if not vfs.isSaveExcluded(p) then table.insert(publishable, p) end

typed/builtin//modules/api/engine/vfs/vfs/list

vfs.list(path: string?) -> { VfsListEntry }

List entries in a VFS directory.

typed/builtin//modules/api/engine/vfs/vfs/memResident

vfs.memResident() -> { { path: string, bytes: number, kind: string } }

List the MemFs entries that are NOT resident-by-default — the writable in-memory layer's binary blobs and its large text files (text at or above the inline-text size threshold). These are the bytes vfs.evict can reclaim: the ones kept in RAM rather than left to fall through to the on-disk BlobStore cache. Small text (resident by default) is omitted. The audit counterpart to vfs.evict and to reading with { keep = true } — use it to see what encoded bytes are held in RAM, and why.

Returns { { path: string, bytes: number, kind: string } } — Array of { path, bytes, kind }; kind is "binary" for non-text content and "large-text" for oversized text. Empty when the VFS isn't up.

for _, e in ipairs(vfs.memResident()) do print(e.path, e.bytes, e.kind) end

typed/builtin//modules/api/engine/vfs/vfs/mkdir

vfs.mkdir(path: string, opts: VfsOpts?) -> boolean

Create a directory. mkdir -p semantics — idempotent. Errors if a file already exists at the same path. While play is running an authored /source directory waits for the lock to lift and the refusal RAISES with the reason; writing a file under the path creates it as part of that write, and scratch under /source/tmp/ creates as in edit mode.

Parameters

  • path string — Directory path.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns boolean — True if the directory exists after the call. A creation the running play session refuses raises with the whole reason.

vfs.mkdir("/zero/source/scenes/")

typed/builtin//modules/api/engine/vfs/vfs/move

vfs.move(src: string, dst: string, opts: { quiet: boolean? }?) -> (boolean, string?)

Move a file from src to dst. By default fires the destination's write side effects; pass opts.quiet = true to suppress them. While play is running a move takes the source away, so authored /source content that predates play is refused and the refusal RAISES with the reason; content this play session created moves and stays tracked on the play shadow.

Parameters

  • src string — Source absolute VFS path.
  • dst string — Destination absolute VFS path.
  • opts { quiet: boolean? } (optional) — Optional { quiet: boolean? }.

Returns (boolean, string?) — True on success; (false, errmsg) when the paths themselves refuse it. A move the running play session refuses raises with the whole reason instead.

vfs.move("/zero/source/a.luau", "/zero/source/b.luau")

typed/builtin//modules/api/engine/vfs/vfs/mutationSeq

vfs.mutationSeq() -> number

Lifetime count of VFS mutations the engine has APPLIED — the drain's clock. A write queues its side effects (an asset's content reload, the assetType's onChange, a component or scene registration) and a later frame runs them; this number advances as each one completes. Read it, write, then poll for a larger value to learn the queue has moved past the point you wrote at — instead of waiting a guessed number of frames. It counts every mutation kind, so it answers about the pipeline rather than about one file; asset.reloadSeq(ref) is the per-asset reading.

Returns number — Count of applied VFS mutations this session. Monotonic.

local at = vfs.mutationSeq()
vfs.write("/zero/source/tmp/note.txt", "hi")
repeat task.wait() until vfs.mutationSeq() > at

typed/builtin//modules/api/engine/vfs/vfs/pendingWrites

vfs.pendingWrites() -> { string }

List the /source paths with an in-flight local write the synced manifest has not reflected yet — the read-your-writes frontier. A just-written file appears here until its upload round-trips and the synced dirty state catches up; world.vcsStatus unions these so a fresh edit reads back as dirty immediately. Empty when fully synced.

Returns { string } — Array of VFS paths with pending (unconfirmed) local writes.

for _, p in ipairs(vfs.pendingWrites()) do print(p) end

typed/builtin//modules/api/engine/vfs/vfs/playShadowAuthors

vfs.playShadowAuthors() -> { [string]: { id: string, name: string? } }

The agent behind each currently-shadowed /source path: the ZeroMind user id the write was attributed to, and the username to show for it. Several agents drive one engine at once and every one of their in-play source edits sits in the same shadow set, so this is how a review, a refusal or a verdict tells one agent's pending work from another's. A path written with no actor identity carries no entry — it belongs to no agent in particular, and stays settleable by any of them.

Returns { [string]: { id: string, name: string? } } — Map of shadowed path to { id, name }.

local mine = vfs.currentAuthor()
for path, who in pairs(vfs.playShadowAuthors()) do
if mine == nil or who.id ~= mine.id then print(path, "belongs to", who.name) end
end

typed/builtin//modules/api/engine/vfs/vfs/playShadowPaths

vfs.playShadowPaths() -> { string }

List the /source paths edited during running play that are currently held as copy-on-write SHADOWS (MemFs-only, on-disk original untouched) — the universal play shadow-copy set. These are the in-play edits persist promotes over the originals on confirm, or drops on a guarded discard. Empty outside play or when nothing was edited.

Returns { string } — Array of normalized VFS paths currently shadowed.

for _, p in ipairs(vfs.playShadowPaths()) do print(p) end

typed/builtin//modules/api/engine/vfs/vfs/promotePlayShadow

vfs.promotePlayShadow(path: string) -> string

Promote a single play-shadow edit into a canonical write. Re-asserts the live overlay bytes through the full write pipeline with the play write lock released, then unmarks the path. The bytes stay in the engine end to end, so binary content promotes exactly. Takes ONE file path, and needs the write lock released, so run it inside a pause you take and hand back. A promotion that cannot happen raises with the reason: the path is not shadowed, the path is a folder covering shadowed edits, play is running, the workspace is read-only, or the write-through failed. A shadow entry whose bytes are gone is dropped as promoted, so the path comes back with nothing written for it.

Parameters

  • path string — Shadowed VFS path to promote (one of vfs.playShadowPaths()).

Returns string — The path the call settled — it is no longer shadowed.

engine.paused = true
local promoted = vfs.promotePlayShadow("/zero/source/cover.jpg")
engine.paused = false

typed/builtin//modules/api/engine/vfs/vfs/read

vfs.read(path: string, opts: VfsOpts?) -> string?

Read a file from the virtual filesystem. Binary-safe. Returns file contents as a string, or nil if the file is not known. Relative paths resolve under opts.root (default /source/). When called from a coroutine and the bytes aren't locally cached, transparently yields the coroutine while the lazy fetch runs.

typed/builtin//modules/api/engine/vfs/vfs/readAsync

vfs.readAsync(path: string, opts: VfsOpts?) -> string

Asynchronous binary-safe read. Returns a promise ID that resolves to the file contents. Useful for reading render textures from the main thread without blocking.

Parameters

  • path string — VFS path.
  • opts VfsOpts (optional){ root = "/source/" }.

Returns string — Promise ID — pass to task.await().

local data = task.await(vfs.readAsync("/zero/runtime/screenshots/last.png"))

typed/builtin//modules/api/engine/vfs/vfs/reload

vfs.reload(modulePath: string?) -> boolean

Clear entries from the require() cache so the next require(name) re-runs the module's source. Pass a single module identity to drop only that entry; call with no arguments to drop every cached module.

Parameters

  • modulePath string (optional) — Module identity to reload (omit to reload all).

Returns boolean — For a single identity, whether a module was cached under that name and has now been dropped — false says the name matched nothing. The no-arg form returns true.

vfs.reload("@mylib/utils.helpers")

typed/builtin//modules/api/engine/vfs/vfs/remove

vfs.remove(path: string, opts: VfsOpts?) -> (boolean, string?)

Remove a file. Refuses to remove directories unless opts.recursive = true. Refuses protected system roots. While play is running, authored /source content that predates play is refused and the refusal RAISES with the reason; content this play session created is removable, a folder included.

Parameters

  • path string — VFS path to remove.
  • opts VfsOpts (optional){ root = "/source/", recursive = false }.

Returns (boolean, string?) — True on success; (false, errmsg) when the path itself refuses the removal. A removal the running play session refuses raises with the whole reason instead, so a pcall around the call reads it — and the lock answers ahead of whether the path is there, so a locked /source path raises whether or not it holds anything.

vfs.remove("/zero/source/scratch.luau")

typed/builtin//modules/api/engine/vfs/vfs/revertPlayShadow

vfs.revertPlayShadow(path: string) -> string

Revert a single play-shadow edit: restore the pre-play copy captured at the first play-mode write (the last edit-mode state, unstaged edits included) into the live slot — or remove the file when it did not exist at that moment — then unmark the path. Hot-reload picks the original back up, so the running session actually reverts. Takes ONE file path, and needs the write lock released, so run it inside a pause you take and hand back. A revert that cannot happen raises with the reason, on the same terms as vfs.promotePlayShadow.

Parameters

  • path string — Shadowed VFS path to revert (one of vfs.playShadowPaths()).

Returns string — The path that is now reverted and no longer shadowed.

engine.paused = true
local reverted = vfs.revertPlayShadow("/zero/source/Foo.component/init.luau")
engine.paused = false

typed/builtin//modules/api/engine/vfs/vfs/unmarkPlayShadow

vfs.unmarkPlayShadow(path: string) -> boolean

Forget a single play-shadow path after it has been promoted (saved over source) or discarded. Tracking only — never touches the bytes.

Parameters

  • path string — VFS path to unmark.

Returns boolean — Always true.

vfs.unmarkPlayShadow("/zero/source/Foo.component/init.luau")

typed/builtin//modules/api/engine/vfs/vfs/unwatch

vfs.unwatch(watcherId: number) -> boolean

Remove a previously registered VFS watcher.

Parameters

  • watcherId number — Watcher id returned by vfs.watch.

Returns boolean — True if the watcher was found and removed.

vfs.unwatch(id)

typed/builtin//modules/api/engine/vfs/vfs/watch

vfs.watch(path: string, callback: (string, string) -> ()) -> number

Register a callback that fires when a VFS path is written or removed. Two match modes: exact, or folder/prefix (key ends with /, and fires for any descendant). The callback runs in the VM that registered it. Returns a watcher id for vfs.unwatch.

Parameters

  • path string — Exact path, or folder path ending in /.
  • callback (string, string) -> ()(mutated_path, kind) -> (), kind "write" or "remove".

Returns number — Watcher id.

local id = vfs.watch("/zero/source/", function(path, kind) print(kind, path) end)

typed/builtin//modules/api/engine/vfs/vfs/write

vfs.write(path: string, content: string, opts: VfsOpts?) -> (boolean, string?)

Write content to a file. Binary-safe. Overwrites existing files by default — pass opts.overwrite = false to refuse to clobber. While play is running a /source write lands on the play shadow: it succeeds and reads back, live in the session with disk source untouched, and is discarded on a guarded play-exit unless accepted. Scratch under /source/tmp/ writes through untouched. Pass opts.durable = true to say these bytes ARE the source: the write reaches canonical /source with play still running and the session still in play, hot-reloading the modules and components that read it, so the edit is observed running in the same play session with nothing left to promote. A durable write RAISES with the reason when the bytes cannot become canonical source.

Parameters

  • path string — VFS path to write to.
  • content string — File content (binary-safe).
  • opts VfsOpts (optional){ root = "/source/", overwrite = true, quiet = false, durable = false }.

Returns (boolean, string?) — True on success; on failure returns false + error message. A durable write raises instead of returning false.

vfs.write("/zero/source/notes.md", body)
vfs.write("/zero/source/game/Vent.component/init.luau", src, { durable = true })
  • api
  • reference