> ## Documentation Index
> Fetch the complete documentation index at: https://starcryptdocs.starsolenterprise.com/lua-docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Variable and Symbol Renaming in Lua/Luau

> Replace every human-readable identifier with meaningless names using systematic renaming strategies that preserve lexical scoping, upvalue chains, and table key consistency.

Variable and symbol renaming is the foundational obfuscation technique for Lua and Luau. It strips every human-readable name from your source code and replaces it with meaningless identifiers that carry no semantic value. When applied correctly, renaming makes reverse engineering significantly harder because an attacker can no longer infer purpose from names like `calculateDamage` or `playerInventory`. This guide covers what renaming can and cannot obscure, demonstrates multiple naming strategies, and explains the critical scope analysis rules you must follow to avoid breaking your script.

## What Renaming Obfuscates

A thorough rename pass targets every identifier that you control:

* **Local variables**: `local score = 0` becomes `local _a = 0`
* **Function names**: `function addPoints(p)` becomes `function _b(_c)`
* **Parameters**: including `self` in method definitions
* **Upvalues**: local variables captured by nested functions
* **Table keys**: string keys in tables you define and access within the same script
* **Module names**: `local Utils = require("Utils")` becomes `local _d = require("Utils")` (the require string itself is a separate concern handled by string encoding)

## What Renaming Does NOT Obscure

Renaming is powerful but has hard limits. You must not rename identifiers that belong to the runtime environment:

* **Standard library names**: `print`, `table`, `math`, `string`, `pcall`, `error`, `pairs`, `ipairs`, `tonumber`, `tostring`, `type`, `assert`, `collectgarbage`, `getmetatable`, `setmetatable`, `rawget`, `rawset`, `rawequal`, `load`, `loadfile`, `dofile`
* **Globals injected by the host**: in Roblox, names like `game`, `workspace`, `script`, `Instance`, `Vector3`, `CFrame`, `Enum` are provided by the engine and must remain intact
* **String literals inside require paths**: `require("ReplicatedStorage.Module")` must keep the exact path unless you also rewrite the module structure
* **Metamethod names**: `__index`, `__newindex`, `__add`, `__tostring`, and others are looked up by exact string match at runtime

> [!WARNING]
>
> Renaming a standard library name or a Roblox global will cause runtime errors. Your obfuscator must maintain a whitelist of protected names.

## Before and After Example

Here is a readable game score tracker and its fully renamed equivalent. Both scripts behave identically at runtime.

```lua Original Readable Code theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local ScoreTracker = {}
ScoreTracker.__index = ScoreTracker

function ScoreTracker.new(playerName)
    local self = setmetatable({}, ScoreTracker)
    self.playerName = playerName
    self.points = 0
    self.history = {}
    return self
end

function ScoreTracker:addPoints(amount)
    self.points = self.points + amount
    table.insert(self.history, amount)
end

function ScoreTracker:removePoints(amount)
    self.points = math.max(0, self.points - amount)
    table.insert(self.history, -amount)
end

function ScoreTracker:resetGame()
    self.points = 0
    self.history = {}
end

function ScoreTracker:getLeader()
    return self.playerName
end

return ScoreTracker
```

```lua Fully Renamed Code theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _a = {}
_a.__index = _a

function _a._b(_c)
    local _d = setmetatable({}, _a)
    _d._e = _c
    _d._f = 0
    _d._g = {}
    return _d
end

function _a:_h(_i)
    _d._f = _d._f + _i
    table.insert(_d._g, _i)
end

function _a:_j(_i)
    _d._f = math.max(0, _d._f - _i)
    table.insert(_d._g, -_i)
end

function _a:_k()
    _d._f = 0
    _d._g = {}
end

function _a:_l()
    return _d._e
end

return _a
```

Notice that standard names like `setmetatable`, `table.insert`, and `math.max` remain untouched. The renamed version is functionally identical but offers no semantic clues.

## Naming Strategies

The choice of replacement names affects both readability for an attacker and the size of your output. Here are the most common strategies from simplest to most aggressive.

### Sequential Integers

The simplest approach assigns `_0`, `_1`, `_2`, `_3` in declaration order.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _0 = 10
local _1 = "hello"
local function _2(_3)
    return _3 + _0
