Sandbox

What a script cannot touch, what replaces it, and how files and modules work inside C:\xiaohook.

Scripts cannot reach the disk or the system outside C:\xiaohook: this page lists what was removed or changed in the standard Lua libraries, what to use instead, and what each execution context allows.

What was removed

Removed Replacement
the whole io library xiao.fs
ffi (not loaded, not in package.preload) none
os.execute, os.exit, os.getenv, os.remove, os.rename, os.tmpname, os.setlocale none, os keeps clock, date, difftime, time
dofile, loadfile load(xiao.fs.read(path))
package.loadlib, package.seeall, native modules, package.path and package.cpath lookups require restricted to C:\xiaohook\lua
binary chunks (bytecode) in load, loadstring, require and executed scripts text source only
debug.debug none, the rest of debug is there
jit.attach, the jit.profile module none: their callbacks would run outside the time limits

What was changed

Function In this client
print writes one line to the console (4096 bytes at most, 512 lines kept), through the global tostring
load text only; default chunk name =(load); a mode or env argument is ignored, use setfenv
loadstring text only; default chunk name is the text
require package.preload, then C:\xiaohook\lua\<name>.lua and <name>/init.lua
package.path, package.cpath empty strings, never read
package.loaders two searchers: package.preload and the C:\xiaohook\lua searcher
os.date checks the format first: an unknown directive is the error os.date: invalid conversion specifier '%Q' instead of a crash of the game
debug.sethook cannot install a hook; an error inside a packet or cmd hook

Everything else of base, string, table, math, bit, coroutine and the LuaJIT jit library is the stock LuaJIT 2.1 version, built without the Lua 5.2 extensions. require also finds the modules LuaJIT preloads: table.new, table.clear, string.buffer and jit.util.

Loading bytecode, from string.dump or from a file, fails with attempt to load chunk with wrong mode.

Files

xiao.fs works inside C:\xiaohook. Paths are relative to that folder, / and \ both work, a leading / is ignored, and .. is allowed as long as the resolved path stays inside.

New xiao.workspace writes only C:\xiaohook\lua\workspace\<name>.json, with names limited to letters, digits, _ and - (plus the one time move of older files).

Every xiao.fs failure raises an error, so wrap risky calls in pcall:

local ok, content = pcall(xiao.fs.read, "config/settings.txt")
if not ok then
    xiao.fs.makefolder("config")
    xiao.fs.write("config/settings.txt", "enabled=1\n")
    content = "enabled=1\n"
end
print(content)

Running another file

dofile and loadfile do not exist. Read the file and compile it, or use require for modules:

local chunk, err = load(xiao.fs.read("lua/helpers.lua"), "=helpers")
if chunk == nil then error(err) end
chunk()

xiao.fs paths start at C:\xiaohook, so a script in the scripts folder is lua/<name>.lua.

Modules

require("lib.util") loads C:\xiaohook\lua\lib\util.lua (or lib/util/init.lua) once. The same path rules apply, with C:\xiaohook\lua as the root: a module name cannot leave that folder. Error messages and details are in require and Modules with require.

Execution contexts

New Code does not run with the same rights everywhere. The main chunk, timers, signals, on_frame and paint listeners run on the render thread with every function available. Packet hooks and cmd hooks run on game threads, inside a 2000 us budget, and refuse what could block or touch the render thread:

Call inside a hook Result
xiao.timer.spawn xiao.timer.spawn is not available inside a packet hook or a cmd hook
xiao.config functions xiao.config is not available inside a packet hook or a cmd hook
xiao.workspace functions xiao.workspace is not available inside a packet hook or a cmd hook
xiao.notify xiao.notify is not available inside a packet hook or a cmd hook
debug.sethook debug.sethook is not available inside a packet hook or a cmd hook
xiao.paint inert: nothing is drawn, fonts and images are not baked or measured
xiao.game reads no frame: lists are empty, single reads return nil

xiao.timer.after and xiao.timer.every work in a hook: their callback runs later on the render thread, where everything is allowed again. Plain coroutines work too, inside the hook budget. The full list is in Execution contexts.

Time limits

The client stops code that runs too long: 2000 ms for the main chunk, 100 ms for the packet receivers and for each frame, 2000 us for each packet or cmd hook. A timeout cannot be caught with pcall, and a script cannot install a debug hook that would get around it, see debug and Time limits.