CLI tool
kuizuo/js-deobfuscator avatar
kuizuo/js-deobfuscator

js-deobfuscator reads obfuscated JavaScript by rewriting the AST

An automated JavaScript deobfuscation tool based on Babel AST, with an online playground, CLI, and programmable API.

1,186 stars270 forksJavaScriptMIT

At a glance

What is it?
This project is an automated JavaScript deobfuscation tool built on Babel AST, and its pipeline is a sequence of passes: find the decoder, unwrap it, decode the strings, unflatten the control flow, strip the protection layer and rename what is left. It comes as a CLI, a browser playground and a TypeScript API, and the hard part is always the first pass.
Who is it for?
js-deobfuscator is worth adopting if you regularly have to read obfuscated JavaScript, for instance a bundle you need to audit or a script you need to understand, because the pipeline covers the whole job from string decoding to variable renaming rather than stopping at one layer.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 73 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The whole job is a sequence of AST passes

The project describes itself as a fully automated JavaScript deobfuscation tool powered by Babel AST, built to turn obfuscated code into readable source efficiently. The word that carries the design is AST.

Working on the syntax tree rather than on text is what makes the rest possible. A string array, a control flow switch, a decoder function with a string parameter, an anti-debug loop that checks the debugger statement: each of these is a shape in the tree, and each can be matched and rewritten as a pattern. A tool that operated on the source text would be matching substrings, and obfuscators are built to defeat that.

The feature list is organised as a pipeline, and reading it in order tells you what the tool does.

Decoder discovery is first: locate decoders by string array length, by call count, or using manually supplied setup code and decoder names. String decoding is second: detect string arrays and rotators, unwrap decoder wrappers, and replace calls with the decoded values. Control-flow recovery is third: unflatten control flow, remove dead or distracting code, and merge object properties and assignments. Code cleanup follows: unminify, format, and rename variables with hex, short or custom matching, with optional keyword marking. Protection removal comes after that: remove self-defending and anti-debug logic, with repeated passes for heavily obfuscated code.

That ordering is not arbitrary. You cannot decode the strings until you find the decoder, and you cannot unflatten the control flow until the strings are readable, because the flattening usually hides behind an array of string keys. The pipeline is a dependency chain, and the first stage is the one that decides whether the rest works.

The licence is MIT, the primary language is JavaScript, and the playground is deployed at js-deobfuscator.vercel.app.

Finding the decoder is the step that decides everything

Of the three decoder discovery methods the documentation lists, the two automatic ones are heuristics and the third is you telling it the answer.

The string array length method looks for the array that holds the obfuscated strings, on the assumption that it will be the largest string collection in the file. The call count method counts how many times a candidate function is called, on the assumption that a decoder used across the whole file will be called a lot. The manual method takes setup code and decoder names that you supply.

That third option is the interesting one, because it is what the examples directory demonstrates. Each example includes a setupCode.js file, described as optional code injected before execution. That is the escape hatch: when the heuristics do not find the decoder, you can hand the tool the code that sets the obfuscation up, and the pipeline can then proceed with the decoder identified.

The API exposes the same choice as configuration. The deob function takes an options object, and the documented example sets decoderLocationMethod to 'callCount' with a decoderCallCount of 300, and mangleMode to 'hex'. So the number is a knob, not a constant, which means a sample whose decoder is called fifty times needs a different setting, and a sample with a decoy array that is longer than the real one needs the manual path.

The honest reading is that this stage is where the automation runs out. The later stages, control flow recovery and renaming, are pattern matching over a tree, and they either work or they do not. The first stage is a search over a program that was written specifically to resist the search, and the fact that the tool exposes three strategies with different failure modes is the design acknowledging that.

Protection removal needs more than one pass

The last stage of the pipeline is the one that sounds like a hack and behaves like an engineering problem.