end
```

This is easy to implement and produces small output, but an attacker can quickly spot patterns. Sequential names also make it obvious which variables were declared first.

### Hex-Style Names

Using hexadecimal prefixes adds visual noise without increasing size much.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _0x1A = 10
local _0x2B = "hello"
local function _0x3C(_0x4D)
    return _0x4D + _0x1A
end
```

The `_0x` prefix tricks the eye into reading numbers rather than identifiers, slowing down manual analysis slightly.

### Unicode Homoglyphs

Lua 5.3 and Luau support UTF-8 identifiers. You can substitute visually similar characters from other alphabets.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local О = 10       -- Cyrillic Capital Letter O (U+041E)
local а = "hello"  -- Cyrillic Small Letter A (U+0430)
local function ƒ(x)
    return x + О
end
```

Other useful homoglyphs include:

* `Е` (Cyrillic Ye, U+0415) vs `E` (Latin E, U+0045)
* `р` (Cyrillic Er, U+0440) vs `p` (Latin P, U+0070)
* `ѕ` (Cyrillic Dze, U+0455) vs `s` (Latin S, U+0073)

You can also embed zero-width spaces (U+200B) or zero-width non-joiners (U+200C) inside identifiers. These characters are invisible in most editors but are significant to the Lua lexer.

> [!WARNING]
>
> Homoglyphs can break in environments with strict font rendering or when copied through channels that strip non-ASCII characters. Test your output in the target runtime before deploying.

### Keyword Lookalikes

Names that resemble reserved words create visual confusion without being syntax errors.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _if = 10
local _end = "hello"
local _do = true
local function _while(_then)
    return _then + _if
end
```

Because `_if` is not the literal token `if`, the parser accepts it. An attacker scanning quickly may misread these as control flow keywords.

### Random Alphanumeric Strings

The strongest naming strategy generates 8-16 character random strings.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local kx9mP2vL = 10
local Qr4Tw8zN = "hello"
local function bJ3hF7dC(aX5kL2pR)
    return aX5kL2pR + kx9mP2vL
end
```

Random names have no pattern to exploit. The tradeoff is larger script size and slightly slower parsing in some interpreters.

### Collision Attacks

You can intentionally reuse the same short name at different lexical scopes to confuse readers. A human analyst may assume `_a` in two places refers to the same entity.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _a = 10

local function outer()
    local _a = "shadow"
    local function inner()
        local _a = true
        return _a
    end
    return inner()
end
```

Each `_a` is a distinct local bound to its own scope. This is legal Lua but mentally taxing to trace.

## Scope Analysis

Renaming must be scope-aware. Lua's lexical scoping means that a local variable is visible from its declaration point to the end of the block, including inside nested functions that capture it as an upvalue. A naive global search-and-replace will break your script.

### Lexical Scoping in Lua

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local x = 10          -- outer x

local function f()
    print(x)          -- prints 10 (upvalue)
    local x = 20      -- inner x shadows outer x
    print(x)          -- prints 20
end

f()
print(x)              -- prints 10
```

The two `x` variables are completely separate storage locations. A renamer must track which `x` each reference binds to.

### Broken Rename Example

Here is what happens when a renamer ignores scope:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Original
local playerCount = 5

local function getCount()
    return playerCount
end

local function setCount(n)
    local playerCount = n
    return playerCount
end
```

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- INCORRECTLY RENAMED (broken)
local _a = 5

local function _b()
    return _a
end

local function _c(_d)
    local _a = _d      -- ERROR: shadows _a but getCount still references outer _a
    return _a
end
```

The incorrect renamer collapsed both `playerCount` locals into `_a`. This changes behavior: `getCount` now captures the inner `_a` if the closure is created after `setCount` runs, or worse, it may reference the wrong slot depending on bytecode layout.

### Correct Scope-Aware Rename

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- CORRECTLY RENAMED
local _a = 5

local function _b()
    return _a         -- binds to outer _a
end

local function _c(_d)
    local _e = _d     -- separate name for inner local
    return _e
end
```

