LLM-assisted deobfuscation of minified / mangled JavaScript using humanifyjs plus surrounding tooling for extracting and reintegrating modules.
Tools at a glance| Tool | Purpose | When |
|---|---|---|
humanifyjs (npx) |
Rename mangled vars/functions via LLM | Mangled but already valid JS |
prettier / js-beautify |
Reformat single-line minified code | Before humanify, if needed |
webcrack |
Unpack webpack / browserify bundles into per-module files | When bundle is one big file |
| Source-map sleuthing | // # sourceMappingURL=... may exist next to the bundle |
Always check first — beats LLM rename |
Always check for a sourcemap (bundle.js.map or sourceMappingURL comment in the file) before spending tokens on humanify. If found, original sources are recoverable for free.
Use Gemini unless the user says otherwise:
npx humanifyjs gemini -m "gemini-3.8-flash" -o <outputDir> <inputFile>
Requires GEMINI_API_KEY in env (the project's root .env is the usual place).
Other backends:
# OpenAI
npx humanifyjs openai --apiKey="$OPENAI_API_KEY" --model gpt-4.1-mini bundle.js
# Local (slower, free)
npx humanifyjs local bundle.js
Critical gotchas
1. Pseudo-TTY required in non-interactive shells
humanify calls process.stdout.cursorTo(...) for its progress bar. In Claude Code / CI / piped contexts, stdout is not a TTY and the call crashes with TypeError: process.stdout.cursorTo is not a function.
Wrap the call in script -q /dev/null:
script -q /dev/null npx humanifyjs gemini -m "gemini-3.8-flash" -o "$OUT" "$IN"
(macOS/BSD script syntax. On Linux: script -qc "<cmd>" /dev/null.)
2. Output filename is fixed
humanify always writes its result as <outputDir>/deobfuscated.js regardless of input filename. Don't expect module_1234.js in → module_1234.js out.
3. Long files = long runs
humanify processes top-level declarations sequentially with one LLM call each. A 5 MB bundle can take 30+ minutes. Strategies:
- Run on single modules extracted from the bundle, not the whole thing
- Pre-split with
webcrackfirst if the bundle is webpacked - Pick the cheapest model that still reads the code well.
gemini-3.8-flashis the default here; on a very large bundle a lite variant (gemini-3.5-flash-lite) cuts the per-declaration cost, at some accuracy cost on dense code
4. Keep an archive of the original
Before any rewrite, copy the pristine bundle to archive/bundle.original.js. humanify is non-deterministic and you may want to re-run with a different model.
When the bundle is small (<500 KB) or you have time:
mkdir -p archive out
cp bundle.js archive/bundle.original.js
script -q /dev/null npx humanifyjs gemini \
-m "gemini-3.8-flash" \
-o out bundle.js
# result: out/deobfuscated.js
Then optionally run prettier --write out/deobfuscated.js for nicer formatting.
Common in fxhash / generative-art bundles: a single bundle contains many id: (params) => { ... } modules and you only want to humanify a few interesting ones.
- Identify the module ID — search the bundle for distinctive strings, then trace back to the enclosing
<id>: (e, t, n) => {opener. - Extract the module body — match the opener, count braces (respecting strings) until the matching
}. - Wrap as a standalone arrow — write
(e, t, n) => { <body> }to a temp file. humanify needs valid top-level code. - Run humanify —
script -q /dev/null npx humanifyjs gemini -m "gemini-3.8-flash" -o "$OUT" "$IN". - Unwrap — strip the
(e, t, n) => { ... }shell back off the result. - Save and stub — write the deobf'd body to
modules/module_<id>_<name>.js, replace the body in the bundle with a stub like<id>: (e, t, n) => { window.DEOBF[<id>](e, t, n); }.
If the project already has a scripts/deobfuscate.js (or similar), reuse it — the brace-counting / string-escape logic is finicky enough that re-deriving it for each new bundle is a waste. A typical script: takes a numeric module ID, extracts the body from bundle.js, runs humanify on the wrapped function, unwraps, and writes modules/module_<id>_<name>.js.
When dealing with a webpack bundle, webcrack splits modules cleanly before humanify even runs:
npx webcrack bundle.js -o webcracked/
# Then humanify each module of interest:
script -q /dev/null npx humanifyjs gemini \
-m "gemini-3.8-flash" \
-o humanified webcracked/modules/123.js
Examples
Example 1: deobfuscate a small fxhash bundle
User says: "humanify the bundle.js in this fxhash project"
Actions:
- Check for sourcemap:
head -c 4096 bundle.js | tail -c 200— look forsourceMappingURL. None? Continue. - Confirm
GEMINI_API_KEYin.env. Source it:set -a; source .env; set +a. - Archive:
mkdir -p archive && cp bundle.js archive/bundle.original.js. - Run:
mkdir -p out && script -q /dev/null npx humanifyjs gemini -m "gemini-3.8-flash" -o out bundle.js. - Beautify:
npx prettier --write out/deobfuscated.js.
Result: out/deobfuscated.js with renamed vars, ready to read.
Example 2: deobfuscate one module out of a giant bundle
User says: "Deobfuscate module 3658 from og/bundle.js, name it treeGenerator"
Actions:
- If the project already has a deobfuscation helper (e.g.
scripts/deobfuscate.js), run it:node scripts/deobfuscate.js 3658 treeGenerator. - Otherwise: extract the module body by brace-counting, wrap as
(e, t, n) => { ... }, run humanify withscript -q /dev/null, unwrap, save tomodules/module_3658_treeGenerator.js, stub the bundle.
Result: New file in modules/ with a readable version of the function; bundle.js shrunk by stubbing.
Example 3: humanify crashed with cursorTo is not a function
User says: "humanify just dies immediately"
Diagnosis: Non-TTY stdout (this is the default in Claude Code's bash tool).
Fix: Re-run wrapped in script -q /dev/null <cmd> (macOS) or script -qc "<cmd>" /dev/null (Linux).
cursorTo is not a function
Cause: stdout is not a TTY. Fix: wrap in script -q /dev/null.
GEMINI_API_KEY not set
Cause: humanify reads from process env, not from the project .env automatically. Fix: set -a; source .env; set +a in the same shell, or pass inline: GEMINI_API_KEY=... npx humanifyjs gemini ....
Output is identical to input (no renames)
Cause: humanify only renames identifiers it considers mangled (short, single-letter). Already-readable code is left alone. Fix: confirm the input is actually obfuscated; if names like useState survive that's expected, not a bug.
Rate limits / quota errors from Gemini
Cause: large file, many top-level functions → many sequential LLM calls. Fix: switch to local backend for free local rename, or extract a single module (Workflow B) instead of the whole bundle.
humanify takes hours
Cause: big bundle, sequential per-function calls. Fix: pre-split with webcrack, or only humanify the modules you actually need.
Output directory contains stale deobfuscated.js
Cause: humanify writes to a fixed filename. A failed second run will pick up the previous good output. Fix: rm -f $OUT/deobfuscated.js before each run.