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.
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.
readfile(path: string): string
writefile(path: string, contents: string): ()stringwritefile("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.
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))
endappendfile
Append bytes to an existing file without rewriting it. Cheaper than read-modify-write for log files or growing buffers.
appendfile(path: string, contents: string): ()voidappendfile("log.txt", os.date() .. " - launched\n")isfile and isfolder
Existence checks. Both return a boolean and never throw.
isfile(path: string): boolean
isfolder(path: string): booleanbooleanif isfile("config.json") then ... end
if not isfolder("cache") then makefolder("cache") endmakefolder, delfolder, delfile
Creation and deletion.
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.
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.
listfiles(folder: string): { string }{ string }for _, entry in listfiles("cache") do
print(entry, isfile(entry) and "file" or "folder")
endTo recurse, you call listfiles on each subfolder you see. Most scripts wrap that into a helper at the top:
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
endloadfile and dofile
Load Luau code from disk. loadfile returns the chunk as a function (you call it to execute); dofile loads and immediately runs.
loadfile(path: string): (function?, string?)
dofile(path: string): ()function | nil, string | nillocal fn, err = loadfile("plugins/extra.luau")
if fn then
local ok = pcall(fn)
else
warn("failed to load:", err)
endThe 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.
getcustomasset(path: string): stringstring-- 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 = playerGuiThe 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
isfilefirst. - Plugin system. Auto-load every
.luauin 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.
getcustomassetURLs 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 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.