atherhubather.hub
Back to Guides
SUNC
9 min read
May 11, 2026

The SUNC Filesystem API

The SUNC filesystem library is a thin, scriptable interface to a sandboxed folder on the user's machine. Scripts can read, write, append, enumerate, and execute files inside that sandbox — everything you need to cache settings, persist key state, or load extra code that's too big to keep inline.

Ather
Ather
Lead developer at Atherhub. Writes about Roblox internals, Luau, script engineering, and platform security.Last updated May 11, 2026

The sandbox

Every executor that implements the SUNC filesystem keeps a dedicated workspace folder on disk, usually called workspace from inside scripts. All filesystem calls are relative to that folder. You can't escape it with ../; you can't reach the user's home directory; you can't enumerate paths outside the sandbox. Most executors implement this as a literal directory on the user's machine, but the script-visible behaviour is the same regardless: a flat-rooted virtual filesystem you can do everything in.

readfile and writefile

The two most-used primitives. readfile returns a string with the file's contents; writefile replaces the file with the bytes you pass.

luau
readfile(path: string): string
writefile(path: string, contents: string): ()
Returnsstring
The raw bytes of the file. Binary-safe — embedded NUL bytes survive.
writefile("config.json", '{ "theme": "dark", "fps": 144 }')
print(readfile("config.json"))
--> { "theme": "dark", "fps": 144 }

A common idiom: persist user settings as JSON and rehydrate at script load. The combination is small enough to fit on a single screen.

luau
local HttpService = game:GetService("HttpService")

local function loadConfig(path: string, defaults: { [string]: any })
    if not isfile(path) then return defaults end
    local ok, data = pcall(HttpService.JSONDecode, HttpService, readfile(path))
    if not ok or typeof(data) ~= "table" then return defaults end
    return data
end

local function saveConfig(path: string, data: { [string]: any })
    writefile(path, HttpService:JSONEncode(data))
end

appendfile

Append bytes to an existing file without rewriting it. Cheaper than read-modify-write for log files or growing buffers.

luau
appendfile(path: string, contents: string): ()
Returnsvoid
No return value. If the file doesn't exist, it's created.
appendfile("log.txt", os.date() .. " - launched\n")

isfile and isfolder

Existence checks. Both return a boolean and never throw.

luau
isfile(path: string): boolean
isfolder(path: string): boolean
Returnsboolean
True if the path exists and is the right kind. False for nonexistent paths or wrong kind.
if isfile("config.json") then ... end
if not isfolder("cache") then makefolder("cache") end

makefolder, delfolder, delfile

Creation and deletion.

luau
makefolder(path: string): ()
delfolder(path: string): ()
delfile(path: string): ()

makefolder creates intermediate folders if needed (so makefolder("cache/icons") works even when cache doesn't exist yet). delfolder removes the folder and everything in it — there's no "safe" variant that errors on non-empty folders, so be deliberate. delfile is straightforward.

Idempotency
All three are idempotent in the sense that "make a folder that exists" and "delete a file that doesn't exist" both silently succeed. You can call them without guarding with isfolder / isfile first.

listfiles

Enumerate the immediate contents of a folder. Returns the full paths (relative to the sandbox root) of every file and folder directly under the given path — not recursive.

luau
listfiles(folder: string): { string }
Returns{ string }
Array of paths for every entry. Order is not guaranteed; sort if it matters.
for _, entry in listfiles("cache") do
    print(entry, isfile(entry) and "file" or "folder")
end

To recurse, you call listfiles on each subfolder you see. Most scripts wrap that into a helper at the top:

luau
local function walk(root: string, out: { string }?): { string }
    out = out or {}
    for _, entry in listfiles(root) do
        table.insert(out, entry)
        if isfolder(entry) then walk(entry, out) end
    end
    return out
end

loadfile and dofile

Load Luau code from disk. loadfile returns the chunk as a function (you call it to execute); dofile loads and immediately runs.

luau
loadfile(path: string): (function?, string?)
dofile(path: string): ()
Returnsfunction | nil, string | nil
loadfile returns the loaded chunk plus nil on success, or nil plus an error string on failure. dofile has no return value but throws on error.
local fn, err = loadfile("plugins/extra.luau")
if fn then
    local ok = pcall(fn)
else
    warn("failed to load:", err)
end

The classic use is a plugin folder: drop Lua files into aplugins/ subfolder, enumerate them on startup, load each one. That gives you script-extensibility without recompiling or redistributing your main script.

getcustomasset and getfileurl

Bridge between the filesystem and Roblox content URIs. Some Roblox APIs (Image, Sound, MeshPart) need a URL or a content ID — they don't accept raw bytes. These functions return a URL pointing at a file in your sandbox.

luau
getcustomasset(path: string): string
Returnsstring
A rbxasset://... or similar URL that Roblox can use as a content reference for the file at path.
-- Cache a PNG to disk, then expose it to a Roblox ImageLabel
writefile("logo.png", game:HttpGet("https://example.com/logo.png"))

local label = Instance.new("ImageLabel")
label.Image = getcustomasset("logo.png")
label.Size = UDim2.fromOffset(64, 64)
label.Parent = playerGui

The flow shape — fetch over HTTP once, persist to disk, then serve to Roblox UI without re-fetching — is the most common reason to ever use the filesystem API in practice.

Common patterns

  • Settings persistence. JSON-encode a table at the end of a session, decode it at the start.
  • Asset cache. Skip a slow HTTP fetch on subsequent loads by checking isfile first.
  • Plugin system. Auto-load every .luau in a folder so users can drop their own additions.
  • Logging. Append a line on every interesting event; rotate the file when it gets too large.

Gotchas

  • Paths are relative to the sandbox root. Absolute paths or../ traversal don't work and produce errors.
  • File contents are bytes, not strings. Don't assume the file you read is valid UTF-8 unless you wrote it that way.
  • getcustomasset URLs are valid only while the file exists. Deleting the file invalidates any UI that references it.
  • Roblox API rate limits still apply when you fetch content for cache. Don't spin in a tight loop assuming HTTP is free.

Wrap-up

The filesystem library is one of the simpler SUNC libraries — a dozen functions, each doing the obvious thing. Most scripts use three of them (readfile, writefile, isfile) and never need the rest. But knowing the full surface (getcustomasset in particular) is the difference between "has to re-fetch every load" and "runs offline after first load".

Ather
Written by Ather

Ather is the lead developer behind Atherhub. He's been writing Luau and Roblox tooling for the better part of a decade, with a focus on the messy interface between game-script internals and the platforms that host them. Have feedback on this article? Drop it in the Discord.