Make the source smaller
In the minifier, use directives to fold expressions, control names, or request localization.
The catboy.loan field guide
A practical guide to the macros and attributes you can use when minifying or obfuscating your Luau.
-- A small hint for the obfuscator
local key = 2468
local message = LPH_ENCSTR(
"hello there", 2468, key
)
print(message)
01 / Start here
Macros are instructions for the transformer, not built-in Lua functions. Choose the tool that matches the directives in your source.
In the minifier, use directives to fold expressions, control names, or request localization.
Use the obfuscator for VM controls, protected literals, prechecks, and other obfuscation macros.
Weak, Medium, and Strong all use a VM and require Roblox. For non-VM output, use the separate minifier.
Same prefix, different job. Minifier-only directives such as CTB_CONST are not expanded by obfuscation. CTB_ATTRIBUTES also accepts different options in each mode. Don't mix the two families in one run.
02 / Before you begin
Write the call directly in your source; the selected transformer replaces it while building the output.
For obfuscation, CTB_* is the primary macro family; LPH_* is a compatibility alias. For example, CTB_ENCSTR and LPH_ENCSTR select the same obfuscation macro. Unknown LPH-prefixed names are reserved and rejected.
If you also run the original source without obfuscating it, you can define a simple global fallback in a guard. The obfuscator removes that guarded fallback.
if not CTB_OBFUSCATED then
CTB_ENCSTR = function(value) return value end
end
print(CTB_ENCSTR("hello"))
Search the reference below, or browse by section.
03 / Obfuscation
These names are for the obfuscation pipeline. Every CTB_ macro below also accepts an LPH_ compatibility alias.
Values & predicates
Evaluates a numeric expression once and returns that snapshot. In the serialized VM, the value also helps unlock protected continuation instructions; the compact VM backends and Minify compare it directly. A mismatch fails the integrity check on every tier.
CTB_ASSUME(value, expectedNumber)local place = CTB_ASSUME(game.PlaceId, 123456)
print(place) -- replace 123456 with your actual place IDLimits: the expected value must be a finite numeric literal in the exact-number range. Protection extends only through VM blocks where the snapshot is provably available, not the whole function. With the non-VM minifier, use an ordinary equality check instead; inside VM(NONE) it is rejected.
Produces an arithmetic predicate that resolves to the boolean you request. Use it in a condition when you want the generated expression to be less direct than a literal.
CTB_OPAQUE(true | false)if CTB_OPAQUE(true) then
print("this branch runs")
endCTB_OBFUSCATED becomes true during obfuscation. CTB_LINE becomes the original source line number. Neither takes parentheses.
if CTB_OBFUSCATED then
print("built from line", CTB_LINE)
endObvious literal branches and short-circuits are folded, but this isn't a general dead-code analysis pass.
Protected literals
Protects a quoted or long-bracket string literal with ChaCha20. The short form generates an embedded key; the three-argument form uses the value supplied at runtime to unlock it.
CTB_ENCSTR(text [, key, expression])Aliases: CTB_STRENC, LPH_ENCSTR, LPH_STRENC
local runtimeValue = 2468
local message = CTB_ENCSTR("hello", 2468, runtimeValue)
print(message)Key forms: an exact integer, a 64-character hexadecimal string, or the same hex string wrapped in braces for a 32-byte table. The runtime expression must supply the matching number, hex string, or byte table. The ciphertext is not authenticated; embedded or client-visible keys are recoverable.
Uses the same literal and optional key forms as CTB_ENCSTR, but returns the decoded bytes as a Luau buffer.
CTB_ENCBUF(text [, key, expression])Aliases: CTB_BUFENC, LPH_ENCBUF, LPH_BUFENC
local bytes = CTB_ENCBUF("hello")
print(buffer.tostring(bytes))Requires a host with Luau's buffer library.
Stores a numeric literal as protected text and converts it back at runtime. Optionally bind it to up to eight expected integer values.
CTB_ENCNUM(number [, { [expression] = expectedInteger }])Aliases: CTB_NUMENC, LPH_ENCNUM, LPH_NUMENC
local slot = 21
local amount = CTB_ENCNUM(100, { [slot] = 21 })
print(amount)Expected binding values must be exact integers. The numeric key mixer is not a cryptographic key-derivation function.
Execution & control
Moves a check into the loader and mixes its result into the VM and vault keys. The check must return the expected integer or integer array before the protected payload can be decoded.
CTB_PRECHECK(function() ... end, expected)CTB_PRECHECK(function()
return game.PlaceId
end, 123456) -- use your actual place ID
print("check passed")Limits: use an anonymous function with no parameters or captured outer locals. Expected values must be exact integers; arrays contain 1–64 values. Requires a VM preset and cannot be used with file-level VM(NONE).
Damages the active VM context and raises an error. Catching that error does not restore the wiped VM state. It can be used as a statement or an expression.
CTB_CRASH()if not allowed then
CTB_CRASH()
endWithout a VM, including file-level VM(NONE), it raises an error instead. Do not use a client-side crash as authorization.
Creates an indexed container with a declared length. The optional second argument chooses the starting index: 1 by default, or 0.
CTB_STACKALLOC(size [, 0 | 1])local slots = CTB_STACKALLOC(3, 0)
slots[0] = "first"
local packed = slots:pack()
print(#slots, packed.n)
slots:clear()Current implementation: this is a table-backed compatibility helper, not actual stack/register allocation. It supports indexing, #, pack, unpack, and clear; it does not enforce non-escaping use. Size must be an integer from 0 to 100,000.
Rewrites a supported arithmetic expression with mixed bitwise and arithmetic operations, while keeping a fallback for values or environments where the rewrite isn't safe.
CTB_REWRITE(expression [, options])local a, b = 8, 4
local value = CTB_REWRITE(a + b, {
preset = "strong", budget = "medium"
})
print(value)The supported subset includes numeric literals, identifiers, +, -, *, and selected bit32 calls. Options are fast, standard, strong, or extreme presets; small, medium, or large budgets; and an optional identifier context. At most eight distinct identifiers are captured. This is not a complete MBA rewriter.
04 / Obfuscation
Place CTB_ATTRIBUTES(...) as the first statement of a function or the file. Multiple supported attributes can share one call. LPH_ATTRIBUTES is also accepted in obfuscation mode.
local function calculate(value)
CTB_ATTRIBUTES(VM(ONYX), PRESET(SECURE))
return value * 2
endAvailable attributes
NONE emits native Lua instead of VM instructions. OPAL and ONYX select distinct custom opcode spaces; ONYX also has a separate operand/mode layout. They are not the proprietary Luraph VMs.
A native function can access mutable locals captured from the VM, and native descendants inherit NONE. Switching a native descendant back to a VM is not supported. File-level NONE cannot be combined with a runtime key or CTB_PRECHECK.
Chooses the VM's opcode-variant count: two for FAST, three for BALANCED, and four for SECURE. Descendant VM functions inherit the setting unless they override it.
This function attribute is separate from the website's Weak, Medium, and Strong pipeline presets. It cannot be applied to a native VM(NONE) function.
Wraps the function in an environment-backed callable with no lexical captures on the wrapper itself. The original function can still capture locals.
Requires getfenv and setfenv. File-level use and nested use inside native functions are not supported. Use a VM preset for this attribute.
On native code, rewrites method-call syntax into ordinary member lookup and calls. The receiver is evaluated once, and method lookup happens before arguments are evaluated. Descendants inherit the transform; TRANSFORM() clears it.
The rewrite introduces closures and stack frames and can change Roblox-specific __namecall behavior. Other transformation options are not implemented.
Not implemented: OPTIMIZE, ERROR_HANDLING, ENCRYPT, INLINE, UNROLL, UNSAFE_ENVIRONMENT, and other TRANSFORM modes are rejected rather than silently ignored. Loop attributes are not supported. The separate minifier does not offer VM or function-preset controls.
Compatibility aliases: LPH_ASSUME, LPH_OPAQUE, LPH_ENCSTR, LPH_STRENC, LPH_ENCBUF, LPH_BUFENC, LPH_ENCNUM, LPH_NUMENC, LPH_PRECHECK, LPH_CRASH, LPH_STACKALLOC, LPH_REWRITE, LPH_OBFUSCATED, LPH_LINE, LPH_ATTRIBUTES.
05 / Minification
These directives belong to the regular minifier. Open the minifier to use them. They are expanded during transformation and are not runtime functions.
Expressions & names
Asks the minifier to fold the supplied expression where it can safely evaluate a constant.
CTB_CONST(expression)print(CTB_CONST(6 * 7))Replaces a string literal with encoded bytes and a small decoder that reconstructs it while the program runs.
CTB_ENCODE_STRING("literal")Aliases: CTB_OBFUSCATE, CTB_HIDE_CONSTANT
print(CTB_ENCODE_STRING("hello"))Not encryption: this byte-shift encoding is easy to reverse. It only discourages casual text searching and should not be used for secrets. It requires a literal string.
CTB_RENAME(identifier, "name") sets a local or global identifier's spelling. CTB_KEEP_NAME(local) keeps a local's existing name during renaming.
local count = 42
local label = "wins"
print(CTB_RENAME(count, "score"), CTB_KEEP_NAME(label))Choose a valid, non-conflicting Luau identifier when using CTB_RENAME. Normal optimization may still remove an unused value.
Each unwraps its expression. CTB_LOCALIZE requests the global-localization pass for the file; CTB_NO_LOCALIZE disables that pass. If both are present, disabling wins.
local say = CTB_NO_LOCALIZE(print)
say("hello")Unwraps an expression so it can be considered by the minifier's ordinary safe passes. It does not guarantee that a function or expression will be inlined.
CTB_INLINE(expression)local value = CTB_INLINE(20 + 22)
print(value)In minifier mode, put this as the first statement inside a function. When any function has these attributes, the current minifier skips ordinary optimization passes for the whole file to preserve its structure. NATIVE also emits Luau's @native annotation.
local function add(a, b)
CTB_ATTRIBUTES(NO_DCE, NO_INLINE)
return a + b
end
print(add(2, 3))Accepted names:
NO_DCENO_INLINENO_RENAMENO_MINIFYNO_FOLDNO_PACK_STRINGSNO_LOCALIZEKEEP_LOCALSPUREKEEPNATIVEOPTIMIZEDEBUGThese names do not each apply a separate function-level optimization rule yet. This is distinct from obfuscation-mode LPH_ATTRIBUTES / CTB_ATTRIBUTES.
06 / Advanced
A VM build can require an external string at execution time. The key isn't embedded in the generated output.
In the obfuscator, choose Weak, Medium, or Strong, and enter your optional key in the Runtime key field before building. Leave the field empty for a build without a key. The exact key is needed each time the output runs, so keep it somewhere safe.
The field is available for all three offered presets, not for file-level VM(NONE). The browser sends the key to the service with your source and does not save it to browser storage. Clearing the workspace or closing the tab removes it from the form.
You can also supply runtimeKey through the API or library, or use --runtime-key with the CLI.
At runtime, provide the same value as the first chunk argument, or set _CTB_KEY in the script environment before loading the output. The first-argument form preserves subsequent chunk arguments. The environment value is scrubbed when the host permits it.
-- On hosts where loadstring is available:
local chunk = assert(loadstring(obfuscatedSource))
chunk(runtimeKey, "other argument")Alternatively, on a host with a writable script environment, set the key before running the generated chunk:
getfenv()._CTB_KEY = runtimeKey
-- Run the generated chunk in this environment.Missing or incorrect keys fail with VM integrity check failed. When using the hosted obfuscator or API, both source and the supplied key are sent to the service. Command-line arguments may also be visible to other local processes.
07 / Read this too
Obfuscation raises the effort needed to inspect code. It does not turn a client into a trusted environment.
The setmetatable-wrapped public dictionary exposes one executor factory plus real stateless arithmetic continuations and numeric native-function aliases. Inverse operand programs are shuffled across multi-return state fragments, not padded with unused methods. Fragments receive numeric inputs and a floor primitive, never the private owner, vault, key ring or receipts. A bound executor snapshots those safe components and creates fresh private state on every invocation. CTB framing is retained: there is no fake vendor marker or banner, no proprietary ISA claim and no guaranteed detector label. Compression has its own outer wrapper. Loaders, the crypto broker, page/constant decoders and post-VM resolvers become private lexical bindings, not callable table members. Returning the object no longer provides an enumeration oracle. Per-frame, one-use identity receipts bind prototype loads, pages, PCs and instruction-authorized constant reads. A page is authenticated into a private byte buffer; a sequential cursor yields one record, erases consumed bytes and wipes the remaining buffer on normal exit. Replay, wrong-PC/frame requests and closed-frame reuse poison the invocation and wipe active page buffers. Fresh captured identities prevent closure caching from reviving spent receipts. Ordinary user errors rely on host garbage collection for abandoned frame storage, not synchronous zeroing. Editing source, inspecting upvalues or instrumenting an owned host can still recover plaintext; this is extraction friction, not secrecy or attestation.
Every prototype has disjoint opcode IDs and block remapping. Per-handler logical/wire permutations, signs and generated reversible programs compose four nonlinear shear families, including cyclic dependencies. Builds choose flat, row-bucketed or column-bucketed dispatch; each page entry, including revisits, refreshes private handler coordinates and samples a bucket width. Threaded instruction whitening depends on the preceding record. Register/constant-specialized and multi-effect instructions include paired clears/stores, table initialization and arithmetic/unary/move-plus-return; 181 specialized/fused semantic forms have direct tests. These are reversible obfuscation transforms, not cryptographic secrets. They increase size and CPU cost; benchmark actual supported Luau workloads rather than assuming a particular reverse-engineering time.
ConstantArray and EncryptStrings generate nonlinear multi-round tokens for normal-sized pools; very large pools retain untruncated fallback indexing. Authenticated post-VM vaults keep opaque store closures rather than canonical record arrays. Selected reads move records to fresh sparse addresses, change field coordinates and remask nonce/tag/ciphertext buffers, erasing retired buffers and routes. Standalone/pre-VM resolvers also randomize state-field identities and runtime offsets, remask selected ciphertext with per-record masks, refresh constant type/integer masks and poison rejected token/string-check paths. Untouched records retain their own masks. The underlying AEAD ciphertext and keys do not rotate: this is representation camouflage, not new encryption. Tests check actual structural and storage changes, not merely unequal output blobs. These changes add CPU/allocation cost and cannot make a reusable deobfuscator impossible against a runtime-owning analyst.
Serialized VM instruction pages use ChaCha20-Poly1305, as do VM-pool values and post-VM string/constant records. HKDF-SHA-256 derives an external-key root; HChaCha20 separates it into three instruction keys, three string keys, and three constant keys, then derives a leaf key per chunk. The Lua implementation has not been independently audited. The root directory (including stored edges), prototype metadata and lazy child bodies also have AEAD protection. The outer envelope, in-memory edge checks and standalone/pre-VM legacy codecs still use weaker custom protection; the Lua loader source and host are not authenticated. Use a high-entropy key; it can be observed in a controlled client, and a key embedded in a self-contained build provides no offline secrecy. VM output fails if both debug.info and debug.getinfo are unavailable. This is tamper detection, not protection against offline extraction or a controlled client.
The shared broker stores keys in re-shared private fragments with invocation-varying routes and access order. One-use decode leases, reentrancy checks, share-integrity checks and authentication failures burn the ring and poison the invocation. Restoring ciphertext is not recovery; start a fresh invocation. Runtime diversity does not consume or reseed math.random, but its masks are not cryptographic entropy. Keys for a fixed ciphertext stay fixed, and the nine lane keys derive from one root. A client owner can still instrument plaintext or request fresh leases. Explicit nonserialized VM mode and standalone output do not receive this broker. Authentication adds CPU and output-size overhead; test real workloads.
Compressed obfuscator builds need a host that permits loadstring or load. For VM builds, the loader sends only the executor to it and supplies the VM payload separately. A controlled runtime can still inspect that payload, and the loader can make small outputs larger.
Bytecode and vault readers reject oversized or non-canonical integer encodings, truncated records, invalid counts, and invalid instruction-page bounds. Cryptographic inputs require correctly sized integer-octet arrays. This validation does not authenticate all metadata or replace host isolation and resource limits.
VM presets require Roblox and include environment and anti-hook checks. A controlled client can imitate or patch those checks. The VM runs user code against host globals: it is an obfuscator, not a security sandbox. Do not use it to run untrusted programs in a privileged host. Test output in your actual target environment.
This is an independent, partial compatibility layer, not full macro or attribute parity with another obfuscator. Unsupported features are called out above rather than advertised as working.
Do not ship credentials in client code or use an obfuscated client to authorize important actions. Enforce trust-sensitive decisions on a server.
Keep a copy of your source and test the output where it will run.