Protect your Luau scripts
LionFuscator turns readable Luau/Roblox source into a protected file that runs identically but is extremely hard to read, edit, or steal. It layers a bytecode virtual machine, an encrypted VM loader, runtime-tied keys, string/constant encryption, control-flow flattening, anti-tamper & anti-hook, and per-build metamorphism - the same class of protection as Luraph.
Quick start
- Open the Dashboard and paste your
Luau source, or drag a
.luafile onto the drop zone. - Pick a preset (start with
max- the default - orperformancefor FPS-sensitive game code). - Press Obfuscate. You get the protected file in seconds, with a validation check that confirms the output is structurally sound.
- Copy or Download the result and test it in your executor.
Every build spends one credit. Each build is also metamorphic - obfuscating the same file twice produces two structurally different outputs, so signatures never transfer.
Presets
A preset is a bundle of protection layers tuned for a trade-off between strength and runtime cost. Stronger presets add more layers; the only layer with meaningful per-frame cost is the per-function VM (see FPS).
| Preset | Best for | What it adds |
|---|---|---|
light | Quick, minimal protection | Variable renaming + string encryption. |
balanced | General scripts | + number obfuscation, control-flow flattening, property & builtin encryption, 2 decoders. |
performance | FPS-critical game code | Every cheap-but-strong layer (encrypted strings, flattening, opaque predicates, property/builtin encryption, anti-tamper, anti-hook, metamorphism) but no per-instruction VM. |
max default |
Strong protection, most scripts | Everything in performance + expression VM + property/method encryption + 3 decoder & 3 VM pools + runtime-tied keys. |
ultra | Maximum protection | max + function-body virtualization + encrypted VM loader + 8 decoders + anti-hook probe. |
performance preset.
Protection layers
Bytecode virtualization
Function bodies are lifted into a custom virtual machine with a per-build instruction set. The source-level logic is not present in the output - only virtualized code is.
Encrypted VM loader
The interpreter itself ships encrypted and is reconstructed at load. The output file on disk is not directly readable execution material.
Runtime-tied keys
Decryption keys are derived at load time from runtime state - never baked as literals. Recovering any string or constant requires reproducing the live derivation.
String & constant encryption
Every string, number, property name and table key is encrypted and split across independent decode paths. Nothing readable survives a scan.
Control-flow flattening
Straight-line bodies become dispatch-loop state machines with shuffled states and predicate guards - the original source order is not recoverable.
Anti-tamper & anti-hook
The engine binds decryption to the integrity of the runtime it loads into. If the environment has been modified, output resolves to unusable data - there is no clean intermediate to walk away with. Layered cross-checks make partial tampering equally unproductive.
Property/method encryption
Property access and method calls are rewritten through encrypted lookups. No Roblox API name is readable in the output.
Metamorphic output
Structural details are randomized per build. Tools and signatures tuned to one output do not transfer to the next.
FPS & performance
Most layers (string/const encryption, flattening, property encryption,
anti-tamper/hook, metamorphism) are essentially free at runtime. The one
layer with real per-frame cost is the per-function bytecode VM in
ultra. Three ways to keep FPS high:
- Use the
performancepreset. It keeps every cheap-but-strong layer and drops only the per-instruction VM - ideal for scripts that run every frame. - Auto-native (on by default in ultra). Trivial pass-through
functions - a lone call or a lone
returnof a call/field/value, like one-line UI handlers and thin per-frame dispatchers - are kept native automatically. Zero security loss: those bodies have no logic the VM could hide. - Hot event callbacks stay native (on by default in ultra).
Functions connected to per-frame and per-input signals -
RenderStepped,Heartbeat,Stepped,InputChanged,BindToRenderStep, and the rest - run native, because they fire many times per second and are the main FPS cost. Every other layer (renaming, string/const/property encryption, flattening, anti-tamper/hook) still applies to them, and every function they call stays fully virtualized. Turn off Native render / input callbacks in the dashboard's VM & FPS card to virtualize these too. - Max FPS switch. The dashboard's “Max FPS (disable function VM)” checkbox turns the per-function VM off for the whole file while keeping every other ultra layer.
Keeping specific functions native
For surgical control, annotate a hot function with a
--@native comment on the line directly above it. That body
stays native (no VM overhead) while everything else is still fully
virtualized - the best of both worlds for aimbots, render loops, and
tight per-frame code.
--@native
local function onRenderStep(dt) -- runs native, no VM overhead
updateAimbot(dt)
end
To exclude a whole block at once - a UI library, a rendering
module, anything that runs hot and holds no secrets - wrap it between
--@native-begin and --@native-end. Every
function inside the region runs native; everything outside stays fully
virtualized. Regions can nest, and an unclosed --@native-begin
runs to the end of the file.
--@native-begin
local UI = {}
function UI.draw(...) ... end -- native
function UI.layout(...) ... end -- native
--@native-end
local function validateKey(k) -- still fully virtualized
...
end
Only the per-function bytecode VM is skipped in a native region - renaming, string / constant / property encryption, control-flow flattening, anti-tamper and anti-hook still apply, and every function the region calls stays virtualized.
Compression ultra only
Ultra stages its interpreter through an encrypted VM loader. That one-time loader blob can be LZW-compressed to cut file size and load-time download - applied under the encryption (compress-then-encrypt), so it never exposes plaintext and never changes in-game FPS.
| Level | Dictionary | Notes |
|---|---|---|
0 | - | Off (default) |
1 | 12-bit | Lightest; smallest runtime table |
2 | 14-bit | Balanced |
3 | 16-bit | Best ratio (helps most on large scripts) |
Header / watermark Premium
Every build carries a short branded header comment by default. Its contents are metamorphic - no two builds share the same layout, so it is not a fixed signature.
On a paid plan you can, from the dashboard, choose:
- Branded (default) - the standard LionFuscator header.
- No header - completely clean, unbranded output.
- Custom note - your own one-line comment (e.g.
© MyGame 2026). It's sanitized so it can never break out of the comment.
Credits & plans
Access is metered by credits: every successful build spends one. Subscription plans refill to their monthly allotment on a rolling 30-day window; the one-time Now token adds a prepaid batch that never expires.
| Plan | Credits | Notes |
|---|---|---|
| Free | 20 / month | Every preset, no card needed. |
| Now | 1 (one-time) | Single-use token, no subscription. |
| Basic | 500 / month | Monthly subscription. |
| Premium | 1000 / month | + Premium header settings. |
| Commercial | 2000 / month | + obfuscation API. |
| Enterprise | Custom | Curated limits & support. |
Your remaining balance shows in the header and on the dashboard. When you run out, upgrade or grab a token. A build that fails validation does not spend a credit - you're only charged for a validated output. The Commercial and Enterprise plans also include the obfuscation API and let you manage subscriptions (cancel, invoices, payment method) from the dashboard's Billing card.
API Commercial & Enterprise
Obfuscate scripts programmatically over HTTPS. Same engine, same presets, same output as the dashboard - spends one credit per successful build.
Create a key
On the dashboard, open the API keys card, give the key a label, and hit Create API key. The full key is shown once - save it somewhere safe (a password manager or your bot's secrets store). We only store its prefix and a one-way hash; if you lose it, revoke it and create a new one.
Key format: lion_<32 alphanumeric chars>.
Endpoint
POST https://lionfuscator.onrender.com/api/v1/obfuscate
Authorization: Bearer lion_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{
"source": "local x = 1\nprint(x)\n",
"preset": "ultra", // light | balanced | performance | max | ultra
"name": "myscript.lua",
"seed": null, // optional, deterministic build seed
"compress_level": 0, // 0-3, ultra only
"disable_func_vm": false, // FPS switch, ultra only
"auto_native": true, // ultra only, on by default
"watermark": null // null=branded, ""=none, string=custom
}
Response
{
"ok": true,
"job_id": 42,
"output": "-- ... protected Lua ...",
"validated": true,
"warnings": [],
"preset": "ultra",
"seconds": 1.4,
"in_bytes": 12345,
"out_bytes": 62000,
"growth_pct": 402,
"credits": 998
}
Errors
| HTTP | error | Meaning |
|---|---|---|
| 401 | missing_api_key / invalid_api_key |
No Authorization: Bearer … header, or the key is unknown/revoked. |
| 402 | out_of_credits |
Balance is 0 - upgrade or wait for the monthly refill. |
| 403 | api_not_available |
Owner's plan was downgraded off Commercial/Enterprise. |
| 413 | - | Source larger than 2 MB. |
| 422 | - | Engine could not parse the source (message names the exception). |
| 429 | rate_limited |
Per-key quota exceeded (Commercial 60/min, Enterprise 240/min). |
Example - curl
curl -X POST https://lionfuscator.onrender.com/api/v1/obfuscate \
-H "Authorization: Bearer $LIONFUSCATOR_KEY" \
-H "Content-Type: application/json" \
--data @request.json \
| jq -r '.output' > script.protected.lua
Example - Python
import os, requests
with open('script.lua', encoding='utf-8') as f:
source = f.read()
resp = requests.post(
'https://lionfuscator.onrender.com/api/v1/obfuscate',
headers={'Authorization': f'Bearer {os.environ["LIONFUSCATOR_KEY"]}'},
json={'source': source, 'preset': 'ultra', 'name': 'script.lua'},
timeout=120,
)
data = resp.json()
if not data.get('ok'):
raise RuntimeError(data.get('message') or data.get('error'))
with open('script.protected.lua', 'w', encoding='utf-8') as f:
f.write(data['output'])
print(f'Built in {data["seconds"]}s, {data["credits"]} credits left.')
Account & 2FA
Everything account-related lives on the Account page - a link is in the header next to your username. It has:
- Profile - your plan, credit balance, refill date.
- Change password - confirms with your current password. New password must be ≥ 8 characters and different from the old one.
- Change email - confirms with your password. The new address is marked unverified and a verification link is emailed to it.
- Two-factor authentication (TOTP) - see below.
- Delete account - permanently removes your account, obfuscation history, saved outputs, and API keys. Requires your password + typing your username to confirm. Cancel any active subscription first from the dashboard's Billing card.
Setting up 2FA
2FA adds a second sign-in factor so a stolen password alone can't log in. LionFuscator uses standard TOTP - the same protocol Google Authenticator, Authy, 1Password, Bitwarden, and every serious password manager already support.
- Open Account → Two-factor auth and click Set up 2FA.
- Scan the QR code with your authenticator app (or paste the shown secret manually).
- Enter the 6-digit code your app now shows to prove enrollment worked - 2FA is only turned on once we've verified a live code, so a mis-scan can't lock you out.
- Save your 10 backup codes. Each works once (format
abcd-1234) and lets you sign in if you lose your device. You can regenerate them at any time - old codes stop working.
To sign in with 2FA enabled, enter your password as usual, then the 6-digit code (or a backup code) on the next screen. Backup codes are one-shot - using one removes it from your account.
To turn 2FA off, go back to Account → Two-factor auth and confirm with your password. The secret and any unused backup codes are wiped.
Security model
LionFuscator raises the cost of reverse-engineering enormously, but no client-side protection is unbreakable - the machine that runs your script can, in principle, observe it. What LionFuscator guarantees:
- No readable surface. After
max/ultra, no string, number, Roblox property, method name, or global appears in plaintext. - Execution-gated recovery. Keys are derived at runtime from a tamper-tainted master, so strings and constants can't be resolved by static analysis alone.
- Correct by construction. The VM runs with Lua's own semantics and conservatively falls back on anything outside its proven subset - the worst case is "a function isn't virtualized", never a broken script.
- Metamorphic. Every build differs, so a signature or a script written against one output doesn't transfer to the next.
FAQ & troubleshooting
My protected script errors in the executor.
Re-run with the performance preset (no per-function VM) to
isolate whether the per-function VM is involved, and confirm the
original script runs cleanly first. Always test one build before
shipping. If a specific function misbehaves under ultra, mark it
--@native or use the Max FPS switch.
"This file is already obfuscated."
You pasted a previously-protected file. Obfuscate the original source, not an output - double-obfuscation isn't supported.
The output is much larger than my input.
Expected - the VM interpreter, decoders and encrypted tables add fixed overhead. Turn Compression on in the dashboard (levels 1-3) to shrink the file. It doesn't change runtime FPS - only the one-time load-time decompress.
Does obfuscation slow my game down?
Only the per-function VM in ultra has per-frame cost. Use
performance, auto-native, or --@native for hot
paths - everything else is effectively free.
Is my source code stored?
No. We store build metadata and the protected output so you can re-download from history - never your plaintext input.
Ready to protect a script? Open the dashboard →