Minified JavaScript is hard to read because the whitespace and the local names are gone. Obfuscated JavaScript is harder still: every string sits in one shuffled array behind a decoder function, and if and switch logic is hidden behind lookup objects and dead branches. This tool turns both back into code you can read. Use it to debug your own production build, to review a script before you add it to your site, or to audit third-party code you are allowed to inspect.
Paste the code or open a .js file. The page formats it, decodes what it can, and shows what changed. For a webpack or browserify bundle, it also lists the modules and lets you download them as a .zip. Nothing is uploaded, and the code you paste is never run.
What it does
Input. Paste code into the box, drop a .js, .mjs or .cjs file onto it, or choose a file. The limit is 5 MiB. The parser recovers from many syntax errors, so a truncated file usually still works.
Transforms. The work is done by webcrack 2.16.0 (MIT) on top of Babel, with four switches:
- Decode string arrays. Code from obfuscator.io (the
javascript-obfuscatorpackage) keeps its strings in an array, rotates the array at start-up until a checksum matches, and reads each string through a decoder that may add Base64 or RC4. webcrack finds the array, the rotation function and the decoders, including the small wrapper functions that call them with shifted indexes. The decoders then run in this page’s own evaluator (see below), and every call is replaced by the plain string. - Rename
_0x…and destructured names. Obfuscator names such as_0x4e2a1bbecome short names that say what the variable holds:pfor parameters,ffor functions,vLSDemoUserfor a variable that starts as the string"demo-user". When a value is unpacked from an object (const { formatPrice: i } = require(947)), it takes the property name back:const { formatPrice } = …. - Split webpack / browserify bundles. webpack 4 and 5 bundles, webpack chunk files (
self.webpackChunk… .push(…)) and browserify bundles are split into one file per module, withrequire(id)calls pointing to./id.js. Browserify keeps the original file names (./cart,./format), so those come back too. - Rebuild JSX.
React.createElement(…)andjsx(…)calls are turned back into JSX tags.
The rest always runs: formatting, !0 → true, void 0 → undefined, a["b"] → a.b, comma sequences split into statements, ternaries and && chains used as statements turned into if, number and string expressions folded, and obfuscator.io’s self-defending and debug-protection code taken out. Control-flow flattening (while(true) switch dispatchers), proxy-function objects and dead branches are undone in the same webcrack step as the string array, so only when a string array is found and Decode string arrays is on.
Output. The readable code, with Copy code and Download .js buttons; a summary of what changed (size and line count before and after, how many strings were decoded, how many distinct _0x names there were before and after, the bundle type and module count); and, for bundles, the module table with a preview and download per module and Download modules as .zip.
A real example
The examples on this page come from our own test fixtures: a three-file shopping-cart app, bundled with esbuild and then obfuscated with javascript-obfuscator 5.9.0 using its high-obfuscation preset (minus debugProtection and disableConsoleOutput, which would stop a test run). The input starts like this:
(function(_0x567fa5,_0x462189){var _0x30f486=_0x567fa5();function _0x4c9d0a(_0x39c21a,_0x45f13e,_0x327a90,_0x37d2ce,_0x592a53){return _0xc5f8(_0x45f13e-0x1f2,_0x37d2ce);} …
That is 52,088 bytes on 2 lines, with 942 distinct _0x names and every string RC4-encrypted. The output is 2,891 bytes on 116 lines. 353 decoder calls were replaced by their strings, and no _0x name is left. One module of the result:
var vC = class {
constructor(p11) {
this.owner = p11;
this.items = [];
}
addItem(p12, p13, p14 = 1) {
if (p14 <= 0) {
throw new Error("quantity must be positive");
}
…
receipt() {
var v8 = this.items.map(p15 => p15.quantity + " x " + p15.name + " = " + formatPrice(p15.unitPrice * p15.quantity));
v8.push("TOTAL " + formatPrice(this.totalCents()));
return v8.join("\n");
}
};
The method names, the strings and formatPrice are back. The parameter names (p12, p13, p14) are not, because the obfuscator never kept them. Our tests run the original and the deobfuscated program side by side in Node and check that they print exactly the same output. They do this for this file, for the default preset, for terser and esbuild minified builds, and for a webpack 5 and a browserify bundle of the same app.
Timing, measured in Node 22 on an Apple-silicon laptop: about 1 second for the 52 KB high-obfuscation file, most of it spent decoding RC4 strings while the rotation loop searches for its checksum; the page itself, in headless Chromium on the same machine, reported 0.8 s. A plain minified file is much faster. The TypeScript compiler minified with esbuild (3.6 MB) came out as 5.6 MB and 181,896 lines in 6.5 seconds. Babel keeps the whole syntax tree in memory, which is why the input limit is 5 MiB.
How string decoding works without running the code
The usual way to decode a string array is to run the obfuscator’s own decoder: webcrack’s command-line version runs it in an isolated-vm sandbox. A web page can only do that with eval or new Function, which this site’s Content Security Policy does not allow, and should not: the snippet comes from code you do not trust.
So this page brings its own small evaluator. webcrack cuts out just the string array function, the rotation function and the decoders, and the evaluator walks their syntax tree and computes the result. That code can only touch values it creates itself, plus a short list of pure built-ins: parseInt, String.fromCharCode, decodeURIComponent, RegExp#test, Math, and the usual string, array and number methods. It has no access to window, self, fetch, timers, postMessage, real prototypes or constructor. A step budget (50 million evaluation steps), a separate work budget charged by the size of every string and array a built-in reads or creates (100 million characters or elements), a call-depth limit and caps on string and array size stop runaway loops and memory growth. Regular expressions are limited to the one pattern obfuscator.io’s self-defending code uses, tested against at most 512 characters, so a crafted pattern cannot hang the page. Decoded strings that add up to more than 4 times the input (or 1 MiB) are not inlined. Functions stringify to their own source text, as in a browser, because obfuscator.io’s self-defending code compares a function’s text against a regular expression.
When a decoder needs something outside that subset (for example a spread, new on a built-in, or any regular expression other than obfuscator.io’s own), evaluation stops, the strings stay encoded, and the summary says found, but not decoded. Formatting, renaming and bundle unpacking still apply; control-flow objects, switch dispatchers and dead branches are left in place, because webcrack undoes them in the same step as the strings. You can also turn decoding off with the first switch.
How your code stays private
Your code is not uploaded:
- Parsing, transforms, string decoding, renaming, unpacking and the
.zipall run in a Web Worker on your device. The code is never executed, not even inside the worker: the evaluator described above computes decoder results withouteval. - Code, strings and module names are shown as plain text; nothing from the input is interpreted as HTML.
- Analytics events are coarse: success or an error code, whether the code was pasted or opened as a file, the string-decoding status (
decoded,none,failedoroff), the bundle type, a range for the number of modules, a size bucket and a duration bucket. They never contain code, strings, file or module names. The full list is on the privacy page. - The tool page shows no ads.
How to verify this yourself
- Open your browser’s developer tools and switch to the Network tab before you paste anything. Deobfuscate some code, copy it and download the
.zip. The only requests are page assets and the worker script/_astro/jsdeob-worker-*.js; none carries your code. - Or load the page, go offline (turn off Wi-Fi, or tick Offline in the Network tab) and run it. It still works.
- The worker runs under the Content Security Policy
default-src 'none'; script-src 'self'; connect-src 'none'. There is no'unsafe-eval', so the browser would blockevalandnew Functioneven if something tried to use them, and the worker cannot make network requests.
Limitations
- Original local names are gone. Minifiers and obfuscators drop them; nothing in the file says what they were. The tool gives placeholder names. If the site ships a source map, the source map restorer gives you the real files instead.
- String decoding has a subset. It covers obfuscator.io’s
none,base64andrc4encodings, index shifts, wrapper functions and rotation, as our fixtures show. Other obfuscators may use constructs the evaluator refuses; their strings then stay encoded. - No virtual-machine obfuscation. Obfuscators that compile code into their own bytecode and ship an interpreter (JScrambler’s more aggressive modes, custom VMs) are not undone.
- Scope-hoisted bundles stay one file. Rollup, Vite and esbuild production builds merge modules into shared scopes, so there are no module boundaries left to split. Only webpack and browserify keep a module table.
- Self-defending code. If you turn string decoding off on obfuscator.io output that uses
selfDefending, the reformatted code may hang when run, because that check detects formatting. Read it; do not run it. - Input size. 5 MiB, and a three-minute time limit.
FAQ
Is the output guaranteed to behave like the input?
No tool can promise that for every input, but each transform is meant to keep behaviour the same, and our tests check it by running both versions of every fixture. Treat the output as something to read. If you need to run code, run the original.
Why do I see names like vLSHttpsapiexamplecomv1?
The renamer builds names from what a variable is first assigned: vLS… means a string literal (here "https://api.example.com/v1/orders"), vLN0 a number, vO an object, vC a class, f a function, p a parameter. Turn renaming off to keep the _0x names if you want to compare with the original file.
The summary says “found, but not decoded”. What now?
The decoder uses something the evaluator does not support (the reason is shown in brackets). The decoder function and its calls are kept in the output, so you can still follow which call returns which string. The command-line webcrack, which runs decoders in isolated-vm, may decode them on your own machine.
Does it deobfuscate Hermes bytecode, WebAssembly or Python?
No, only JavaScript source. For WebAssembly, use the WASM analyzer.
Compliance
Deobfuscate your own code, code you are authorized to review, or open-source and third-party scripts whose licenses allow it. Reading code to understand what it does is not permission to copy it or to get around a license, and some licenses and laws restrict reverse engineering. See the disclaimer.
Related guides
- JS bundle and source map restorer: when a bundle comes with a source map, restore the original files and names instead of deobfuscating.
- Unminify and Recover JavaScript Source from Bundles with REA: source maps, webpack chunk recovery, Wakaru unpacking and data-flow tracing from the command line.
- Trace Website API Calls with REA: Browser, HAR and Bundles: find which part of a front-end bundle sends a request.
- Reverse Engineer an Electron App with REA: Electron apps ship minified JavaScript inside
app.asar; extract it with the ASAR analyzer and read it here. - Deobfuscate javascript-obfuscator (obfuscator.io) Output: Tested: 14 option sets run through this tool, with sizes, decoded strings and the options it cannot undo.