AegisLoader Docs

Introduction

AegisLoader protects your Roblox Luau and delivers it per run. Your script ships on a custom engine — renamed, string-encrypted, and virtualized — so the original source never leaves readable, and each download is armed and licensed on its own.

How protection works

When you upload a script it runs through four transforms before it ships:

  • Rename — every local, global and field becomes a meaningless token.
  • String encryption — string constants are sealed and decoded at runtime, never shipped in the clear.
  • Number rewriting — numeric literals are re-spelled into equivalent, unreadable forms.
  • Virtualization — your logic is translated into instructions for an engine shipped inside your build, so it never exists as readable Luau.

The first three are near-free at runtime. Virtualization is the strongest layer — and the heaviest to run, which is why hot code gets its own directive.

Directives

A directive is a one-line marker you place in your own source to tune how a piece of it is protected. Two rules hold for every directive:

  • It runs raw — with its one-line stub each marker is a harmless no-op (an identity passthrough, or for resume a plain false), so your script still runs correctly when executed unprotected. See Shimming.
  • The AEGIS_ prefix is reserved. Don't name your own locals or globals AEGIS_*; the obfuscator reads those names and rewrites them.

More directives land here as they ship. Each gets its own page with the exact rules and its raw-mode stub.

Integrating

Free, Paid and Ad builds ship a complete loader line — you paste one line and you're done. If you run Custom mode, your own UI fetches and applies the key; the Integration API documents those endpoints.

Directive · Performance

AEGIS_FASTLANEAEGIS_FASTLANE(fn) → fn

Run your hottest functions at native Luau speed — without taking protection off them.

Virtualized code is slow on a hot path: every step is fetched and decoded one at a time, which is invisible once a frame and a freeze sixty times a frame. AEGIS_FASTLANE marks a single function to build on the light tier — it runs as native Luau, but is still renamed and string-encrypted.

i
Light is not “off”. A fast-lane function is still renamed and its strings still encrypted — cheaply protected, never a readable hole.

Tiers

Heavy · default

Virtualized

  • Runs on the engine
  • Opcodes gone — no native Luau
  • Strongest; slowest per call
Light · AEGIS_FASTLANE

Native

  • Runs as native Luau
  • Still renamed + string-encrypted
  • Native speed; for hot paths

Using it

Add the one-line identity stub at the top of your script so it also runs un-obfuscated, then wrap any hot function literal:

local function AEGIS_FASTLANE(f) return f end  -- stub: identity when run raw

-- a per-frame loop — keep it off the heavy tier
local render = AEGIS_FASTLANE(function(dt)
    esp:redraw(); tracers:update(dt)
end)
RunService.RenderStepped:Connect(render)

-- a metamethod fires on every index
proxy.__index = AEGIS_FASTLANE(function(self, key) return resolve(self, key) end)

The wrapper is an expression that returns the function, so use it anywhere a function goes — assign it, connect it inline, store it, or return it.

When to use it

  • RenderStepped / Heartbeat / Stepped handlers and their helpers.
  • __index / __namecall metamethod hooks.
  • Tight no-yield loops — per-frame math, per-tick scans.

Rule of thumb: more than a handful of times per frame → light tier. Once at startup or on a click → leave it virtualized. Mark as few functions as you can; each one is moved off the strongest tier.

Common mistakes

  • Passing a named reference — AEGIS_FASTLANE(myFn) does nothing. Wrap the function literal in place.
  • Forgetting to use the return value — the marker returns the function; assign or connect what it gives back.
  • Reusing the reserved name for your own variable.

Rules

ArgumentExactly one function literal written in place. A named reference (AEGIS_FASTLANE(myFn)) does nothing.
ReturnsThe same function, unchanged — the marker is identity at runtime.
FormA bare call, AEGIS_FASTLANE(fn). An indexed call (t.AEGIS_FASTLANE(fn)) is not the directive.
KeptRename, string encryption, number rewriting.
SkippedVirtualization, control-flow flattening, junk insertion.
No markerYour build is byte-for-byte identical to one built without the feature.
Directive · Anti-tamper

