Skip to main content
Anti-debug techniques defend your Lua and Luau scripts against dynamic analysis, where an attacker runs your code under a debugger, injects hooks, or instruments function calls to observe behavior and extract secrets. By detecting these intrusions at runtime, your script can alter its behavior, mislead the analyst, or refuse to execute sensitive logic. This page covers debug library detection, hook fingerprinting, timing checks, environment integrity verification, Luau-specific checks, and safe response strategies.

The Debug Library

Lua provides a powerful debug library that analysts use to inspect running code. Functions like debug.sethook, debug.getinfo, debug.getlocal, and debug.getupvalue allow an attacker to trace execution, read local variables, and inspect closures. Your first line of defense is detecting whether this library is present and whether it is being actively used against you.

Checking for the Debug Library

Start by verifying that debug exists and has not been replaced with a stub:
If debug is missing entirely, that may indicate a sandboxed environment rather than an active debugger. Treat this as a separate signal (see Sandbox Detection).

Detecting Active Hooks

If the debug library is present, check whether a hook has been installed. The debug.gethook() function returns the current hook function, mask, and count. If any of these are non-nil, a debugger is likely tracing your execution:
If is_hook_active() returns true, your script is running under a debugger or tracer that has set a line, call, or return hook.

Hook Detection and Countermeasures

Analysts often hook standard library functions like print, tostring, and pcall to intercept arguments and return values. You can detect these hooks by comparing the string representation of functions against expected patterns.

Function Fingerprinting

In standard Lua, native functions print as function: 0xADDRESS. In LuaJIT, they print as builtin#N. If a function has been wrapped or replaced with a Lua closure, its string representation changes:
Use this to verify the integrity of commonly hooked functions. Here is a check for the top five targets:
If check_common_hooks() returns true, one of these functions has been replaced or wrapped.

Closure Hooking

In Lua, type(print) always returns "function" even if the function has been replaced with a closure. The tostring trick above is more reliable. For additional confidence, compare the function’s bytecode identity using string.dump (Lua 5.1+):
Capture the original dump at script startup, before any attacker has had a chance to hook the function.

Timing-Based Detection

A debugger stepping through code inflates execution time. By measuring a known-fast operation, you can detect whether a human or automated debugger is slowing execution.

Using os.clock()

Use os.clock() for high-resolution timing in Lua. Measure a tight loop and compare against a threshold:
Garbage collection can inflate timing, so pause it with collectgarbage("stop") before measuring and restart it afterward. Choose a threshold based on your target platform. On a modern CPU, one million integer additions should take well under 0.1 seconds. If the elapsed time exceeds 0.5 seconds, a debugger is likely present.

Luau and Roblox Timing

In Luau and Roblox, os.clock() is available and preferred for high-resolution timing. tick() is also available but deprecated. Use os.clock() for consistency:

Environment Integrity Checks

Verify that critical functions have not been replaced since script startup. Capture their original string representations at load time and compare them periodically or before sensitive operations.

Original Reference Capture

At the top of your script, store the original representations:
Later, verify them:

Metatable Tampering

In stock Lua, getmetatable("") returns nil because strings do not have a user-accessible metatable by default. If an analyst has attached a metatable to strings to intercept operations, this check will fail:
You can extend this pattern to other types that should not have metatables in your expected environment.

Luau and Roblox Specific Checks

Luau and Roblox expose additional APIs that help you detect development and analysis environments.

Detecting Roblox Studio

Roblox Studio is the primary environment where analysts decompile and debug scripts. Detect it with RunService:
If your script is intended for live servers only, treat IsStudio() as a debug signal.

Verifying Execution Context

Check whether the script is running on the server or client as expected:
If a LocalScript is unexpectedly running on a server, or a Script on a client, the environment may be manipulated.

Script Parent Verification

Verify that your script resides in the expected service:

Player-Based Checks

For LocalScript instances, verify that the script is associated with the expected player:
An exploiter may clone a LocalScript into an unexpected context. Parent and player checks help catch this.

Responding to Detection

Once you detect a debugger or tampered environment, you have several response options. Choose based on your threat model and tolerance for false positives.

Response Strategies

Fake Mode Pattern

Fake mode is one of the most effective responses. Replace real function logic with decoy implementations that produce plausible but incorrect results:
If an analyst is tracing math.random to extract a key, they will receive all zeros and waste time analyzing meaningless output.
Randomize which checks run and in what order on each execution. If an attacker discovers one check, they should not be able to predict or bypass the others. Use a seeded shuffle based on a runtime value like os.time() to vary the check sequence.
False positives are a real concern. The debug library is used legitimately by profilers, test frameworks, and some game engines. Aggressive anti-debug checks may break legitimate tooling. Provide a development-only disable mechanism, such as an environment variable or a signed flag, so your team can debug without triggering protections.