devirt.dev
A generic JavaScript deobfuscator.
Pure-static partial evaluation collapses obfuscated JavaScript into<br>the program underneath. No per-obfuscator rules. No signature<br>updates. The optimizer never recognizes what it is reversing, so the<br>same passes generalize across obfuscators instead of one matcher per tool.
View on GitHub → See how it works →
Generic by design.
Most deobfuscators target one tool at a time. One for<br>obfuscator.io, one for Jscrambler. The matchers<br>break the moment the obfuscator changes a constant, and the tooling<br>rots between releases.
devirt.dev does not pattern-match. It lifts the sample into a custom<br>SSA-based IR and runs standard compiler optimization passes, constant<br>propagation, DCE, GVN, value-range analysis to fixpoint.
Why a custom IR.
LLVM IR is built for hardware code generation. V8's TurboFan is built<br>for JIT performance. Neither models JavaScript semantics in enough<br>detail to fold the obfuscation primitives directly, and neither is<br>designed for inputs that actively resist analysis.
The devirt.dev IR represents JavaScript directly: prototype chains,<br>type coercion edges, exception control flow, observable side effects.<br>Memory SSA and side-effect tracking sit in the core. The passes do not<br>recognize obfuscation. They optimize the IR, and the obfuscation<br>happens to not survive.
The pipeline.
Six stages, wrapped in a verified fixpoint: the passes repeat until the<br>IR stops changing, and every iteration checks that observable behavior is<br>preserved.
01 Parse<br>Build a real AST with the oxc compiler frontend.
02 Normalize<br>Untangle syntax so structure is visible, member access, comma chains, destructuring.
03 Control-flow recovery<br>Rebuild readable if/while/for from flattened switch-dispatchers, using an SSA IR with dominator analysis and a relooper.
04 Decode<br>Execute string-decoders and constant blobs in a sandboxed JS engine and inline what they actually compute.
05 Dataflow + DCE<br>Fold constants, inline single-use temps, collapse opaque predicates, delete dead code.
06 Rename<br>Assign meaningful names by inferred role.
What it looks like.
Illustrative inputs and outputs. For real world samples browse the corpus on Hugging Face →<br>String array + control-flow flattening<br>Input<br>var _0x4a2f=['log','Hello, world!'];(function(_0x2f1e,_0x4a2fa){var _0x2b3c=function(_0x1e4f){while(--_0x1e4f){_0x2f1e['push'](_0x2f1e['shift']());}};_0x2b3c(++_0x4a2fa);}(_0x4a2f,0xe8));var _0x2b3c=function(_0x2f1e,_0x4a2fa){_0x2f1e=_0x2f1e-0x0;return _0x4a2f[_0x2f1e];};var _0x5af1=0x1;while(_0x5af1){switch(_0x5af1){case 0x1:console[_0x2b3c('0x0')](_0x2b3c('0x1'));_0x5af1=0x0;break;}}
Output<br>console.log("Hello, world!");
Self-contained bytecode VM<br>Input<br>var VM = (function () {<br>var P = [0x01, 0x07, 0x01, 0x23, 0x02, 0x03], S = [];<br>var OPS = {<br>0x01: function (a) { S.push(a); },<br>0x02: function () { var b = S.pop(), a = S.pop(); S.push(a + b); },<br>0x03: function () { return S.pop(); }<br>};<br>var ip = 0;<br>while (ip P.length) {<br>var op = P[ip++];<br>if (op === 0x03) return OPS[op]();<br>if (op === 0x01) OPS[op](P[ip++]);<br>else OPS[op]();<br>})();<br>console.log(VM);
Output<br>console.log(42);