REFERENCE / LUAQ

Documentation

This page shows what Luaq supports and how to use it. The call-style macros work with both the LUAQ and LPH prefixes. Comment directives use the exact --!luaq: format shown below.

01

Overview

Luaq macros tell the obfuscator what you want done while the script is being built. Put them in normal Luau, obfuscate the script, and Luaq handles them before the protected output is made.

For local testing, keep your shims inside if not LUAQ_OBFUSCATED then ... end. Luaq removes that branch from the protected build.

Some macros also have comment versions. Use those when you want the same behavior without wrapping a function or table, especially when you want Luau type checking to keep seeing a normal declaration.

02

LUAQ_OBFUSCATED

This becomes true when Luaq builds the script. It is mainly for test code or shims you want in the normal source but not in the obfuscated output.

if not LUAQ_OBFUSCATED then
    print("dev mode active")
end
03

LUAQ_INLINE(fn)

Use this on a small helper you want Luaq to inline. Supported calls get the helper body directly, and the result still goes through the rest of the protection pipeline.

  • Scope stays clean: every inlined call gets its own bindings, so locals do not leak into another call.
  • Arguments run once: Luaq evaluates the arguments before it substitutes the function body.
  • Still protected: the inlined code goes back into the script and can be virtualized with everything else.
  • Keep it small: this is meant for compact helpers. The current limit is 256 AST nodes.
  • Supported code: expressions, normal blocks, control flow, multiple returns in assignments, nested inline helpers, stable read-only upvalues and table field changes.
local add = LUAQ_INLINE(function(a, b)
    return a + b
end)
print(add(10, 20))
local state = { clicks = 0 }
local bump = LUAQ_INLINE(function(amount)
    state.clicks = state.clicks + amount
    return state.clicks
end)
print(bump(2))
04

LUAQ_CRASH()

Stops the current thread when this path is reached. Each use gets its own randomized crash path instead of one obvious static pattern.

if tampered then
    LUAQ_CRASH()
end
05

LUAQ_ENCSTR(string)

Encrypts a string literal while building the script and restores it only when the protected script needs it. Pass a literal string, not a variable or dynamic concatenation.

local apiEndpoint = LUAQ_ENCSTR("https://api.internal.domain/v1/authenticate")
06

LUAQ_NO_OBF(string)

Leaves a literal string unobfuscated in the generated script. Unlike LUAQ_ENCSTR, this macro keeps the string visible in the output while preserving its decoded bytes exactly.

Pass exactly one literal string. Quoted strings and Luau long-bracket strings both work. Variables, concatenation, function calls and other expressions are not accepted.

local verificationKey = LUAQ_NO_OBF("123456789...")
local verificationKeyLong = LUAQ_NO_OBF([=[123456789...]=])
local alternativeKey = LPH_NO_OBF("123456789...")
  • Compile-time only: Luaq handles the macro while building your script. The obfuscated output needs no runtime function or import.
  • Exact bytes: the decoded string value stays unchanged, including escape sequences interpreted by Luau.
  • Both prefixes: LUAQ_NO_OBF and LPH_NO_OBF behave the same way.
  • Visible by design: do not use this macro to conceal passwords, API secrets or other private values.
07

LUAQ_ENCNUM(number)

Protects a static number literal during the build. Integers and floats are encoded into the output and recovered when they are used.

local bindPort = LUAQ_ENCNUM(8443)
08

LUAQ_ENCFUNC(fn)

Encrypts a function at build time. The normal one-argument form lets Luaq handle the keys. The three-argument form is only for setups that provide their own encryption and decryption keys.

  • 1 argument: pass the function and let Luaq do the rest.
  • 3 arguments: use your own encryption and decryption key inputs if your setup actually needs them.
local protectedTask = LUAQ_ENCFUNC(function()
    return executeCriticalRoutine()
end)
protectedTask()
09

LUAQ_NO_VIRTUALIZE(fn)

Keeps a function out of the custom bytecode VM. Luaq can still rename and transform it, but the function runs as normal Luau. Use this for hot callbacks or tight loops where you do not want VM overhead.

local renderStep = LUAQ_NO_VIRTUALIZE(function(deltaTime)
    updateFrame(deltaTime)
end)
RunService.RenderStepped:Connect(renderStep)
10

LUAQ_NO_UPVALUES(fn)

Builds the function without normal lexical upvalue capture. Use it when the function should stay isolated from locals around it and your runtime supports the required environment lookup behavior.

local isolatedFn = LUAQ_NO_UPVALUES(function()
    return 123
end)
isolatedFn()
11

LUAQ_INDEX_TO_NUM(table)

LUAQ_INDEX_TO_NUM(table) takes table keys Luaq can find statically and swaps them for randomized numeric indexes during the build. Matching field accesses get rewritten to the same numbers.

The numbers change between builds. This happens at build time, so there is basically no extra runtime work from the conversion itself.

local state = LUAQ_INDEX_TO_NUM({
    _luaq_health = 100,
    _luaq_alive = true,
})
print(state._luaq_health)
state._luaq_alive = false

Conceptual obfuscated result

