Documentation

Everything you need to obfuscate Luau: how the obfuscator works, the dashboard around it, the HTTP API, and the in-script macros.

Introduction

MoonVeil is an obfuscator for Luau. You hand it a script and get back a hardened bundle that still behaves the same on the runtime you ship to, with the internals of your code dissolved.

Every build runs through three stages:

  • Optimize. Your script is lowered to MoonVeil's IR, where constants are folded and dead code is dropped before any protection runs. This is free speed for the obfuscated result.
  • Protect. The compile mode you pick rewrites the code, and macros can retune any function on top of the mode.
  • Bundle. Everything is emitted as one self-contained Luau file.

This page covers what the obfuscator can do, what each mode costs at runtime, and how the dashboard around it works. If you are new, start with the quick start below and read the mode sections before your first real build. Looking for a specific macro or endpoint? The macro reference and the API reference have the details.

Quick start

The fastest path from source to protected script runs through the Studio:

  1. Open the Studio and paste your script into the input pane, or press the upload button in the pane's bottom left corner to load a .lua or .luau file.
  2. Press Options and pick a compile mode. We recommend the Safe Env mode.
  3. Press Obfuscate. The output pane fills with the bundle, ready to copy to the clipboard or download as a file.

That is the whole loop. Everything else on this page is about tuning what happens in step two and how to run the same thing from your own tooling.

The MoonVeil Studio with a script in the input pane and an obfuscated bundle in the output pane
The Studio: your source on the left, the obfuscated bundle on the right.

Compile modes

The compile mode decides how the whole script is protected. It is the biggest knob you have, and it trades runtime cost against protection. Macros can override the mode for individual functions afterwards.

ModeWhat it doesRuntime costOutput sizePlans
CFFRewrites control flow into an opaque state machineLowSmallAll
VMCompiles functions to a private bytecode run by a bundled interpreterHighLargeAll
Safe EnvVirtualizes like VM, then locks the bundle to one environment behind anti-tamper checksHighestLargestBasic and above

CFF mode

Control flow flattening takes apart the shape of your functions. Branches, loops, and early returns stop being an outline a reader can follow and become states of a dispatcher: the body runs inside a loop that picks its next state at runtime. Your logic still executes as plain Luau operations, which is why this mode stays fast.

CFF is the default compile mode. Every CFF setting can also be set per function with MV_CFF or the --!mv:cff comment directive.

local step = MV_CFF(function()
  for i = 1, 10 do print(i) end
end, true, true, 10)

-- or --

--!mv:cff true false 10
local function factorial(n)
    if n <= 0 then return 1 end
    return n * factorial(n-1)
end

The settings, in the order the Options dialog shows them:

SettingWhat it does
Mangle control flow %Chance to insert fake branches into the machine. Higher means more junk code, a bigger bundle, and a slower function.
DecomposeBreaks the state machine apart further so states are computed instead of selected. Stronger flattening, more overhead.
Mangle nextMangles the successor and dispatch indices, so state transitions are not readable constants.
Mangle stringsMangles string constants in the function instead of leaving them in the constant table.
Mangle globalsMangles global accesses, so calls like game:GetService do not appear as readable constants.

In the Studio these sit under the CFF group of the Options dialog. On the API they are the options.cff object.

The Options dialog in CFF mode showing the CFF settings
CFF settings in the Options dialog. Every toggle maps to a field of options.cff on the API.

VM mode

The whole script is compiled and lifted to a selected VM. Protected functions are compiled to a bytecode instruction set generated for your build and shipped with the interpreter that runs it.

The cost is interpreter overhead, so virtualize the functions that need it instead of the whole script when you can. Two macros are built for exactly that:

  • MV_VM virtualizes a single function inside a build that is compiled in another mode.
  • MV_ENC_FUNC encrypts a function body so it only decrypts when a runtime key matches.
local verify = MV_VM(function(pw)
  return pw == "s3cret"
end)

local verify = MV_VM(function(pw)
  return pw == "s3cret"
end, "fox")

-- or --

