Guide: Hooking
Replacing a function with your own, keeping the original callable, and doing it without leaving obvious traces.
Hooking swaps the implementation of a function for one of yours, handing you back the original so you can still call it. It is the foundation of almost everything interesting: watching remote calls, blocking an anti-cheat check, changing what a game function returns.
Three tools cover almost every case. hookfunction replaces a plain function. hookmetamethod replaces an entry in an object's metatable — which is how you intercept __namecall, the single most useful hook in Roblox. oth.hook is the low-level path for C closures when you need the original beneath multiple layers.
hookfunction takes the target and your replacement, and returns the original. Keep that return value — without it you have no way to call through.
local original
original = hookfunction(workspace.FindPartOnRay, function(self, ray, ignore, terrain, water)
-- Do your own thing, then fall through to the real one.
print("FindPartOnRay from", getcallingscript())
return original(self, ray, ignore, terrain, water)
end)Always call through unless you mean not to
A hook that silently returns nil instead of calling the original will break the game in ways that are hard to trace back to you. Return original(...) unless blocking the call is the whole point.
Roblox compiles object:Method(args) into a single __namecall invocation rather than an index followed by a call. That means one hook on __namecall sees *every* method call on every instance — including FireServer and InvokeServer.
local Players = game:GetService("Players")
local old
old = hookmetamethod(game, "__namecall", newcclosure(function(self, ...)
local method = getnamecallmethod()
if not checkcaller() and (method == "FireServer" or method == "InvokeServer") then
print(("%s:%s(%s)"):format(self:GetFullName(), method, table.concat({...}, ", ")))
end
return old(self, ...)
end))- getnamecallmethod() tells you which method was called — it is only meaningful inside the hook.
- checkcaller() is false when the call came from the game rather than from you, which is how you avoid logging your own traffic.
- newcclosure wraps your Luau function so it looks like a C closure to anything inspecting it. Without it, a game that checks iscclosure(getrawmetatable(game).__namecall) sees the hook.
restorefunction puts a hooked function back the way it was. Do this when your script unloads — a hook that outlives the script that installed it is a good way to break the next one.
local original = hookfunction(target, replacement)
-- ... later, when tearing down
restorefunction(target)isfunctionhooked tells you whether a function currently carries a hook, which is worth checking before installing a second one on top.
The full entry list lives in Closures (hookfunction, newcclosure, restorefunction, setstackhidden), Metatable (hookmetamethod, getnamecallmethod, getrawmetatable) and OTH (oth.hook, oth.get_root_callback).