SUNC Script Inspection
Every running Roblox script has bytecode in memory, a compiled closure, and (often) a known content hash. SUNC exposes all three through a small family of inspection functions that let you discover and interact with code you didn't write — from server scripts to local handlers in the player's GUI.
getscriptbytecode
Returns the raw Luau bytecode for a given script.
getscriptbytecode(script: LuaSourceContainer): stringstringlocal local_script = game.StarterPlayer.StarterPlayerScripts.Combat
local bytecode = getscriptbytecode(local_script)
print(#bytecode, "bytes")
-- Typically a few KB for a small script.The most common use isn't to read the bytecode directly (it's opaque without a disassembler), but to hash it for identity checks or to spot when a script has changed since you last looked. crypt.hash(bytecode, "sha256") gives you a stable identifier that ignores whitespace, comments, and other source-level noise.
getscriptclosure
Returns the compiled function value of a script — the actual callable closure the engine produced from the script's source.
getscriptclosure(script: LuaSourceContainer): functionfunctionlocal script = game.ReplicatedStorage.MyModule
local closure = getscriptclosure(script)
local fresh = closure()
-- 'fresh' is a new instance of whatever MyModule returns,
-- separate from any cached require() result.The main use case is re-instantiating ModuleScripts. Roblox caches the return value of a module's firstrequire forever; getscriptclosure lets you bypass that cache to get a fresh copy when you need one. This is invaluable for live-debugging — change the module's source, re-fetch the closure, run it, and see new behaviour without restarting the place.
getscripthash
Returns a content hash of a script. Useful as a stable identifier across sessions.
getscripthash(script: LuaSourceContainer): stringstringlocal hash = getscripthash(game.ServerScriptService.Combat)
print(hash) --> "3a8f9c..."Most executors implement this as SHA-256 over the bytecode, which gives stable cross-session identity even if the script's in-memory pointer address changes. Practical use: caching results of expensive script analysis, keyed by hash.
getcallingscript
Returns the script that called the current function. The rare reflection primitive that introspects the call stack rather than a passed argument.
getcallingscript(): LuaSourceContainer?LuaSourceContainer | nillocal original
original = hookfunction(workspace.FindFirstChild, function(self, ...)
local caller = getcallingscript()
if caller then
print("FindFirstChild called from", caller:GetFullName())
end
return original(self, ...)
end)Combined with a hook, getcallingscript tells you which game-side script is doing what. That's the quickest way to figure out who's firing a particular RemoteEvent, or which script is constantly calling a property setter.
A worked example: dump every server script
Putting the inspection primitives together: a small script that enumerates every currently-running script alongside its hash and size. Useful for getting a feel for what code is actually live in a game.
local function dumpScripts()
for _, s in getrunningscripts() do
local ok, bytecode = pcall(getscriptbytecode, s)
if ok then
local hash = getscripthash and getscripthash(s) or "?"
print(("%-30s %5d bytes %s"):format(
s:GetFullName():sub(1, 30),
#bytecode,
hash:sub(1, 12)
))
end
end
end
dumpScripts()On a typical game this prints dozens of lines — server scripts in ServerScriptService, LocalScripts in StarterPlayerScripts, ModuleScripts throughout. Each row tells you exactly where the script is and how big it is.
A worked example: re-run a module without the cache
More advanced: take a ModuleScript that's been required, get a fresh copy by going through the closure rather than require, and mutate the fresh copy independently.
local moduleScript
for _, m in getloadedmodules() do
if m.Name == "Config" then moduleScript = m end
end
if moduleScript then
-- Standard cached require:
local cached = require(moduleScript)
-- Re-run the chunk to get a brand new return value:
local fresh = getscriptclosure(moduleScript)()
print(cached == fresh) --> false (different tables)
fresh.tweakedValue = "hello"
-- cached.tweakedValue is still nil; we have a private copy.
endThe two values share nothing — they were produced by two separate executions of the module chunk. This is the mechanism behind "mod a game without changing what other scripts see."
Caveats
getscriptbytecodeon a script you don't have permission to inspect (e.g., a server script you're looking at from a client) may return an empty string or error, depending on the executor.getscriptclosurereturns the closure of thechunk, not the script's top-level scope. Calling it twice produces two independent runs; values assigned to upvalues in the first don't exist in the second.getcallingscriptonly works from inside a function called from another script. Calling it at the top level of your own chunk just returns your own script.- Bytecode is not stable across Roblox versions. A hash captured today may not match a hash captured next week even if the source is unchanged, because the compiler might have changed.
Wrap-up
Script inspection is the SUNC corner that turns a Roblox game from a black box into a partially-readable system. You can find every script, hash it for identity, peek at its bytecode, re-run it from scratch, and trace which script is calling which function at runtime. None of these are flashy on their own, but together they're the difference between "guessing what a game does" and "knowing."
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.