The documentation says it removes self-defending and anti-debug logic, with repeated passes for heavily obfuscated code. That phrase, repeated passes, is doing the work. A single pass removes what it recognises. A second pass sees a program whose structure is now different, because the control flow has been unflattened and the strings have been replaced, and it can now recognise things it could not before. The two operations are mutually enabling, which is why the tool needs more than one of them.

Self-defending code is the reason this exists as a separate stage rather than a detail of the string decoding. Obfuscators emit code that detects tampering, for example by checking that a function's toString still matches what it expects, and re-obfuscates itself if it does not. A deobfuscator that rewrites the file without removing that check produces a file that detects the rewrite and reacts, and the usual outcome is a program that appears to work and then does something else.

The keyword marking option in the cleanup stage belongs to the same family of concerns. After renaming variables, you may want to mark certain ones as keywords so they stand out, and that is a small thing that makes the difference between a wall of meaningless identifiers and a file where the suspicious variable is visible.

The step after that is the cleanup proper, where unminify, format and rename run. That is the stage that produces something you can read, and it is worth remembering that a deobfuscated file is a reconstruction, not the original source. The formatting is the tool's, the variable names are invented by a mangling mode, and the comments are gone. What you get is a file that behaves like the obfuscated one and reads like nothing you wrote.

Three front ends over the same core package

The core lives in packages/deob, which the project structure describes as the core AST transforms and the deob CLI. Everything else is a way to reach it, and the documentation is explicit that there are three.

The CLI path is the one most people will use. The quick start clones the repository, installs with pnpm and runs the deob binary:

bash
git clone https://github.com/kuizuo/js-deobfuscator
cd js-deobfuscator
pnpm install

# Process one file and write output.js to a directory
pnpm exec deob path/to/input.js -o ./out

# Read from stdin
cat path/to/input.js | pnpm exec deob > output.js

The stdin form matters, because it means the tool composes with a pipeline rather than only with a file on disk.

The website is the online Playground, built with Nuxt 3 and Monaco, so you can paste a file into a browser and read the result without installing anything. The scripts in the root package.json give it four entry points, web:dev, web:build, web:generate and web:serve, and the website has its own filter in the pnpm workspace.

The third front end is the TypeScript API, which is the one to use if you want to script the tool rather than run it:

ts
import { readFileSync } from 'node:fs'
import { deob } from 'deob'

const code = readFileSync('input.js', 'utf8')
const { code: outputCode, save } = await deob(code, {
  decoderLocationMethod: 'callCount',
  decoderCallCount: 300,
  mangleMode: 'hex',
})
await save('./out')

The API returns both the transformed code and a save function, so the caller decides where the result goes, which is the difference between a tool and a library.

The examples directory is the real test suite

The example directory contains real obfuscation samples, and the documentation says so plainly. The project structure lists it as real-world samples and runners, and the top level of the repository shows the directories: cx, cycgo, gk, jsonv6, lamz, maoyan, obfuscator, pdd and wangyi.

Their presence tells you the tool was built against real files rather than against synthetic examples. That matters for deobfuscation in a way it does not for most libraries. A synthetic obfuscated file is generated by one obfuscator with its default settings, and the patterns in it are the patterns the tool author looked at while writing the transforms. A real file is obfuscated by a tool with a particular configuration, often layered with extra protections, and it may contain dead code and unusual patterns that the tool has never seen.

Each example contains an index.ts with the configuration and the runner, an input.js and an output.js pair, and the optional setupCode.js. So every sample is a fixture with a known configuration, which means the configuration is part of the test, not an afterthought. When a new obfuscator version changes its output, the fix is a new entry in this directory.

There is also a minimal directory for quick local runs, tmp, with obfuscated code in tmp/input.js and the result written to tmp/output.js, and a package script called tmp that runs npx deob tmp/input.js -o tmp. That is the loop a contributor uses: drop a file in, look at the output, adjust the options or the transforms.

The test runner is vitest, and the linting is eslint with the antfu configuration.

Where the automation runs out

The limitation is not that the tool is weak, it is that obfuscation is not one technique, and the pipeline covers a family of them.

