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.
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
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))
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
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")
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_OBFandLPH_NO_OBFbehave the same way. - Visible by design: do not use this macro to conceal passwords, API secrets or other private values.
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)
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()
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)
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()
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,
}
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:
checkmust 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:
expectedKeyis 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.
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 macro | Comment 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,
}
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
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.
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.
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.
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
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
}