SUNC hookfunction Deep Dive
hookfunction is the single most-used SUNC primitive after checkcaller. It looks simple enough — replace one function with another, get the original back — but the details around closure types, upvalue handling, and the relationship with hookmetamethod are where most first-time users get tripped up.
The signature
function hookfunction(target: (...any) -> ...any, hook: (...any) -> ...any): (...any) -> ...any(...any) -> ...anytarget. Call it from inside your hook to delegate. The returned function is the same closure type as the original was — that's a detail that matters for detection.local original
original = hookfunction(workspace.FindFirstChild, function(self, name, recursive)
print("FindFirstChild on", self, "for", name)
return original(self, name, recursive)
end)Two functions go in. The target is what callers will execute from now on — it's the one whose memory the hook mutates. The hook is what they actually run when they call it. The returned value is the original target's implementation, decoupled from the hook so you can invoke it freely.
Closure-type rules
Roblox closures come in two flavours: C closures (functions written in C, like print or methods on Roblox objects) and Lua closures (functions you define in script). The two flavours have different memory layouts, and you can't generally hook a C target with a Lua hook directly — the call ABI doesn't line up. SUNC'shookfunction handles this by accepting a Lua hook regardless, but wrapping it transparently:
- C target + Lua hook → the engine wraps your hook in a C closure internally so the call frames match. The returned original is still callable as a C function.
- Lua target + Lua hook → straightforward pointer swap, both closures stay Lua.
- Lua target + C hook → only works if the executor explicitly supports it (some do, some don't).
newcclosure. This is so common that you'll see it in almost every published example.local original
original = hookfunction(print, newcclosure(function(...)
return original("[hooked]", ...)
end))
-- iscclosure(print) is now true, and detection that requires C closure for
-- print() will pass. Without newcclosure it would be a Lua closure and trip
-- closure-type fingerprinting.Common patterns
Three patterns cover the vast majority of real hook code:
1. Logger. Observe everything, change nothing. Useful for understanding a game's internal API.
local original
original = hookfunction(workspace.Raycast, function(self, ...)
local result = original(self, ...)
print("Raycast →", result)
return result
end)2. Conditional filter. Most calls pass through unchanged, a tiny minority get rewritten. This is the highest-quality shape for an anti-detection hook — probes still see real behaviour.
local original
original = hookfunction(localPlayer.Kick, function(self, reason)
if checkcaller() then
return original(self, reason)
end
-- Game called Kick on the local player; drop it
return nil
end)3. Full replacement. Never delegate. Risky — anything the game expects from the original behaviour is gone. Use sparingly.
hookfunction(workspace.Destroy, function() end)
-- workspace:Destroy() is now a no-op. Useful for crashing-game scripts;
-- otherwise this nukes the entire workspace if anything calls it.hookmetamethod vs hookfunction
A common point of confusion: when do you use hookmetamethod and when do you use hookfunction? They're related but not interchangeable.
hookfunction(f, hook)replaces the function valuefin place. Anything that calledfby reference is now callinghook.hookmetamethod(obj, name, hook)replaces a metatable entry onobj's metatable without mutating the function value itself. The original function still exists; what changed is which function the metatable points at for that method name.
Why this distinction matters: if a game stored a reference to obj.__namecall at startup and compares against it later, hookmetamethod can pass that check (the stored value still points at a valid function), while hookfunction on the same metamethod would not.
-- Equivalent in effect but very different in inspection:
hookfunction(getrawmetatable(game).__namecall, myHook)
-- vs.
hookmetamethod(game, "__namecall", myHook)The rule of thumb: hook metamethods through hookmetamethod exclusively. Use hookfunction for standalone functions and methods (which aren't accessed via metamethods anyway).
Edge cases that bite
The errors you'll meet most often:
- Stack overflow from self-call. Forgetting to call the saved original and instead calling the target name (which is now your hook) creates an infinite recursion. Always capture the return value and call that, not the original symbol.
- Argument count mismatch. C functions are often strict about argument count. If your hook drops or adds an argument before delegating, the original may throw a less-helpful error than you expected.
- Hooking inside a method. If you hook
obj.Method, callers usingobj:Method()still passobjas the first argument. Your hook signature has to be(self, ...), not(...), or your arguments will be off by one. - Upvalues retained by the original. If the original function captured upvalues, your saved reference keeps those captures alive. Most of the time this is invisible, but in long-lived hooks where the upvalue holds a large table, this is how you accidentally leak memory.
Restoring an original
Unhooking is just hooking back to the saved original. SUNC doesn't define an explicit unhook — the idiom is structural:
local original
original = hookfunction(target, hook)
-- ...later, when you want to undo:
hookfunction(target, original)The trick: you're hooking the same target a second time, but with a function that delegates to the (already-restored) original. Effectively a swap-back.
Performance considerations
A bare Lua-to-Lua hook adds the cost of one function call to every invocation. For most cases that's in the tens of nanoseconds and you won't notice. Two things to watch:
- Hooking very hot paths (per-frame, per-namecall) with logic heavy enough to allocate adds up fast. Profile.
newcclosurewrapping adds another small indirection. Worth the cost for detection-sensitive hooks, unnecessary for purely internal hooks that game code never inspects.
Wrap-up
The mental model: hookfunction is a primitive that mutates one function value into delegating to another. Almost everything else — closure types, namecall hooks, defensive delegation patterns — is built on top. Get those four idioms right (capture the original, wrap with newcclosure when crossing closure types, delegate by default, early-exit on checkcaller) and 95% of real hook code will work and survive inspection.
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.