AEGIS_FAULTAEGIS_FAULT()

A kill-switch. When your own tamper check decides something is wrong, call it and the protected build cleanly bricks itself for the rest of the run.

AEGIS_FAULT() is the reaction, not the detector — you write the check, it delivers the response. When reached, it scrubs the current VM frame, trips a session flag, and raises an error. From that point every protected function in the run returns nothing, so the script can't limp on in a tampered state.

i
It's the trigger, not the sensor. AEGIS_FAULT detects nothing on its own — its value is only as good as the check you wire to it.

Using it

Add the one-line stub so the raw script still runs, then call it as a bare statement inside your own detection:

local function AEGIS_FAULT() end  -- stub: no-op when run raw

-- your own checks decide WHEN to brick
if isHooked(game.HttpGet) or detectedDumper() then
    AEGIS_FAULT()
end

When to use it

  • Anti-tamper tripwire — a checksum of a critical function, or a honeypot value, failed.
  • Hook / dump-tool detection — a sensitive function was hooked, or a known logger is present.
  • Environment refusal — wrong executor, a sandbox, Studio, or an unexpected PlaceId.
  • Your own blacklist gate, on top of the platform's licensing.

Rules

FormA bare statement, AEGIS_FAULT(), with no arguments. Using it in an expression (return AEGIS_FAULT()), with arguments, or as call sugar fails the build — never a silent no-op.
EffectScrubs the VM frame, sets a session flag, scrubs the live key, and errors. Every later protected call in the run then halts too.
ScopeThe current run only — it's in-memory, so re-executing the script starts clean. For a persistent, cross-run ban use your key system's revocation, not this.
pcallCall it where the error can unwind. A fault buried inside a pcall your script survives won't stop that frame (the session flag still halts the next protected call).
No markerYour build is byte-for-byte identical to one built without the feature.
!
What it isn't. Not an anti-dump measure — someone who already pulled the decrypted code keeps it. It raises the cost of running a tampered build; it isn't a wall.
Directive · Secrets

AEGIS_SEALAEGIS_SEAL(value) → value

Force a specific string or number out of the shipped payload and into the loader-delivered vault — even when the automatic vault's budget would otherwise leave it in the clear.

AegisLoader already pulls string and number constants into a server-delivered table automatically, so they read nil in an offline dump. That table has a size budget, though — on a large script the budget fills and the overflow literals ship raw. AEGIS_SEAL forces a chosen value (a webhook, a token) past that budget so it's vaulted even on a large script — best-effort: the one case it can't is a value whose enclosing function is already at Luau's 200-local cap, where SEAL ships it raw rather than risk the build (see Declines).

i
Mostly for large scripts. On a small script your strings are already auto-vaulted — SEAL is redundant there. It earns its keep when that budget is exhausted, or for a tiny value the heuristics skip.

Using it

Add the identity stub, then wrap a single literal — assign it to one local and reference that:

local function AEGIS_SEAL(v) return v end  -- stub: identity when run raw

local WEBHOOK = AEGIS_SEAL("https://discord.com/api/webhooks/...")

Rules

ArgumentExactly one string or number literal, written in place — that's the only form that is vaulted. A variable or expression (AEGIS_SEAL(x), AEGIS_SEAL(a..b)) does not fail the build: it's passed through unchanged (identity) and is not vaulted, so the value ships in the clear. Only call sugar (AEGIS_SEAL "x", AEGIS_SEAL{...}) or the wrong argument count fails the build.
ReturnsThe value, unchanged — identity at runtime.
EffectThe literal becomes a vault lookup; it is not in the shipped build and reads nil on an offline dump.
DeclinesBest-effort: if the enclosing function is at Luau's local cap, SEAL ships the value raw rather than risk the build — it never bricks.
No markerYour build is byte-for-byte identical to one built without the feature.
!
Not at-rest encryption. The vault is delivered to anyone who runs the loader — SEAL keeps the value out of the static build and offline dumps; it doesn't hide it from a live, licensed run.
Directive · Faster launches

