Claude
Skills
Sign in
Back

reverse-engineer-js

Included with Lifetime
$97 forever

Reverse-engineer minified or obfuscated JavaScript bundles. Primary tool: humanify (humanifyjs) — uses an LLM to rename mangled identifiers back to meaningful names. Default model: gemini-3.1-flash-lite-preview. Use when the user says 'deobfuscate this bundle', 'humanify this', 'rename minified vars', 'reverse engineer this JS', 'what does bundle.js do', or is working in a project with a deobfuscated `og/` or `modules/` folder fed by humanify. Covers humanify CLI invocation, the pseudo-TTY workaround for cursorTo crashes, single-module extraction from a bundle, and chaining with beautifiers.

AI Agents

What this skill does


# Reverse Engineer JS

LLM-assisted deobfuscation of minified / mangled JavaScript using [humanifyjs](https://github.com/jehna/humanify) 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.

# Default humanify invocation

Use Gemini with the lite preview model unless the user says otherwise:

```bash
npx humanifyjs gemini -m "gemini-3.1-flash-lite-preview" -o <outputDir> <inputFile>
```

Requires `GEMINI_API_KEY` in env (the project's root `.env` is the usual place).

Other backends:

```bash
# 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`:

```bash
script -q /dev/null npx humanifyjs gemini -m "gemini-3.1-flash-lite-preview" -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 `webcrack` first if the bundle is webpacked
- Use the cheapest competent model (`gemini-3.1-flash-lite-preview` is the current sweet spot)

## 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.

# Workflow A: Whole-bundle deobfuscation

When the bundle is small (<500 KB) or you have time:

```bash
mkdir -p archive out
cp bundle.js archive/bundle.original.js
script -q /dev/null npx humanifyjs gemini \
  -m "gemini-3.1-flash-lite-preview" \
  -o out bundle.js
# result: out/deobfuscated.js
```

Then optionally run `prettier --write out/deobfuscated.js` for nicer formatting.

# Workflow B: Single-module extraction (large bundles)

Common in fxhash / generative-art bundles: a single bundle contains many `id: (params) => { ... }` modules and you only want to humanify a few interesting ones.

1. **Identify the module ID** — search the bundle for distinctive strings, then trace back to the enclosing `<id>: (e, t, n) => {` opener.
2. **Extract the module body** — match the opener, count braces (respecting strings) until the matching `}`.
3. **Wrap as a standalone arrow** — write `(e, t, n) => { <body> }` to a temp file. humanify needs valid top-level code.
4. **Run humanify** — `script -q /dev/null npx humanifyjs gemini -m "gemini-3.1-flash-lite-preview" -o "$OUT" "$IN"`.
5. **Unwrap** — strip the `(e, t, n) => { ... }` shell back off the result.
6. **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`.

# Workflow C: Webpack bundle → per-module files

When dealing with a webpack bundle, [webcrack](https://github.com/j4k0xb/webcrack) splits modules cleanly before humanify even runs:

```bash
npx webcrack bundle.js -o webcracked/
# Then humanify each module of interest:
script -q /dev/null npx humanifyjs gemini \
  -m "gemini-3.1-flash-lite-preview" \
  -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:**
1. Check for sourcemap: `head -c 4096 bundle.js | tail -c 200` — look for `sourceMappingURL`. None? Continue.
2. Confirm `GEMINI_API_KEY` in `.env`. Source it: `set -a; source .env; set +a`.
3. Archive: `mkdir -p archive && cp bundle.js archive/bundle.original.js`.
4. Run: `mkdir -p out && script -q /dev/null npx humanifyjs gemini -m "gemini-3.1-flash-lite-preview" -o out bundle.js`.
5. 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:**
1. If the project already has a deobfuscation helper (e.g. `scripts/deobfuscate.js`), run it: `node scripts/deobfuscate.js 3658 treeGenerator`.
2. Otherwise: extract the module body by brace-counting, wrap as `(e, t, n) => { ... }`, run humanify with `script -q /dev/null`, unwrap, save to `modules/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).

# Troubleshooting

## `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.

Related in AI Agents