Variable Exclusion List defines which identifiers must never be renamed during name mangling. It is the escape hatch you reach for when something outside your file refers to a name by its exact text, and a rename would break that contract silently.
How to define exclusions
The setting is a multi-line list of regular expressions. Each line is one matching rule.
^myname$
^myvalue$
Source code:
function () {
var myname;
var myvalue;
var hello;
}
Output with the exclusions above:
function () {
var myname;
var myvalue;
var a;
}
Configuration
In jso.config.json, the publicNames array is the ergonomic form — a list of regular expressions for public names to preserve, mapped to VariableExclusion:
{
"publicNames": ["^MyWidget$", "^ga$", "^dataLayer$"]
}
The raw engine option takes the same patterns as one newline-separated string:
{
"options": {
"VariableExclusion": "^MyWidget$\n^ga$\n^dataLayer$"
}
}
What actually needs excluding
Most identifiers do not need to be here. Local variables, parameters, and local functions can always be renamed, because nothing outside the file can see them. Exclusions are for names that cross a boundary the obfuscator cannot see:
- Globals other scripts call. A widget entry point (
window.MyWidget), an analytics queue (dataLayer, ga), a callback name a third-party script looks up by string.
- Names referenced from HTML. An inline
onclick="submitForm()" attribute is a string in your markup, not code the obfuscator rewrote — the function it names must keep that name.
- Names built at runtime. Anything reached through
window[someString], eval, or a dynamic import() path. The obfuscator cannot follow a computed reference, so it cannot know the name is load-bearing.
- Framework and platform contracts. Lifecycle method names a framework invokes by convention, service-worker event names, and anything a native bridge resolves by string.
The failure mode when you miss one is worth internalising: the build succeeds, the file parses, and the feature is broken at runtime in a way that only shows up on the path that uses it. That is why verifying protected output means running your tests, not just checking that the output loads.
Not the same as reservedStrings
Two different escape hatches for two different transform families — mixing them up is a common early mistake:
| VariableExclusion / publicNames | reservedStrings |
| Protects | Identifiers (variable and function names) | String literals |
| Against | ReplaceNames, RenameGlobals, DeepObfuscate | MoveStrings, EncodeStrings |
| Use when | Something calls a name from outside the file | Something matches a literal byte-for-byte |
For a whole region of a file rather than individual names, use inline directives, which mark the region in the source itself. For property and method names specifically, see Protect Members.
Anchor your patterns
These are regular expressions, not exact names, and an unanchored pattern matches far more than people expect. id matches id, but also idx, valid, hidden, and candidate — so a single sloppy line can leave most of your identifiers un-renamed while the build still reports success and the output still looks obfuscated at a glance.
- Anchor both ends for an exact name:
^dataLayer$, not dataLayer.
- Anchor the start for a deliberate prefix convention:
^api_ keeps every identifier you have prefixed for export.
- Matching is case-sensitive.
^myWidget$ does not match MyWidget.
- Audit the result. Diff the identifier count in protected output before and after adding a pattern. If adding one exclusion left dozens of names readable, the pattern is too broad.
Keep the list short: every excluded name is a name an analyst gets for free, and a long exclusion list is usually a sign the module's public surface is too wide. Where you can, narrow the surface instead — expose one entry point object, exclude that one name, and let everything behind it be renamed.