CloakJS is a single-file, dependency-free JavaScript obfuscator for the browser and Node.js. It renames bindings, hides strings behind a generated decoder, rewrites numeric literals, flattens control flow inside function bodies, injects opaque predicates and unreachable decoy code, escapes identifiers, and can add runtime guards. It returns JavaScript source as a string — it never executes the code for you.
<script src="https://cdn.jsdelivr.net/gh/Infinitode/CloakJS@main/cloakjs.min.js"></script>
<script>
const code = CloakJS.obfuscate('console.log("Hello from CloakJS!")', "extreme");
console.log(CloakJS.obfuscateWithStats("var n = 21; console.log(n * 2);", "high").stats);
</script>Important
CloakJS is not distributed through npm. There is a package named cloakjs on the npm registry, but it is an unrelated AOP/Proxy wrapper library ("a small AOP/Proxy/Wrapper library with a fluent interface", currently 1.0.1, maintained by somebody else) — it is not this project and it does not contain this obfuscator. Do not install that package expecting CloakJS. Grab cloakjs.js or cloakjs.min.js from this repository (or from jsDelivr) instead; the library has no dependencies and no package manifest, so there is nothing to resolve and nothing to build.
Warning
Obfuscation makes source harder to read, not secure. It cannot protect secrets, credentials, licence keys or security-critical checks that ship to the client, and it does not resist an analyst who can instrument the running page. Everything CloakJS does is reversible by hand or by tooling. Treat it as a speed bump and a deterrent — never as a security boundary.
extremeis a real mode now. In 1.2.0 its options were identical tohigh; it now turns on identifier escaping, codepoint-safe string splitting, decoy string-array entries, maximum flattening and decoy amounts, and a recurringdebuggertrap.- Control-flow flattening reaches inside function bodies (nested bodies are flattened before the bodies that contain them), with a shuffled
caselayout and decoy cases that can never be selected. - A self-contained decoder. Generated code no longer depends on
atob,Uint8ArrayorTextDecoder: it ships its own Base64, alphabet-substitution, RC4 and UTF-8 handling and only usesTextDecoderas a fast path when the host has one. - Template literals are first class. Identifiers inside
`${…}`are analysed and renamed, and static templates are encrypted like ordinary strings (tagged templates are left alone). - Much broader, scope-aware mangling. Parameters (including defaults and rest),
catchbindings, class names, named function expressions and template substitutions are renamed — but a spelling is only renamed when every occurrence lies inside a scope that declares it, so globals owned by other scripts keep working. - New options:
stringSplitting(+MinLength,MaxParts),stringArrayShuffle,stringArrayDecoys,stringEncoding,stringAlphabet,identifierEscaping,preserveFunctionNames,controlFlowThreshold,deadCodeAmount,debugProtection(+Interval),wrapInIife,disableConsoleOutput,compactOutput,seed. - Validated options with types. A typo like
stringEncryptonnow throws aTypeErrorinstead of being silently ignored, and wrong value types are rejected before anything runs. - Reproducible output via
seed, richerstats(including thetechniquesthat were actually applied) and clearerwarnings.
Download cloakjs.js (readable) or cloakjs.min.js (minified), or load either from jsDelivr:
<!-- Minified build -->
<script src="https://cdn.jsdelivr.net/gh/Infinitode/CloakJS@main/cloakjs.min.js"></script>
<!-- Development: use .../cloakjs.js instead -->For reproducible deployments, replace @main with a tag or commit hash from this repository. The minified file is generated from cloakjs.js with Terser; there is no npm release. The badge above shows jsDelivr's live download information rather than a hard-coded count.
In Node.js (any modern Node, no extra packages):
const CloakJS = require("./cloakjs.js");
const result = CloakJS.obfuscateWithStats('const who = "world"; console.log(`hi ${who}`);', "extreme");
console.log(result.code); // obfuscated source, as a string
console.log(result.stats); // what the pipeline actually applied
console.log(result.warnings); // anything it refused to do — check these
// reproducible output while you are building a pipeline
const first = CloakJS.obfuscate("var a = 1; console.log(a);", { seed: "build-42" });The file is UMD: it works as the global CloakJS in a <script> tag, as an AMD module, and as a CommonJS module. The obfuscated script it produces is self-contained — the generated decoder implements its own Base64, alphabet substitution, RC4 and UTF-8 handling, so it runs in a bare sandbox with no globals, polyfills or helper files (it will use TextDecoder/Uint8Array when the host provides them).
| Method / property | Description |
|---|---|
CloakJS.obfuscate(source, options?) |
Return the obfuscated JavaScript string. |
CloakJS.obfuscateWithStats(source, options?) |
Return { code, stats, warnings }. Check warnings before shipping. |
CloakJS.measure(source, options?) |
Return { originalBytes, obfuscatedBytes, ratio } for a fresh randomized output. |
CloakJS.resolveOptions(options?) |
Return a validated copy of a preset or of the defaults merged with your options. |
CloakJS.presets, CloakJS.defaultOptions |
Available presets and default settings (every preset defines every key). |
CloakJS.techniques |
Metadata for every transform: key, name, summary, changesBehavior. |
CloakJS.NAME, CloakJS.VERSION, CloakJS.BANNER |
Metadata. |
source must be a string. options may be a preset name ('off', 'low', 'medium', 'high', 'extreme'), an options object, or omitted (defaults to the high settings). Unknown preset names, unknown option names and wrong option types throw, so a typo like stringEncrypton fails loudly instead of silently doing nothing.
Output is randomized and differs between calls; pass a seed when you need reproducible builds. measure generates its own random sample, so it does not necessarily describe a previous obfuscate result.
| Field | Meaning |
|---|---|
identifiersRenamed |
Distinct spellings that were renamed. |
identifiersEscaped |
Identifier occurrences rewritten with \uXXXX escapes. |
stringsEncrypted |
String literals (and static template literals) moved into the encoded array. |
stringsSplit |
Strings that were split into concatenated chunks. |
stringArrayEntries |
Entries in the emitted string array, including decoys. |
numbersEncoded |
Numeric literals that were rewritten as expressions. |
controlFlowBlocks |
switch dispatchers that were generated. |
deadCodeBlocks |
Unreachable decoy blocks that were inserted. |
opaquePredicates |
Opaque conditions that were generated. |
originalBytes, obfuscatedBytes, ratio |
UTF-8 byte counts; ratio is output ÷ input, so anything above 1 means the output grew. |
techniques |
The technique keys that were actually applied to this input. |
seed |
The seed that was used, or null. |
warnings reports things CloakJS deliberately skipped: eval()/with() (dynamic scope makes renaming unsafe), new Function("…") built from a string, module syntax, or an output that failed the internal parse check. It never guarantees behavioural equivalence — test the generated code yourself.
| Preset | Identifier mangling | Identifier escaping | String array | String splitting / decoys | Number encoding | Control flow flattening | Dead code + opaque predicates | Self-defending | Debug protection | Console silencing | IIFE wrapper |
|---|---|---|---|---|---|---|---|---|---|---|---|
off |
– | – | – | – | – | – | – | – | – | – | – |
low |
yes | – | – | – | – | – | – | – | – | – | – |
medium |
yes | – | Base64, ordered | – | yes | – | – | – | – | – | – |
high |
yes | – | RC4 + shuffled alphabet | – | yes | yes | yes | yes | – | – | – |
extreme |
yes | yes | RC4 + shuffled alphabet | yes | yes | yes (maximum) | yes (maximum) | yes | yes | – | – |
extreme is a genuine mode, not a relabelled high: compared to high it turns on identifier escaping, string splitting (down to 6-character chunks, up to 5 parts), six decoy entries per string array, the maximum decoy amount, and an anti-debugging timer. disableConsoleOutput and wrapInIife change observable behaviour, so they are options that no preset enables.
Every preset enables compactOutput. A partial options object overrides only the keys you supply:
const result = CloakJS.obfuscateWithStats(
'var message = "hi"; for (var i = 0; i < 2; i++) { console.log(message, i); }',
{ identifierMangling: false, controlFlowFlattening: false, stringArrayDecoys: 12 }
);
if (result.warnings.length) console.warn(result.warnings);
console.log(result.code, result.stats.techniques);| Option | Type | Default (high) |
What it does |
|---|---|---|---|
identifierMangling |
boolean | true |
Renames declared bindings whose occurrences provably stay inside a scope that declares them. |
identifierEscaping |
boolean | false (true in extreme) |
Rewrites identifiers with \uXXXX escapes, e.g. _0x1a2b becomes _\u0030\u0078\u0031a2b; the escapes parse to the same name. |
preserveFunctionNames |
boolean | false |
Keeps function and class names, so Function#name and constructor.name stay readable. |
stringEncryption |
boolean | true |
Moves strings into an encoded array read through a generated decoder. |
stringEncoding |
"base64" | "rc4" |
"rc4" |
Cipher applied to each payload. |
stringAlphabet |
"standard" | "shuffled" |
"shuffled" |
Base64 alphabet substitution for the array, with optional reversal of each entry. |
stringArrayShuffle |
boolean | true |
Shuffles array entries and adds an index map, so positions leak nothing. |
stringArrayDecoys |
count | 0 (6 in extreme) |
Adds unused, well-formed entries to the string array. |
stringSplitting |
boolean | false (true in extreme) |
Splits strings into codepoint-safe chunks that are decoded separately and concatenated. |
stringSplitMinLength |
count | 8 (6 in extreme) |
Shortest string that may be split. |
stringSplitMaxParts |
count | 4 (5 in extreme) |
Upper bound on chunks per string. |
numberEncoding |
boolean | true |
Rewrites numeric literals as hex, binary, octal, bitwise, arithmetic or scaled expressions. |
controlFlowFlattening |
boolean | true |
Moves statement runs into a shuffled switch dispatcher, inside function bodies as well as at the top level. |
controlFlowThreshold |
ratio | 1 |
Probability of flattening each eligible statement run (0 disables it, 1 flattens all of them). |
deadCodeInjection |
boolean | true |
Inserts unreachable decoy blocks, including dispatcher cases that can never be selected. |
deadCodeAmount |
count | 3 (6 in extreme) |
Rough number of decoy statements per inserted block. |
opaquePredicates |
boolean | true |
Generates conditions whose value is computed at obfuscation time and used as guards. |
selfDefending |
boolean | true |
Adds a prelude that hashes generated functions and throws if they were patched. |
debugProtection |
boolean | false (true in extreme) |
Installs a recurring debugger trap through a guarded setInterval. |
debugProtectionInterval |
count (ms) | 4000 (2500 in extreme) |
Timer period for the trap. |
disableConsoleOutput |
boolean | false |
Replaces console.log and friends with no-ops before the payload runs. |
wrapInIife |
boolean | false |
Wraps the program in (function(){ … })(); so top-level declarations stay private. |
compactOutput |
boolean | true |
Collapses blank lines and indentation. Token-aware, so string and template bodies are never touched. Not a minifier. |
seed |
string | number | null | null |
Seeds the generator for reproducible output. |
- Identifier mangling collects bindings from
var/let/const, function declarations, named function expressions, parameter lists (including defaults and rest parameters),catch (…)clauses and class names. A spelling is renamed only when every occurrence lies inside a scope that declares it, which keeps references to globals defined in other scripts working. Property names, object keys and shorthand properties, method and field names, labels, destructuring patterns, module bindings and anything reachable dynamically (eval,with, string keys such asobj["name"]) are left alone. Template substitutions are analysed, so`${count}`is renamed with the rest of the code. - Identifier escaping writes identifiers with unicode escapes. Escapes normalise to the same name, so behaviour is unchanged, but the source stops matching grep patterns.
- String encryption evaluates each eligible literal, encodes its UTF-8 bytes (RC4 with a random per-run key, then Base64), optionally substitutes a shuffled Base64 alphabet and reverses the entry, and stores it in an array that the generated decoder reads through a shuffled index map. Call sites look like
_0x1f3(0x2,41), sometimes with decoy arguments. Static template literals are treated like ordinary strings; tagged templates are skipped. Directive prologues, object keys,caselabels, method names andimport/exportspecifiers are skipped too. - String splitting breaks long strings into codepoint-safe chunks (never inside an emoji or surrogate pair). With encryption on, each chunk becomes its own array entry and the call site becomes a concatenation, so no array entry holds a complete string.
- Number encoding rewrites literals as hex, binary, octal, XOR/OR masks, shifts,
~, sums, products with remainder, or exact power-of-two scaling. Single-digit literals, BigInts andInfinity/NaNare left as they are, and floats are only scaled by powers of two so their values stay bit-exact. - Control flow flattening splits each region (a function body or the top level) into statements and moves runs of statements that cannot change hoisting, scoping or jump targets into
switchclauses with random labels. Case clauses are emitted in a shuffled layout while a sequence array preserves execution order, and decoy cases are never selected. Declarations (var/let/const/function/class),do…whilebodies,else/catch/finallycontinuations and the final top-level statement stay outside the dispatcher, and nested function bodies are flattened before their enclosing bodies. - Opaque predicates are expressions whose value is evaluated at obfuscation time and verified with
Function, so a guard that should be true always is. They wrap statements, dispatchers and dead branches. - Dead code injection adds unreachable blocks that reference real runtime pieces (loops, local helper functions, decoy decoder lookups, array operations) so they cannot be dismissed by pattern alone. Injection points are only chosen after a
;whose neighbours are notelse/while/catch/finally, and never in a position that would change the script's completion value. - Self-defending prelude hashes the source text of generated functions through
Function.prototype.toString()and throwsError("Unexpected token")when the text no longer matches — including when a patchedtoStringreturns[native code]. The hash ignores whitespace, so re-indenting the output still runs, but renaming or patching a generated function does not. It covers the generated decoder and its own probe function; it does not detect an attacker who disables the check itself. - Debug protection installs
setInterval(function(){ … debugger … }, interval), guarded bytypeof setInterval, and calls.unref()when the handle supports it so Node.js processes can still exit. - Console silencing and IIFE wrapping are opt-in behaviour changes, described in the tables above.
Read this section before shipping obfuscated output.
| Situation | What happens |
|---|---|
Code reads someFunction.name or this.constructor.name |
Mangling renames functions and classes, so those strings change. Set preserveFunctionNames: true to keep them. |
| Bindings that are part of a public API | Mangling renames top-level declarations too. Disable identifierMangling (or use low and check) when other scripts refer to them by name. |
Property access through strings, e.g. obj["count"] |
Every spelling that appears as a property name, object key or string key is left untouched, so such code keeps working. |
eval(), with(), new Function("… " + someLocal) |
Mangling and escaping are skipped automatically and a warning is added; the other transforms still run. |
import / export |
Module syntax is detected and mangling is skipped with a warning, because new Function (the internal parse check) only accepts scripts. Strings in from clauses are untouched. |
| Output is minified after obfuscation | selfDefending throws, because a minifier renames or rewrites the generated functions. Pure re-indentation (whitespace only) is tolerated. Disable selfDefending if the output goes through Terser/esbuild/webpack again, or obfuscate after minifying. |
| Script completion value | The last top-level statement is never moved, wrapped or followed by decoy code, so the value a script evaluates to is preserved — except with wrapInIife, which makes it undefined. |
debugProtection in Node.js |
A repeating timer is installed. It is unref'ed when possible so the process can still exit, but code that observes timers will see it. |
disableConsoleOutput |
All common console methods become no-ops for the whole page/process, not just for your code. |
wrapInIife |
Top-level var/function declarations and top-level this stop leaking to the global object, and the completion value becomes undefined. |
| Strict mode | A leading "use strict"; directive is kept in front of all generated code, and moved inside the IIFE when wrapInIife is used. |
| Heavily minified input | CloakJS is a lexer, not a parser. It preserves anything it does not explicitly rewrite and skips statements it cannot prove safe, but always test the generated code. |
| Size | Dead code, decoys, flattening and the decoder all add bytes. On a multi-kilobyte file expect roughly 1.0× (low), 1.2× (medium), 2–4× (high) and 4–8× (extreme). Small snippets grow much more, because the generated decoder is a fixed cost that does not shrink with the input. |
CloakJS uses a conservative lexer rather than a full JavaScript parser: it understands strings, template literals (including nested substitutions), regular expressions, comments, ASI-sensitive statement splitting, destructuring, classes including private fields, generators and async functions well enough to avoid touching what it cannot reason about. It does not support JSX, TypeScript, decorators or stage-3 syntax, which may end up only partially obfuscated. Every generated output is checked with new Function, and a syntax error is reported through warnings instead of being hidden. If you need production-grade semantic guarantees, use a parser-based tool and review its output too.
- It does not encrypt anything with a key the client does not have. The RC4 key, the alphabet and the decoder all ship with the output.
- It does not stop runtime inspection: patching the decoder, dumping the string array or attaching a debugger all still work.
- It does not protect secrets. If a value must stay secret, it must not be sent to the client.
Run the dependency-free Node test suite (no fixtures, no network):
node --testThe suite compares the behaviour of the original and the obfuscated program for every preset and for a matrix of option combinations, so a change that alters program semantics fails the run.
To regenerate the minified file locally, fetch Terser as a temporary build tool (the library itself has no dependencies and needs no manifest):
npm exec --yes --package=terser@5.44.0 -- terser cloakjs.js --compress --mangle --comments '/^!/' --output cloakjs.min.jsOn pushes that change cloakjs.js or the test suite, the Ubuntu GitHub Actions workflow runs the tests, regenerates cloakjs.min.js, verifies that the minified build reproduces the readable build's output for the same seed (in both a full sandbox and a bare one), and commits the file back to the same branch only if it changed. The workflow needs Actions to have permission to write repository contents; branch protection rules may prevent an automated push.
Contributions and bug reports are welcome via GitHub issues and pull requests. When reporting a transform bug, include the input snippet, the preset/options, the warning list and the observed output — that is usually enough to reproduce it.
CloakJS is released under the MIT License. Copyright (c) 2026 Infinitode.