> ## Documentation Index
> Fetch the complete documentation index at: https://starcryptdocs.starsolenterprise.com/lua-docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Anti-Debug Techniques for Lua/Luau

> Detect and respond to debuggers, hook injection, and dynamic analysis tools in Lua and Luau scripts to deter runtime reverse engineering.

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:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function is_debug_library_present()
    return type(debug) == "table"
        and type(debug.sethook) == "function"
        and type(debug.gethook) == "function"
        and type(debug.getinfo) == "function"
end
```

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](/lua-docs/lua-docs/obfuscation/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:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function is_hook_active()
    if not is_debug_library_present() then
        return false
    end
    local hook, mask, count = debug.gethook()
    return hook ~= nil or (mask ~= nil and mask ~= "") or (count ~= nil and count > 0)
end
```

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:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function fingerprint_function(fn, expected_pattern)
    local repr = tostring(fn)
    return string.match(repr, expected_pattern) ~= nil
end
```

Use this to verify the integrity of commonly hooked functions. Here is a check for the top five targets:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function check_common_hooks()
    local targets = {
        { fn = print,      pattern = "^function: 0x" },
        { fn = tostring,   pattern = "^function: 0x" },
        { fn = tonumber,   pattern = "^function: 0x" },
        { fn = pcall,      pattern = "^function: 0x" },
        { fn = load,       pattern = "^function: 0x" },
    }

    for _, target in ipairs(targets) do
        if not fingerprint_function(target.fn, target.pattern) then
            return true, target.fn
        end
    end
    return false, nil
end
```

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+):

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _orig_print_dump = string.dump(print)

local function is_print_hooked()
    return string.dump(print) ~= _orig_print_dump
end
```

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:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function timing_check(threshold_seconds)
    collectgarbage("stop")
    local start = os.clock()

    -- Known-fast operation: empty loop
    for i = 1, 1000000 do
        local _ = i + 1
    end

    local elapsed = os.clock() - start
    collectgarbage("restart")

    return elapsed > threshold_seconds
end
```

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:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function roblox_timing_check(threshold)
    local start = os.clock()
    for i = 1, 1000000 do
        local _ = i + 1
    end
    return (os.clock() - start) > threshold
end
```

## 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:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _orig = {
    load      = tostring(load),
    print     = tostring(print),
    tostring  = tostring(tostring),
    pcall     = tostring(pcall),
}
```

Later, verify them:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function is_environment_tampered()
    return tostring(load)     ~= _orig.load
        or tostring(print)    ~= _orig.print
        or tostring(tostring) ~= _orig.tostring
        or tostring(pcall)    ~= _orig.pcall
end
```

### 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:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function is_string_metatable_tampered()
    return getmetatable("") ~= nil
end
```

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`:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local RunService = game:GetService("RunService")

local function is_running_in_studio()
    return RunService:IsStudio()
end
```

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:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function is_expected_context(expected_server)
    local is_server = RunService:IsServer()
    return is_server == expected_server
end
```

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:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function is_parent_correct(expected_parent)
    return script.Parent == expected_parent
end

-- Example usage:
if not is_parent_correct(game:GetService("ServerScriptService")) then
    -- Possible tampering
end
```

### Player-Based Checks

For `LocalScript` instances, verify that the script is associated with the expected player:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local Players = game:GetService("Players")
local LocalPlayer = Players.LocalPlayer

local function is_valid_player()
    return LocalPlayer ~= nil
        and LocalPlayer.UserId > 0
        and LocalPlayer.Parent == Players
end
```

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

| Strategy | Description | Risk |
| - | - | - |
| Silent Disable | Stop executing sensitive logic without error | Low visibility, analyst may not notice |
| Fake Mode | Continue running but produce wrong outputs | Misleads analyst, wastes their time |
| Crash | Trigger a misleading error or infinite loop | Obvious, but analyst knows something is protected |
| Log and Continue | Record the event and proceed normally | Allows monitoring without disrupting users |

### 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:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function enter_fake_mode()
    local fake_math = {}
    setmetatable(fake_math, {
        __index = function(_, key)
            return function()
                return 0
            end
        end
    })

    -- Replace the real math library
    math = fake_math
end
```

If an analyst is tracing `math.random` to extract a key, they will receive all zeros and waste time analyzing meaningless output.

> [!TIP]
>
> 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.

> [!WARNING]
>
> 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.