What it handles is the string array family, with arrays, rotators and wrapper functions, and the control flow flattening family, with the switch dispatch, the dead code and the merged properties. Plus the protection layer of self-defending and anti-debug logic, and the general mess of minified output and mangled names. That is the majority of what you meet in the wild, and the example directory suggests the author has met it a lot.

What it does not cover is anything that is not a source-to-source transformation of the same program. Obfuscation that hides data rather than code, for example encrypted payloads decoded at runtime with a key fetched elsewhere, leaves a program that deobfuscates into something that still looks like noise. A VM based obfuscator, where the logic is turned into bytecode interpreted by a bundled interpreter, produces a file whose readable output is a bytecode interpreter and a byte array, and no amount of AST rewriting changes that, because the semantics are no longer in the syntax tree. The same applies to anything where the meaningful part runs in WebAssembly.

There is a second limit worth stating plainly, about what the output is. A deobfuscated file is a reconstruction, and the tool is explicit that it unminifies, formats and renames. The names are invented by the mangle mode, hex or short or a custom match, and the original names are gone. So the result is something you can read and reason about, not something you can rebuild a project from, and anyone expecting the deobfuscated file to be diffable against the original source will be disappointed.

The third is the dependency on a specific toolchain, which for a JavaScript project is not trivial. The repository pins pnpm 11.21.0 as its package manager, uses TypeScript 6, eslint 10 and vitest 4, and the whole thing is an ES module workspace with the website on Nuxt 3 and Monaco. A team on a different toolchain will be running the published package rather than the source, and the published package is the version 0.4.2 from 2026-08-11, with the repository pushed to on the same day.

Editorial conclusion

js-deobfuscator is worth adopting if you regularly have to read obfuscated JavaScript, for instance a bundle you need to audit or a script you need to understand, because the pipeline covers the whole job from string decoding to variable renaming rather than stopping at one layer. It is the wrong tool if you need to recover source that was never there, because deobfuscation reconstructs a readable equivalent rather than the original code, and it is the wrong tool if your target uses a protection scheme outside the string array and control flow flattening family. Before you run it, pick the right decoder discovery method for the sample, because the rest of the pipeline depends on finding the decoder first, and note that the current release is v0.4.2 from 2026-08-11 with the repository pushed to on the same day.

Frequently asked questions

How do I run the js-deobfuscator CLI on a file?

Clone the repository, run pnpm install, and then use pnpm exec deob path/to/input.js -o ./out to write output.js into a directory. You can also pipe a file in with cat path/to/input.js | pnpm exec deob > output.js, or drop the code in tmp/input.js and run pnpm tmp to produce tmp/output.js.

How does js-deobfuscator find the decoder?

Three ways are supported: by string array length, by call count, or by manually supplied setup code and decoder names. The API exposes the choice as decoderLocationMethod with options such as 'callCount' and a decoderCallCount value, and the examples include a setupCode.js file for cases the heuristics cannot solve.

What does the js-deobfuscator pipeline actually do?

It discovers the decoder, decodes string arrays and rotators and unwraps decoder wrappers, unflattens control flow and removes dead code, removes self-defending and anti-debug logic with repeated passes, and then unminifies, formats and renames variables using hex, short or custom matching with optional keyword marking.

Can I use js-deobfuscator as a library?

Yes, the TypeScript API imports deob from the deob package and takes the source code plus an options object such as decoderLocationMethod, decoderCallCount and mangleMode. It returns the transformed code and a save function, so the caller decides where the output goes.

What does js-deobfuscator not handle?

The documented feature set covers string arrays, control flow flattening, anti-debug and self-defending code and minified output. Encrypted payloads decoded at runtime, VM style obfuscation that turns the logic into bytecode, and logic living in WebAssembly are not source-to-source transformations, so the output for those remains unreadable however many passes you run.

Official sources

  1. kuizuo/js-deobfuscator on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/kuizuo-js-deobfuscator.svg)](https://hysenlabs.com/projects/kuizuo-js-deobfuscator)

Was this useful?