Skip to main content

Hooking & Intercepting

Hooking lets you intercept function calls — including remote fires — without modifying the game's code. You can spy on all traffic, modify arguments before they're sent, block specific calls, or react to server events before the game's own handlers run.

What is hookfunction?

hookfunction replaces a function with your version while giving you access to the original:

local oldPrint = hookfunction(print, function(...)
-- your code runs before the real print
warn("intercepted:", ...) -- log to warn instead
return oldPrint(...) -- still call the original
end)

After this, every print() call goes through your hook first.

The __namecall Hook (Remote Interception)

The most powerful hook for remotes. When Lua calls remote:FireServer(args), it goes through the __namecall metamethod of the remote's metatable. Hooking this intercepts every remote fire from every script:

local oldNamecall
oldNamecall = hookmetamethod(game, "__namecall", function(self, ...)
local method = getnamecallmethod() -- "FireServer", "InvokeServer", etc.
local args = {...}

-- self is the object being called on (the RemoteEvent/Function)
-- method is the method name
-- args are the arguments

-- Let everything through unchanged
return oldNamecall(self, ...)
end)

This hook fires for every method call on any game object — not just remotes. Filter by method name:

oldNamecall = hookmetamethod(game, "__namecall", function(self, ...)
local method = getnamecallmethod()

if method == "FireServer" then
-- Intercepted a FireServer call
-- self = the RemoteEvent
-- ... = the arguments
end

if method == "InvokeServer" then
-- Intercepted an InvokeServer call
end

return oldNamecall(self, ...)
end)

Logging All Remote Traffic

This is the foundation — log everything so you can find what you need:

local oldNamecall
oldNamecall = hookmetamethod(game, "__namecall", function(self, ...)
local method = getnamecallmethod()
local args = {...}

if method == "FireServer" or method == "InvokeServer" then
local argStrs = {}
for _, v in args do
table.insert(argStrs, tostring(v))
end
print(string.format("[%s] %s %s",
method, self:GetFullName(), table.concat(argStrs, ", ")))
end

return oldNamecall(self, ...)
end)

This is essentially rSpy — you're building the same thing from scratch.

Filtering by Remote Name

Once you know which remote you care about:

oldNamecall = hookmetamethod(game, "__namecall", function(self, ...)
local method = getnamecallmethod()

if method == "FireServer" and self.Name == "AttackRemote" then
-- Only AttackRemote fires
local target = select(1, ...)
print("Attack fired at:", target and target:GetFullName() or "nil")
end

return oldNamecall(self, ...)
end)

Modifying Arguments

You can change arguments before they reach the server. Build a new args list and call with those:

oldNamecall = hookmetamethod(game, "__namecall", function(self, ...)
local method = getnamecallmethod()
local args = {...}

-- Example: inflate the damage value before it's sent
if method == "FireServer" and self.Name == "DamageRemote" then
local target = args[1]
local originalDamage = args[2]
if type(originalDamage) == "number" then
args[2] = originalDamage * 10 -- 10x damage
end
return oldNamecall(self, table.unpack(args))
end

return oldNamecall(self, ...)
end)
caution

Modifying arguments only works if the server doesn't validate the values. Most competent servers validate damage, positions, etc. and reject or clamp impossible values.

Blocking a Remote

Return early without calling oldNamecall to block the call entirely:

oldNamecall = hookmetamethod(game, "__namecall", function(self, ...)
local method = getnamecallmethod()

-- Block the animation remote to reduce server calls (silently)
if method == "FireServer" and self.Name == "AnimationRemote" then
return -- blocked — never reaches server
end

return oldNamecall(self, ...)
end)

Intercepting Incoming Events

To intercept events the server sends to you (before the game's handlers run), hook the OnClientEvent signal or use hookfunction on the callback:

-- Hook a specific incoming remote
local healthSync = game.ReplicatedStorage:WaitForChild("HealthSync")
local originalFire = hookfunction(healthSync.OnClientEvent.Fire, function(self, ...)
local hp = select(1, ...)
print("Server synced my HP to:", hp)
return originalFire(self, ...)
end)

Alternatively, just connect your own handler alongside the game's handler:

healthSync.OnClientEvent:Connect(function(hp)
print("HP received:", hp)
-- You see this PLUS the game's own handler also runs
end)

Hooking Environment Functions

You can also hook functions in the game's script environments to intercept calls the game makes internally. This requires getsenv (get script environment):

local gameScript = Players.LocalPlayer.PlayerScripts:FindFirstChild("GameClient")
if gameScript then
local env = getsenv(gameScript)
local oldAttack = env.AttackFunction
env.AttackFunction = function(...)
print("Game called AttackFunction with:", ...)
return oldAttack(...)
end
end

Cleanup

Always store your hook references. If your script gets re-executed, you'll double-hook. Use a flag:

if _G._hookInstalled then return end
_G._hookInstalled = true

local oldNamecall
oldNamecall = hookmetamethod(game, "__namecall", function(self, ...)
-- ...
return oldNamecall(self, ...)
end)

Or disconnect/unhook by storing the originals and restoring them when done.