--!mv:vm cat
local function factorial(n)
    if n <= 0 then return 1 end
    return n * factorial(n-1)
end

In VM and Safe Env modes you also pick which VM implementation to target. See VM implementations below.

The Options dialog in VM mode showing the VM implementation picker
VM mode adds the VM implementation picker to the Options dialog.

Safe Env mode

Safe Env is the strongest tier. It takes everything from VM mode and adds an environment lock. The bundle is bound to the environment it was built for and verifies it before anything decrypts: environment probes, integrity checks, and instrumentation aimed at debuggers and logging tools. A moved, unpacked, or instrumented build fails closed instead of running.

You choose the lock at build time:

LockLocks toUse it for
LuauAny generic Luau runtimeStandalone runtimes and embedders (luau CLI, lute, your own host)
RbxRoblox environments onlyScripts shipped inside Roblox experiences

MV_PRECHECK is only available in this mode. It runs a check function before the script decrypts and folds the return values into the decryption key, so a build can refuse to run when the environment answers wrong.

MV_PRECHECK(function()
  return 0x1234
end, 0x1234)

-- multiple keys are checked in order against each returned value --

MV_PRECHECK(function()
  return true, "rosiej", 255
end, true, "rosiej", 255)


MV_PRECHECK(function()
  -- globals are fine
  return game.PlaceId
end, 25522223443)

Expect a little extra startup cost while the checks run. Steady state performance sits in the same class as VM mode.

The Options dialog in Safe Env mode showing the environment lock picker
Safe Env adds the environment lock to the mode and VM pickers.

VM implementations

Both implementations come from the same VM family and generate a fresh instruction set per build, so two scripts obfuscated with identical settings do not share a VM. They differ in how the interpreter and bytecode are shaped:

VMTrade-off
CatSmallest and fastest virtualized output
FoxMore intense VM obfuscation and slowest setting

Output options

Two options sit outside the compile mode and shape the emitted bundle:

OptionWhat it does
Compress bundleCompresses the emitted bundle behind a loadstring decompressor. The shipped file is much smaller and decodes itself at startup. The target runtime needs a working loadstring. Paid plans only.
Env wrapperResolves globals through a sandboxed env wrapper instead of getfenv. Use it on runtimes without getfenv, like lute. It also isolates your globals: external scripts that are loadstring'd or required cannot read the obfuscated script's globals.

Macros

Macros are inline directives that steer the obfuscator function by function. They come as calls like MV_VM(function() ... end) or as --! comment directives placed above a function.

The ones that matter most while you are tuning a build:

MacroUse it to
MV_OMITKeep a hot function out of every obfuscation pass and emit it as plaintext
MV_INLINEInline a small function at its call sites instead of emitting a callable
MV_VMVirtualize a single function
MV_CFFFlatten a single function's control flow
MV_ENC_STREncrypt a string literal behind a runtime key
MV_PRECHECKGate decryption on environment values (Safe Env only)

The macro reference lists every macro with signatures and parameters, including the LPH_* aliases that let MoonVeil stand in for other obfuscators without a rewrite.

Studio

The Studio is the dashboard's front end for the pipeline. The three operations at the top pick what runs:

OperationWhat it runs
ObfuscateThe full protection pipeline with the options you set
PrettifyReformats a script into clean, readable source
MinifyRuns the optimizer (constant folding and dead code elimination) and shrinks the script

Below that sit your input script and the output of the last run, split by a draggable divider on desktop and switched with tabs on mobile. The counter above the input tracks your script against your plan's size limit (and against the 5,000,000 character hard ceiling when a run would overflow into a credit). The upload button loads a file from disk; the output pane has copy and download buttons.

A few behaviors worth knowing:

  • Your input and last output are saved locally in the browser, so a reload or an accidental tab close does not lose your work.
  • After a run the button stays disabled until the input changes, which keeps you from spending a request on the same source twice.
  • When a run needs a plan-locked option, exceeds your size limit, or your monthly quota is spent, it can still go through if you turn on Overflow to credits in the Options dialog. The run button shows a costs a credit tag when that is about to happen.

Options dialog