AEGIS_RESUMEAEGIS_RESUME(authorityId, loaderUrl) → boolean

A returning user whose key is still valid skips the key UI entirely — the product loader resumes from a sealed credential, with no re-validate round trips and no raw key left on disk. First-time and invalid users fall straight through to your normal UI.

On a successful unlock the server hands back an opaque resume credential — bound to the device and the key's own expiry — and the loader stores it in place of the raw key. On the next launch AEGIS_RESUME replays that credential: the server unseals it, recovers the key, and runs the exact same live key check — so a returning user loads in one step instead of the keysource+validate round trips.

i
It never weakens security. The credential only carries the key; the live check — ban, expiry, HWID, lease — still decides. A revoked or expired key is rejected and the user falls through to the key UI, exactly as if they'd typed a dead key. (On a reject — or a network blip, which looks the same to the loader — the credential is left in place, not deleted; it grants nothing because the live check rejects it every time, and avoids costing a valid returning user a needless re-ad on a blip.)
!
Enable the connected loaders first, then the key UI. The key UI only stores the credential and launches your product loader with the key blanked — the loader does the actual resume. So every connected product loader must be rebuilt (re-upload, or one ~3h autorotate) to carry the resume handshake before you put the resume key UI live. Do it in the other order — key UI first, while a connected loader is still a pre-resume build — and that loader launches with a blank key and shows your users session ended — revoked or expired. The connected scripts need no source change, just a rebuild.

Using it

Add the stub, then call it at the top of your key script and return when it resumes — otherwise fall through to your own UI in the same script:

local function AEGIS_RESUME(id, url) return false end  -- stub: no-op when run raw

if AEGIS_RESUME(CONFIG.Id, LOADER_URL) then return end  -- returning user resumed → done

-- ...your normal key UI runs here (prompt / ad gate / validate).
-- On a successful validate, persist the returned credential instead of the raw key:
--   local res = validateKey(key)            -- { ok, rt = "ar1..." }
--   if res and res.ok and res.rt then
--     writefile("AegisLoaderCache/aegis_resume_"..CONFIG.Id..".bin", res.rt)
--   end
!
It lives in your key script, so protect that script. AEGIS_RESUME only activates when the script it's in is itself protected with AegisLoader. If you host your key UI raw, the stub makes it a harmless no-op (returns false, your UI runs as normal) — to get resume without protecting the key UI, paste the full snippet from the Integration API instead.

Rules

ArgumentsauthorityId — your key authority (the key-system id for a shared connected script, otherwise the project id). loaderUrl — your product loader URL. Exactly two; any other form fails the build.
Returnstrue when it resumed — you should return. false when there's nothing to resume — fall through to your key UI.
EffectA returning user skips the keysource+validate round trips, and the raw key is never written to disk — only the sealed credential is.
RequiresBoth builds resume-ready: (1) the key script protected with AegisLoader so this directive expands, and (2) every connected product loader rebuilt (re-upload, or one ~3h autorotate) so it carries the resume handshake — the loader, not the key UI, performs the resume. Roll out the connected loaders first: deploying the resume key UI while a connected loader is still pre-resume makes it launch with a blanked key and show “session ended — revoked or expired.” Resume is on server-side platform-wide; the connected scripts need no source change, only a rebuild.
SecurityRevocation, expiry, ban and HWID stay live — the credential is a key-carrier, never an authorization.
No markerYour build is byte-for-byte identical to one built without the feature.
i
Full wire details — the additive rt field, the credential format, and a drop-in for key scripts you don't protect — are in the Integration API.
Guide

Shimming

