Overview
Luaq macros are compile-time instructions for the obfuscator. You put them in normal Luau, Luaq reads them during the build, then applies the requested transform before the protected output is emitted.
For local development, keep your shims inside if not LUAQ_OBFUSCATED then ... end. That branch is dead in an obfuscated build and is removed by the pipeline.
Some macros also have comment-directive forms. They are useful when you want the same Luaq behavior without wrapping a function or table in a macro call, especially if you want Luau language servers and type checking to see an ordinary declaration.
LUAQ_OBFUSCATED
This becomes the boolean true at compile time. It is mainly for development-only code that should exist while you test the source but should not survive into the protected build.
if not LUAQ_OBFUSCATED then
print("dev mode active")
end
LUAQ_INLINE(fn)
Marks a small helper for compile-time inlining. Supported call sites get the helper body directly instead of paying for the normal helper closure and call boundary, while the resulting code still goes through the rest of the Luaq pipeline.
- Scope stays clean: each inlined call gets its own bindings instead of leaking locals or registers into another call site.
- Arguments are evaluated once: Luaq keeps argument evaluation deterministic before it substitutes the body.
- It still gets protected: the inlined code is merged back into the script and can be virtualized with the rest of it.
- Keep it small: the intended target is a compact helper, with the current safety threshold capped at 256 AST nodes.
- Supported shapes: expressions, normal blocks, control flow, multiple returns in assignment positions, nested inline helpers, stable read-only upvalues and table-field mutations.
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 that path is reached. Luaq emits a randomized crash path at each use so the protected output does not have to rely on one obvious static pattern.
if tampered then
LUAQ_CRASH()
end
LUAQ_ENCSTR(string)
Encrypts a string literal during the build and restores it when the protected script needs it at runtime. Pass a literal here, not a variable or a dynamic concatenation.
local apiEndpoint = LUAQ_ENCSTR("https://api.internal.domain/v1/authenticate")
LUAQ_ENCNUM(number)
Does the same kind of compile-time protection for a static numeric literal. Integers and floats are encoded into the protected build and recovered when used.
local bindPort = LUAQ_ENCNUM(8443)
LUAQ_ENCFUNC(fn)
Encrypts a function target at compile time. The normal one-argument form lets Luaq handle the key material and runtime recovery for you. The advanced three-argument form is there when your setup provides its own encryption and decryption keys.
- 1 argument: pass the function and let Luaq handle the rest.
- 3 arguments: provide custom encryption and decryption key inputs when your runtime setup actually needs that control.
local protectedTask = LUAQ_ENCFUNC(function()
return executeCriticalRoutine()
end)
protectedTask()
LUAQ_NO_VIRTUALIZE(fn)
Keeps a function out of the custom bytecode VM. The function can still go through identifier renaming and other source-level transforms, but it executes as native Luau. This is the one to use for hot callbacks or tight loops where VM overhead would be pointless.
local renderStep = LUAQ_NO_VIRTUALIZE(function(deltaTime)
updateFrame(deltaTime)
end)
RunService.RenderStepped:Connect(renderStep)
LUAQ_NO_UPVALUES(fn)
Builds the target function without normal lexical upvalue capture. Use it when the function is meant to stay isolated from surrounding locals and your execution environment supports the required environment-based lookup behavior.
local isolatedFn = LUAQ_NO_UPVALUES(function()
return 123
end)
isolatedFn()
LUAQ_INDEX_TO_NUM(table)
LUAQ_INDEX_TO_NUM(table) takes named keys that Luaq can identify statically and replaces them with randomized numeric indices at obfuscation time. Matching static field accesses are rewritten with the same generated numbers.
The generated indices are different between builds. This is a compile-time transform, so it adds essentially no runtime overhead once the output is produced.
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
- Builds are randomized: those numeric indices are generated per build, so do not expect the same values next time.
- Static access only: Luaq can rewrite fields it can identify from the source. Dynamic access such as
state[key]may not be safe to convert. - The prefix is optional: a distinctive prefix such as
_luaq_is still a good idea because it helps you avoid matching unrelated fields by accident. - Keep external contracts alone: do not use this on tables passed to external APIs, serialized, reflected upon, or accessed dynamically when another consumer expects the original string keys.
- It is obfuscation: numeric field replacement makes the structure less readable, but it is not an access-control mechanism.
Comment-directive form
--!luaq:index_to_num
local state = {
_luaq_health = 100,
_luaq_alive = true,
}
Comment directives
These directives give you the same obfuscator behavior as the matching call-style macro, but the source stays as a normal function or table declaration. That keeps Luau language servers and type checking happier because they do not have to understand a wrapper call around the declaration.
A directive applies to the immediately following compatible function declaration or table declaration. Keep the directive directly above the declaration it is meant to control.
| 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 AST parsing. It is a bare compile-time token, not a function, so use LUAQ_LINE and not LUAQ_LINE().
local currentLine = LUAQ_LINE
Aggressive Optimizations
Runs the heavier optimization pass before the protection layers are finalized. It folds constants, removes dead code, simplifies expressions and reuses VM registers where the compiler can do it safely.
If you are not relying on intentionally weird source behavior, this is normally the option you leave enabled.
Enhanced VM Compression
Reduces the emitted VM payload size. It is useful on larger scripts where the protected output starts getting heavier than you want to ship.
Reserved Prefix Guard
Names starting with LUAQ_ are reserved for Luaq macros. If you make your own variable or function with that prefix and it is not a recognized macro, the build can reject it instead of guessing what you meant.
Full Shim Block
If you want the call-style macros to run harmlessly in Studio or another normal Luau environment, use a development shim like this. Keep the whole thing under if not LUAQ_OBFUSCATED then so Luaq can remove 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_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
end
Enterprise API
Enterprise accounts can use the HTTP API for automated builds. The shared credential and the ready-to-copy curl commands stay inside the authenticated Dashboard under Enterprise API.
POST https://luaq.sillyfa.de/api/obfuscate
x-api-key: <shown in Enterprise dashboard>
file: script.lua