geode.fs reads and writes files inside the running mod's own directories.
Every call takes a root as its first argument.
The root selects one of the mod's directories, and access is sandboxed to it.
A path that escapes the root (for example with ..) or an absolute path is rejected.
| Root | Directory | Access |
|---|---|---|
"save" |
getSaveDir() |
read + write |
"config" |
getConfigDir() |
read + write |
"persistent" |
getPersistentDir() |
read + write |
"resources" |
getResourcesDir() |
read only |
An unknown root raises a Lua error.
Recoverable failures return nil and an error string. See globals Error shapes.
Paths are canonicalized before the sandbox check. A symlink inside a root that resolves outside the root is rejected. LuauAPI never creates symlinks.
geode.fs.read(root: FsRoot, path: string) -> (string?, string?)Reads a file's contents. Returns the contents, or nil and an error message.
geode.fs.write(root: FsRoot, path: string, data: string) -> (boolean?, string?)Writes data to a file, creating parent directories as needed. Returns true, or nil and an error message.
Writing to the read-only resources root fails.
geode.fs.exists(root: FsRoot, path: string) -> (boolean?, string?)Returns true when a file or directory exists, false when missing.
Returns nil and an error message when the path escapes the root or the filesystem fails.
geode.fs.list(root: FsRoot, path: string) -> ({ string }?, string?)Lists the immediate entries of a directory (names, not full paths, not recursive).
Returns an array table, or nil and an error message.
Listings are capped.
geode.fs.mkdir(root: FsRoot, path: string) -> (boolean?, string?)Creates a directory and any missing parents. Returns true, or nil and an error message.
Fails on the read-only resources root.
geode.fs.remove(root: FsRoot, path: string) -> (boolean?, string?)Removes a single file or empty directory (never recursive).
Returns true, or nil and an error message. A missing path still returns true.
Fails on the read-only resources root.
Reads, writes, and directory listings are capped.
See Limits and errors.
local data = geode.json.dump({ count = 3 })
assert(geode.fs.write("save", "state.json", data))
if geode.fs.exists("save", "state.json") then
local text = geode.fs.read("save", "state.json")
print(geode.json.parse(text).count) -- 3
end
local entries, err = geode.fs.list("save")
if not entries then return print(err) end
for _, name in ipairs(entries) do
print(name)
endsrc/bindings/geode/GeodeFsBinding.cppsrc/bindings/geode/ModSandbox.cppsrc/require/PathSandbox.hpptools/luau_codegen/extra_bindings/fs.dluau