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

SUNC Reflection Deep Dive

The reflection primitives — getgenv, getrenv, getsenv, getreg, getloadedmodules, getrunningscripts — let scripts inspect and modify Lua environments that aren't their own. They sit at the heart of cross-script communication, lurking-script discovery, and module re-use without recompilation.

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

Three environments worth distinguishing

When you run a script in an executor, three Lua tables conceptually hold globals:

  • Executor environment (genv) — the executor's shared globals. Survives across executions. Where SUNC functions live.
  • Roblox environment (renv) — Roblox's own global table. Containsgame, workspace, the standard library, all built-in services.
  • Script environment (senv) — the environment of a specific script (yours or someone else's). Contains that script's locals after it ran, plus a chain back to the renv.

The four functions below give you each table.

getgenv

luau
getgenv(): { [string]: any }
Returnstable
The executor's shared global table. Mutations persist across script executions until the executor process restarts.
getgenv().myCache = { items = {} }
-- Visible to any subsequent script run inside the executor.

-- Later, from a different script:
print(getgenv().myCache.items)

The main use case: persisting state across script reloads. When the user re-pastes your loader, your new script starts with a fresh local environment — but getgenv() still has whatever you stashed there last time. That's how the "tear down previous instance" pattern works in the external script tutorial.

getrenv

luau
getrenv(): { [string]: any }
Returnstable
Roblox's global environment. You can read it, but writes go nowhere visible because Roblox-side scripts don't share this table directly.
local renv = getrenv()
print(renv.game == game)  --> true
print(renv.print == print)  --> false  (your script's print may be sandboxed differently)

The reason these can differ: executors sometimes hand scripts a wrapper around print that routes to their own console, while Roblox's renv still has the original built-in. getrenv is how you reach the real one.

getsenv

luau
getsenv(script: LuaSourceContainer): { [string]: any }
Returnstable
The environment of the given script. After the script has run, locals it declared with assignment to globals are visible here.
local serverScript = game.ServerScriptService.Combat
local env = getsenv(serverScript)

print(env.MAX_HEALTH)
print(env.damagePlayer)
-- Whatever Combat declared as globals (not local), you can read here.

The most common use: reading constants or helper functions out of a server-side script you didn't write, so your script can match their conventions. If a game script declares damagePlayer as a global function (not local), getsenv exposes it to your script like any other table entry.

Locals are not visible
Strictly-local variables in someone else's script aren't available through senv — only true globals are. For locals, you'd need debug.getupvalues on a function from that script.

getreg

luau
getreg(): { [any]: any }
Returnstable
The Lua registry — a table the C side of Roblox uses to hold references to user values. Contains coroutines, callback registrations, and other internal state.

The registry is mostly an internal data structure, but some things you might enumerate here are useful: coroutine references for every active thread, weakrefs to loaded modules, callback handles. The registry is one of the lowest-level entry points the Luau VM exposes.

Practical example: enumerate every active coroutine in the VM to find a hung one.

luau
for k, v in getreg() do
    if type(v) == "thread" then
        print("thread", coroutine.status(v))
    end
end

getloadedmodules

luau
getloadedmodules(): { ModuleScript }
Returns{ ModuleScript }
Every ModuleScript that has been required at least once in the current Roblox session.
for _, module in getloadedmodules() do
    print(module:GetFullName())
end
-- Prints every module the game has loaded, regardless of where it lives.

Because ModuleScripts cache their return value after firstrequire, this list tells you exactly which modules are "live" in the VM. Often you can re-require one of them to get the same table the game itself is using — a powerful primitive for modifying game internals without hooking.

luau
local Balance
for _, m in getloadedmodules() do
    if m.Name == "Balance" then
        Balance = require(m)
        break
    end
end

if Balance and Balance.PlayerWalkSpeed then
    Balance.PlayerWalkSpeed = 32  -- mutate the live config
end

getrunningscripts

luau
getrunningscripts(): { LuaSourceContainer }
Returns{ LuaSourceContainer }
Every script that has run in the current session — server, local, and module containers alike.
for _, s in getrunningscripts() do
    print(s.ClassName, s:GetFullName())
end

The catch-all enumeration. Useful when you want to scan a game's scripts to find ones matching a pattern (by name, by source, by location). Often paired with getscriptbytecode or getscriptclosure to actually inspect or interact with a specific script.

A worked example: live tweaking a game

Putting the reflection primitives together: find a game's balance module, mutate a value, and have it take effect immediately because the game holds a reference to the same table.

luau
local function findModule(name: string)
    for _, m in getloadedmodules() do
        if m.Name == name then return require(m) end
    end
end

local Constants = findModule("GameConstants")
if Constants then
    Constants.JumpPower = 100
    -- Anywhere in the game that reads Constants.JumpPower now sees 100,
    -- because we're mutating the same table the game uses.
end

The reason this works is identity. ModuleScripts cache their return value, so every require of the same module gets the same table reference. Mutate the table, and every cached reference sees the change.

Caveats

  • Mutations to getrenv() often don't propagate to Roblox-side scripts because they hold their own per-script environments chained off it. Useful for reading; less useful for writing.
  • getsenv on a script that errored out returns the partial environment as of the failure point. That can be confusing if you expect a fully-populated table.
  • Some executors gate getreg behind a flag because it can be slow on large heaps. Don't call it on a hot path.

Wrap-up

Reflection in SUNC is small but powerful. The whole surface is six functions; the patterns built on top (cross-execution state, module-table mutation, script discovery) cover most of what advanced scripts do. If you've never reached past getgenv before, spending fifteen minutes with the others will permanently expand the set of things you can build.

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.