A generic JavaScript deobfuscator.

Pure-static partial evaluation collapses obfuscated JavaScript into the program underneath. No per-obfuscator rules. No signature updates. The optimizer never recognizes what it is reversing, so the same passes generalize across obfuscators instead of one matcher per tool.

Generic by design.

Most deobfuscators target one tool at a time. One for obfuscator.io, one for Jscrambler. The matchers break the moment the obfuscator changes a constant, and the tooling rots between releases.

devirt.dev does not pattern-match. It lifts the sample into a custom SSA-based IR and runs standard compiler optimization passes, constant 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 for JIT performance. Neither models JavaScript semantics in enough detail to fold the obfuscation primitives directly, and neither is designed for inputs that actively resist analysis.

The devirt.dev IR represents JavaScript directly: prototype chains, type coercion edges, exception control flow, observable side effects. Memory SSA and side-effect tracking sit in the core. The passes do not recognize obfuscation. They optimize the IR, and the obfuscation happens to not survive.

The pipeline.

Six stages, wrapped in a verified fixpoint: the passes repeat until the IR stops changing, and every iteration checks that observable behavior is preserved.

  1. 01 Parse Build a real AST with the oxc compiler frontend.
  2. 02 Normalize Untangle syntax so structure is visible, member access, comma chains, destructuring.
  3. 03 Control-flow recovery Rebuild readable if/while/for from flattened switch-dispatchers, using an SSA IR with dominator analysis and a relooper.
  4. 04 Decode Execute string-decoders and constant blobs in a sandboxed JS engine and inline what they actually compute.
  5. 05 Dataflow + DCE Fold constants, inline single-use temps, collapse opaque predicates, delete dead code.
  6. 06 Rename Assign meaningful names by inferred role.

What it looks like.

Illustrative inputs and outputs. For real world samples browse the corpus on Hugging Face

String array + control-flow flattening

Input
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
console.log("Hello, world!");

Self-contained bytecode VM

Input
var VM = (function () {
  var P = [0x01, 0x07, 0x01, 0x23, 0x02, 0x03], S = [];
  var OPS = {
    0x01: function (a) { S.push(a); },
    0x02: function () { var b = S.pop(), a = S.pop(); S.push(a + b); },
    0x03: function () { return S.pop(); }
  };
  var ip = 0;
  while (ip < P.length) {
    var op = P[ip++];
    if (op === 0x03) return OPS[op]();
    if (op === 0x01) OPS[op](P[ip++]);
    else OPS[op]();
  }
})();
console.log(VM);
Output
console.log(42);