The Options dialog is the Studio's view of the same options object the API takes. Group by group:

  • Mode: CFF, VM, or Safe Env.
  • VM: Cat or Fox. Shown for VM and Safe Env modes.
  • Environment Lock: Luau or Rbx. Shown for Safe Env mode.
  • CFF: the mangling settings. Shown for CFF mode.
  • Output: compress bundle, the env wrapper, and Overflow to credits with your current credit balance.

Options your plan does not include show a padlock. Turn on Overflow to credits and every option unlocks: the ones that would spend a credit say so in their tooltip, and the run button tells you before you spend one.

Dashboard overview

The Overview tab is the account's home base:

  • Plan: your plan name, script size limit, and monthly request quota.
  • Credits: your balance with a shortcut to buy more.
  • This month: a usage donut, requests used against quota, and when the quota resets.
  • All time: how many scripts the account has obfuscated.
  • Activity: recent obfuscations, with a marker on the runs that spent a credit.
The dashboard overview tab showing plan, credits, monthly usage, and recent activity
The Overview tab: plan, credits, this month's quota, and recent activity.

API keys

API keys authenticate your own tooling against the API. Generate as many as you need and name them after where they run (CI, laptop, a deploy hook), then send each one as a bearer token:

const KEY = process.env.MOONVEIL_KEY!;

async function moonveil(op: string, body: object) {
  const res = await fetch(`https://moonveil.cc/api/v2/${op}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error((await res.json()).error);
  return res.text();
}

The raw key is shown once at creation, so copy it when you see it.

The API keys tab listing generated keys with a form to generate a new one
API keys: generate, name, revoke. The raw token is only shown once.

Billing and credits

Billing handles plan changes and credits:

  • Plan tiers: Basic and Commercial upgrade through Stripe and apply as soon as checkout finishes. Manage billing opens the Stripe portal for cards and invoices.
  • Credits: one-time purchases through Stripe. Take the presets (1, 5, 10, 25, 50) or type an amount up to 1000. A processing fee applies to small orders, and the summary shows it before you pay.
  • Credit history: every credit transaction with its timestamp, including which obfuscations spent one.

Credits are what let a run leave your plan. With Overflow to credits on in the Studio (or useCredit on the API) a request spends a credit only when it does not fit the plan: a plan-locked option, a script over your size limit, or a spent monthly quota. Credit runs unlock every feature and accept scripts up to 5,000,000 characters, or your plan's limit when that is higher.

The billing tab showing plan tiers, credit balance, and the buy credits form
Billing: plan tiers on top, credit purchases and history below.

Settings

Settings covers the account itself: your profile (email and display name), password changes for email accounts, and connections. Discord can be linked and unlinked here, which is also what unlocks Discord login and support.

The settings tab with profile, password, and connection sections
Settings: profile, password, and connected login providers.

Plans

What each plan unlocks:

PlanPriceRequestsScript sizeCompile modesVM implementations
SkidFree10 / month200,000 charsCFF, VMCat
Basic$7 / month500 / month5,000,000 charsCFF, VM, Safe EnvCat, Fox
Commercial$30 / month1500 / month10,000,000 charsCFF, VM, Safe EnvCat, Fox

Every plan includes the Studio, the API, and the full macro set. Prettify and minify are free everywhere and only rate limited. Bundle compression is included on paid plans, and credits work on every plan.

API

Everything the Studio does is available over HTTP so obfuscation can sit inside your build:

const KEY = process.env.MOONVEIL_KEY!;

async function moonveil(op: string, body: object) {
  const res = await fetch(`https://moonveil.cc/api/v2/${op}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error((await res.json()).error);
  return res.text();
}
const obfuscated = await moonveil("obf", {
  script: 'print"meow"',
  options: { compileType: "safeEnv", vmType: "fox", safeEnvLock: "rbx", cff: { decompose: true, mangleStrings: true }, compress: true, envWrapper: true },
});

The API reference documents every endpoint and it's expected usage. Obfuscation requests spend your monthly quota unless they pay with a credit; the obfuscate endpoint explains the details.