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

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.

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

The signature

luau
function hookfunction(target: (...any) -> ...any, hook: (...any) -> ...any): (...any) -> ...any
Returns(...any) -> ...any
The original implementation of target. 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).
The newcclosure idiom
When you want your hook to look like a C closure to detection (because it's replacing a C target), wrap the hook in newcclosure. This is so common that you'll see it in almost every published example.
luau
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.

luau
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.

luau
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.

luau
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 value f in place. Anything that calledf by reference is now calling hook.
  • hookmetamethod(obj, name, hook) replaces a metatable entry on obj'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.

luau
-- 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 using obj:Method() still pass obj as 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:

luau
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.
  • newcclosure wrapping 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
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.