A correct renamer maintains a stack of scope tables. Each scope maps original names to generated names. When resolving a reference, the renamer walks from innermost scope to outermost, stopping at the first match.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Pseudocode for scope-aware renaming
scopes = [{}]  -- stack with global scope at bottom

function enterScope()
    table.insert(scopes, {})
end

function leaveScope()
    table.remove(scopes)
end

function declare(name, generated)
    scopes[#scopes][name] = generated
end

function resolve(name)
    for i = #scopes, 1, -1 do
        if scopes[i][name] then
            return scopes[i][name]
        end
    end
    return name  -- global, leave unchanged
end
```

### Upvalue Chains

When a nested function references a local from an outer function, Lua creates an upvalue. Renaming must propagate the new name to both the declaration site and every capture site.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Original
local function makeCounter()
    local count = 0
    return function()
        count = count + 1
        return count
    end
end
```

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Correctly renamed
local function _a()
    local _b = 0
    return function()
        _b = _b + 1
        return _b
    end
end
```

Both occurrences of `count` become `_b`. If the renamer treated the inner function as a separate scope without upvalue linking, it might generate `_c` for the inner reference, breaking the closure.

## Parameter Renaming

Function parameters are local variables that exist only inside the function body. Each parameter gets an independent generated name.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Original
local function calculate(a, b, operation)
    if operation == "add" then
        return a + b
    elseif operation == "mul" then
        return a * b
    end
end
```

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Renamed
local function _a(_b, _c, _d)
    if _d == "add" then
        return _b + _c
    elseif _d == "mul" then
        return _b * _c
    end
end
```

### Vararg Limitation

The vararg parameter `...` cannot be renamed. It is a fixed token in the Lua grammar. You can still obscure its usage by immediately binding it to a local:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function _a(...)
    local _b = {...}
    -- work with _b instead of ...
end
```

### Method Parameter Self

In Lua method syntax `function obj:method(a, b)`, the colon implicitly adds `self` as the first parameter. You can safely rename `self` to any valid identifier, but you must preserve the colon syntax or convert it to dot syntax explicitly.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Original
function Player:getHealth()
    return self.health
end
```

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Safe rename (preserving colon)
function _a:_b()
    return _c.health
end
```

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Also safe (converting to dot)
function _a._b(_c)
    return _c.health
end
```

> [!WARNING]
>
> Do not rename `self` inside a colon-defined method while leaving other references to `self` unchanged. Either rename all occurrences consistently or leave `self` untouched.

## Table Key Renaming

Tables with string keys are ubiquitous in Lua. Renaming these keys requires updating both the definition site and every access site.

### Dot Notation vs Bracket Notation

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Original
local config = {
    maxPlayers = 20,
    gameMode = "capture",
    timeLimit = 300
}

local max = config.maxPlayers
local mode = config["gameMode"]
```

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Renamed
local _a = {
    _b = 20,
    _c = "capture",
    _d = 300
}

local _e = _a._b
local _f = _a["_c"]
```

Both `config.maxPlayers` and `config["maxPlayers""]` must be rewritten to use the new key name. A static analysis pass must track which table a key belongs to.

### Full Table Key Example

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Original
local API = {}
API.baseUrl = "https://api.example.com"
API.version = "v2"
API.timeout = 30

function API:request(endpoint)
    local url = self.baseUrl .. "/" .. self.version .. endpoint
    print("Requesting", url, "with timeout", self.timeout)
    return url
end

return API
```

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Renamed
local _a = {}
_a._b = "https://api.example.com"
_a._c = "v2"
_a._d = 30

function _a:_e(_f)
    local _g = _c._b .. "/" .. _c._c .. _f
    print("Requesting", _g, "with timeout", _c._d)
    return _g
end

return _a
```

> [!WARNING]
>
> Keys passed to external APIs must never be renamed. For example, `Instance:FindFirstChild("Humanoid")` must keep the string `"Humanoid"` because the Roblox engine looks up that exact name. Only rename keys for tables that are entirely internal to your script.

## Upvalue and Closure Renaming

Closures are functions that capture variables from their enclosing scope. Renaming a captured variable requires renaming it at the point of declaration and at every point of use inside the closure.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Original
local function createMultiplier(factor)
    local multiplier = factor
    return function(value)
        return value * multiplier
    end
end

local double = createMultiplier(2)
print(double(5))  -- 10
```

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Renamed
local function _a(_b)
    local _c = _b
    return function(_d)
        return _d * _c
    end
end

local _e = _a(2)
print(_e(5))  -- 10
```

The upvalue `_c` is declared in `createMultiplier` and referenced inside the anonymous returned function. Both sites use `_c`.

## Luau-Specific Notes

Luau extends Lua with a static type system. Type annotations are not present at runtime, but they appear in source code and may leak information.

### Strip Type Annotations Before Renaming

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Original Luau
local x: number = 5
local y: string = "hello"
local function add(a: number, b: number): number
    return a + b
end
```

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- After stripping types, then renaming
local _a = 5
local _b = "hello"
local function _c(_d, _e)
    return _d + _e
end
```

Type annotations like `: number` reveal intent. Strip them before the rename pass, or the attacker sees `playerHealth: number` even after renaming the variable.

### Exported Module Members

In Roblox, a module script typically returns a table of functions that other scripts require. The keys of that returned table are the public API.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Original module
local MyModule = {}

function MyModule.doSomething()
    print("done")
end

return MyModule
```

If you rename `MyModule.doSomething` to `_a._b`, any consumer script that calls `require(script.Parent.MyModule).doSomething()` will break. You have three options:

1. **Do not rename exported keys**: keep `doSomething` intact
2. **Obfuscate the consumer too**: rename the access site in every script that requires the module
3. **Use an indirection table**: return a table with original keys that map to renamed internal functions

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Option 3: indirection table
local _a = {}

function _a._b()
    print("done")
end

return {
    doSomething = _a._b  -- external name preserved, internal name obfuscated
}
```

> **Can I rename \_ENV?**
>
> `_ENV` is a special upvalue introduced in Lua 5.2 that holds the global environment table. In Lua 5.1, the equivalent concept is the implicit global table accessed by unqualified names. You should not rename `_ENV` for several reasons.
>
> First, `_ENV` is looked up by the compiler as a reserved upvalue name. While it is technically a regular local in Lua 5.2+, the compiler treats it specially during chunk compilation. Replacing it with a generated name may confuse the bytecode loader or produce unexpected global resolution behavior.
>
> Second, many scripts rely on `_ENV` being accessible for sandboxing patterns:
>
> ```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
> local _ENV = {}
> setfenv(1, _ENV)  -- Lua 5.1 style
> ```
>
> If you rename `_ENV` here, the sandboxing intent becomes harder to read, but the runtime behavior depends on the exact Lua version. In Luau, `_ENV` does not exist in the same form; globals are resolved differently. The safest rule is: leave `_ENV` untouched unless you are writing a custom bytecode obfuscator that also rewrites the global table reference mechanism.

> **What about metatables and \_\_index?**
>
> Metamethod names like `__index`, `__newindex`, `__add`, `__sub`, `__mul`, `__div`, `__mod`, `__pow`, `__unm`, `__concat`, `__len`, `__eq`, `__lt`, `__le`, `__tostring`, `__gc`, `__mode`, `__call`, and `__metatable` are looked up by exact string match at runtime. If you rename `__index` to `_a`, the metatable mechanism will not find it and your object-oriented code will break.
>
> There is one safe exception: if you implement `__index` as a function table rather than a string key, you can rename the function itself because the string key `__index` remains intact:
>
> ```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
> -- Safe: rename the function, not the metamethod key
> local _a = {}
> _a.__index = _a  -- __index must stay __index
>
> function _a:getValue()
>     return self._b
> end
>
> setmetatable(someTable, _a)
> ```
>
> If you use `__index` to proxy property lookups to an external API (like Roblox Instance properties), the property names inside the proxy function must also remain unchanged:
>
> ```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
> -- Unsafe to rename "Name" here
> function proxy.__index(obj, key)
>     if key == "Name" then
>         return obj:GetName()
>     end
> end
> ```
>
> The rule is simple: metamethod string keys are sacred. Function values assigned to those keys can be renamed, but the keys themselves cannot.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.