local state = {
    [5831] = 100,
    [194] = true,
}
print(state[5831])
state[194] = false
  • Random per build: the numeric indexes are regenerated, so do not expect the same numbers next build.
  • Static access only: Luaq can only rewrite fields it can identify from the source. Dynamic access like state[key] may not be safe to convert.
  • The prefix is optional: using something like _luaq_ is still a good idea so unrelated fields do not get matched by accident.
  • Do not break external tables: avoid this on tables sent to APIs, serialized, reflected, or used by anything that expects the original string keys.
  • This is obfuscation: it makes the table harder to read. It is not access control.

Comment-directive form

--!luaq:index_to_num
local state = {
    _luaq_health = 100,
    _luaq_alive = true,
}
12

LUAQ_PRECHECK(check, expectedKey)

LUAQ_PRECHECK(check, expectedKey) and LPH_PRECHECK(check, expectedKey) do the same thing. Use them as statements. They return nothing and Luaq removes them from the normal program after extracting the check.

PRECHECK runs once during startup before server authorization and before the main program loads. If it errors, returns the wrong shape, or does not match the expected key, startup stops.

LUAQ_PRECHECK(function()
    return tonumber(game:HttpGet("https://your-auth-service.example/key"))
end, 0x12345678)

LPH_PRECHECK(function()
    return {bit32.bxor(0x1200, 0x34), -0x56789}
end, {0x1234, -0x56789})
  • Anonymous function only: check must be a direct anonymous function with no parameters or varargs. Globals are fine, and locals created inside the check are fine, but it cannot capture locals from outside.
  • Exact integer key: expectedKey is one integer literal or an array of 1 to 256 integer literals. Negative and hex values are supported. The runtime result has to match the same shape exactly.
  • Always startup: PRECHECK declarations run once in source order before the main program. Putting one inside a branch or function does not make it conditional.
  • Startup cost only: the check work happens during startup. It does not add work to the main VM loop, and scripts without PRECHECK do not get its extra startup path.
  • Use a real check: hardcoding the expected value is fine for testing, but it is not real authorization.

The URL in the example is just a placeholder. PRECHECK does not add HTTP or networking by itself. Use whatever your target runtime already gives you. If the check calls a global helper, that helper needs to exist before the generated script starts.

13

Comment directives

These directives give you the same Luaq behavior as the matching macro call, but your source stays as a normal function or table declaration. That is useful when you want Luau type checking to see normal code.

A directive only applies to the compatible function or table directly under it, so keep it right above the thing you want it to affect.

Call-style macroComment directive
LUAQ_NO_VIRTUALIZE(function() ... end)--!luaq:no_virtualize
LUAQ_NO_UPVALUES(function() ... end)--!luaq:no_upvalues
LUAQ_INLINE(function() ... end)--!luaq:inline
LUAQ_INDEX_TO_NUM({...})--!luaq:index_to_num

--!luaq:no_virtualize

--!luaq:no_virtualize
local function handlePreRender(deltaTime)
    local fps = math.round(1 / deltaTime)
    label.Text = `FPS: {fps}`
end

--!luaq:no_upvalues

--!luaq:no_upvalues
local function isolatedFunction()
    return 123
end

--!luaq:inline

--!luaq:inline
local function add(a, b)
    return a + b
end

--!luaq:index_to_num

--!luaq:index_to_num
local state = {
    _luaq_health = 100,
    _luaq_alive = true,
}
14

LUAQ_LINE

Gets replaced with the current source line number before parsing. It is a token, not a function, so use LUAQ_LINE, not LUAQ_LINE().

local currentLine = LUAQ_LINE
15

Aggressive Optimizations

Runs the heavier optimization pass before protection is finalized. It folds constants, removes dead code, simplifies expressions and reuses VM registers when it is safe.

For normal scripts, this is usually the option you leave enabled.

16

VM Compression

Makes the emitted VM payload smaller. Use it when the protected output is getting bigger than you want. Standard Luau CLI does not support this option.

17

Reserved Prefix Guard

Names starting with LUAQ_ are reserved for Luaq. If you create your own variable or function with that prefix and it is not a real macro, the build can reject it.

18

Full Shim Block

If you want the call-style macros to do nothing while testing normal Luau, use a shim like this. Keep it under if not LUAQ_OBFUSCATED then so Luaq removes it from the protected build.

if not LUAQ_OBFUSCATED then
    LUAQ_INLINE = function(fn) return fn end
    LUAQ_CRASH = function() while true do end end
    LUAQ_ENCSTR = function(s) return s end
    LUAQ_NO_OBF = function(s) return s end
    LPH_NO_OBF = LUAQ_NO_OBF
    LUAQ_ENCNUM = function(n) return n end
    LUAQ_ENCFUNC = function(fn, _e, _d) return fn end
    LUAQ_NO_VIRTUALIZE = function(...) return ... end
    LUAQ_NO_UPVALUES = function(...) return ... end
    LUAQ_INDEX_TO_NUM = function(t) return t end
    LUAQ_PRECHECK = function(_check, _expectedKey) end
end
19

Ultra API

Ultra is the only normal plan with full API access. If you have Ultra and want the API enabled, join our Discord server and DM @followingdeath with the user ID 1327232266530848811.

The API request itself is simple. Send JSON to the endpoint below with your Bearer token. The successful response is the obfuscated script as plain text.

POST https://highly-laden-rrp-endless.trycloudflare.com/v1/obfuscate
Authorization: Bearer <YOUR_API_TOKEN>
Content-Type: application/json

{
    "source": "print('Hello from Luaq!')",
    "runtime": "roblox",
    "aggressive-optimizations": true,
    "vm-compression": false
}