Keep your script runnable un-obfuscated. A directive is a real name the obfuscator reads, so define a one-line stub and the raw file still runs in Studio.

When you protect a script, the obfuscator recognizes each directive and strips it — AEGIS_FASTLANE(fn) becomes just fn, and so on. But the raw file you test with has no such step: calling AEGIS_FASTLANE there would error with attempt to call a nil value. A shim is a passthrough definition that makes the directive a no-op when run raw.

The pattern

Every directive has a one-line identity stub. Define the ones you use at the top of your script:

-- raw-mode stubs — harmless, stripped when protected
local function AEGIS_FASTLANE(f) return f end
local function AEGIS_FAULT() end
local function AEGIS_SEAL(v) return v end
local function AEGIS_RESUME(id, url) return false end

Each directive's page lists its own stub. The rule is always the same: the stub returns its value unchanged (or does nothing), so your logic is identical whether or not the build is protected.

i
Define it once. The obfuscator rewrites each directive call into its real behavior, and your stub definition is left as an unused, inert local, so a shimmed directive costs nothing in the protected build.

Reserved names

The AEGIS_ prefix is reserved for directives. Don't name your own locals, globals or fields AEGIS_* — the obfuscator reads those names, and a collision will be rewritten or rejected. Keep your stubs the only AEGIS_ identifiers in the file.

Guide

Performance

Protection has a runtime cost, and most of it is avoidable. Where it matters is the heavy tier on hot code — and that's exactly what the directives are for.

Where the cost is

Rename, string encryption and number rewriting are near-free — they change how your code reads, not how fast it runs. Virtualization is the heavy layer: your logic runs on an engine, so one native operation becomes several engine steps. On code that runs once, you never feel it. On code that runs sixty times a frame, you do.

Keep hot code native

Mark genuinely hot functions with AEGIS_FASTLANE — they run as native Luau while staying renamed and string-encrypted. Reach for it on:

  • RenderStepped / Heartbeat / Stepped loops and the helpers they call every frame.
  • __index / __namecall hooks, which fire on every access.
  • Tight per-tick math and scans.

Mark as few functions as possible — each one leaves the strongest tier. Protect everything by default; fast-lane only what you've measured to be hot.

Optimize before you protect

Because one source operation can expand into many engine steps, sloppy code is far more expensive protected than raw. Cache lookups and cut redundant work first:

-- before: four chained lookups, every frame
a.b.c.d.x = 1
a.b.c.d.y = 2

-- after: resolve once
local t = a.b.c.d
t.x = 1
t.y = 2

Fewer instructions is always cheaper — and the win is larger once the code is on the engine.

!
The one hard ceiling. A function doing heavy math every single frame is costly even native. There's no obfuscator trick for that — profile it, cut the work, and fast-lane what remains.
Guide

Compatibility & limits

What changes once a script is protected. None of this affects normal game code — it matters only if you introspect your own functions or depend on exact runtime details.

The debug library

Protected functions are not native Luau closures, so the debug library sees the engine, not your code. debug.getinfo, debug.getlocal, debug.getupvalue and debug.traceback return engine internals or nothing useful for your own protected functions. Don't build logic on introspecting your obfuscated code. Debug writes are different: debug.setupvalue/debug.setconstant on protected code can crash the whole run, not just return nothing useful — if you must use them, mark the target with AEGIS_FASTLANE so it runs as native Luau.

Errors & line numbers

The original local, upvalue and function names are stripped and can't be recovered, so they won't appear in error messages. Line numbers and error text from protected code won't match the raw file — don't match on exact error strings. Errors still propagate and pcall still catches them.

Very large functions

Luau caps a single function at 200 locals/registers. A function already near that ceiling may be kept native rather than virtualized (never mis-compiled, never dropped). If you have one enormous function, splitting it into smaller ones protects it better and runs faster.

Executors & environment

  • Builds run on mainstream executors. The loader probes for what it needs and fails safe if something is missing — the hard requirements are game:HttpGet (or an HTTP-GET request primitive), loadstring, and bit32. Base64 and hashing (SHA-256/HMAC) ship in pure Luau, so executor-native crypt/base64 is used only as an optional fast-path, never required.
  • loadstring(string.dump(f)) on a protected function won't reproduce it — its bytecode is not your function's.
  • Tricks that depend on a native call-stack level (getfenv/setfenv by level) behave differently through the engine.
i
Mark it native if you must. If a function genuinely needs native semantics (your own debug introspection, exact stack levels), wrap it in AEGIS_FASTLANE — it stays renamed and string-encrypted but runs as real Luau.
Integration

Integration API

For Custom mode: your own in-game UI fetches and applies the key, then runs the loader. These are AegisLoader's licensing endpoints — public, called from your script, no API key.

i
Base URL https://api.aegisloader.com. Replace <id> with your project or key-system id, and compute hwid with RbxAnalyticsService:GetClientId(). Free, Paid and Ad builds don't need any of this — they ship a complete loader line.

The getgenv().Key convention

A Custom-mode loader arms by reading getgenv().Key. Set it to the player's key, then run the loader line — nothing else to wire:

getgenv().Key = key
loadstring(game:HttpGet("https://api.aegisloader.com/loader/<id>"))()

The key stays set for the session, so several Custom scripts sharing one key system each read the same getgenv().Key. You don't send the HWID to the loader — it computes its own.

Endpoints

EndpointReturns
GET /loader/<id>The build. loadstring + run it.
GET /api/validate/<id>?key=&hwid=Success { ok:true, mode, expires, activated, rt? }; failure { ok:false, reason }. Checks a key for a device — doesn't bind it.
GET /api/keysource/<id>{ ok, shared, key_system, mode } — whether a script uses its key system's key or its own.
GET /gate/<id>=<token>The ad-unlock page. Open it in a browser.
GET /gate/<id>=<token>/wait{} while pending, then { key } once earned.

Validate a key

Check a key before you run the loader. ok is the verdict. On success you also get mode, expires (unix seconds; 0 = never) and activated (true if the key is already bound to a device). reason appears only on failure — one of key required, invalid key, key banned, key expired, key locked to another device, key unavailable. If resume is enabled for the project, success also carries rt — a sealed resume credential your key UI should persist instead of the raw key.

local Http = game:GetService("HttpService")
local hwid = game:GetService("RbxAnalyticsService"):GetClientId()
local url = "https://api.aegisloader.com/api/validate/<id>?key="..key.."&hwid="..hwid

local ok, res = pcall(function() return Http:JSONDecode(game:HttpGet(url)) end)
if ok and res.ok then
    getgenv().Key = key
    loadstring(game:HttpGet("https://api.aegisloader.com/loader/<id>"))()
else
    warn("key rejected: "..tostring(res and res.reason))
end

Ad unlock

Generate a token, send the player to the gate page, and poll until they've earned a key. The /wait call long-polls — it returns {} on timeout, so just call it again.

local Http = game:GetService("HttpService")
local token = Http:GenerateGUID(false)
setclipboard("https://api.aegisloader.com/gate/<id>="..token)  -- player opens it

while true do
    task.wait(3)
    local r = Http:JSONDecode(game:HttpGet("https://api.aegisloader.com/gate/<id>="..token.."/wait"))
    if r.key then
        getgenv().Key = r.key
        loadstring(game:HttpGet("https://api.aegisloader.com/loader/<id>"))()
        break
    end
end

Key source

If your script is attached to a key system, /api/keysource tells you whether it validates against the key system (shared: true) or its own licenses (shared: false) — so one UI can drive several scripts and gate each against the right id.

!
Wrap network calls in pcall. HttpGet throws on a dropped request; never let a blip brick a paying player. Treat a failed validate as "try again," not "rejected."