A Claude Code agent skill that makes minified,
bundled, or obfuscated JavaScript readable again — by renaming identifiers scope-safely
with an AST while the model supplies the names. It reaches the parts plain renaming
can't: webpack export-ids (.a/.b) and TypeScript class-IIFE constructors, with a
deterministic backbone and a convergence loop for large bundles.
It's the from-scratch reimplementation of humanify's
technique: a script owns correctness (Babel scope.rename), the model owns naming.
Renaming minified JS by hand or with find-and-replace silently corrupts code — shadowed variables merge, names collide. This skill routes every rename through an AST so it's always safe, and it has been run end-to-end on a real 1 MB / 124-module / ~10k-identifier webpack bundle, cutting single-letter identifier tokens by ~63% while keeping the output byte-for-byte behavior-equivalent.
Requires Node.js. Install with the skills CLI:
npx skills add ajsb85/humanify-deobfuscate -gOr clone manually into your agent's skills directory (~/.claude/skills/ for Claude Code),
then install the Babel dependencies:
cd humanify-deobfuscate/scripts && npm installJust ask your agent in natural language — the skill triggers on minified .js,
"deobfuscate", "un-minify", "make this readable", "what does this minified code do", etc.
For example:
"Deobfuscate
vendor.min.jsand tell me what theparseConfigmodule does."
node scripts/extract_identifiers.mjs input.js > identifiers.json # AST → bindings + context
# (the model names them into renames.json keyed by byte offset)
node scripts/apply_renames.mjs input.js renames.json readable.js # scope-safe rename
node --check readable.jsnode scripts/split_bundle.mjs bundle.js list # survey modules
node scripts/split_bundle.mjs bundle.js dump work 73 102 # dump per module
# (one agent per module names its bindings -> work/mod_<idx>.renames.json)
node scripts/split_bundle.mjs bundle.js merge work renames_all.json
node scripts/finish_bundle.mjs bundle.js renames_all.json readable.js # deterministic backboneSee SKILL.md for the full workflow, including the parallel-agent scaling
pattern and the convergence loop.
| Script | Role |
|---|---|
extract_identifiers.mjs |
AST → renameable bindings + context, keyed by byte offset |
apply_renames.mjs |
scope-safe rename (scope.rename) + codegen |
split_bundle.mjs |
bundle mode: list / dump / chunk / merge |
rename_exports.mjs |
post-pass: webpack export-ids (.a/.b → real names) |
detect_ts_classes.mjs |
post-pass: TS class-IIFE inner ctors + super params |
extract_remaining.mjs |
convergence loop: surface remaining nameable bindings |
finish_bundle.mjs / .sh |
one-command deterministic backbone |
The model never edits code — it only suggests one name at a time, and an AST applies it
across the binding's whole scope. The rest of this document is the full theory,
architecture, and runbooks. See also references/algorithm.md
and references/agent-prompt.md.
Text version of the pipeline
bundle.js ──▶ parse (Babel) ──▶ AST T + scope tree ──▶ bindings B (keyed by byte offset)
│
┌─────────────────────────────────────────────────────┘
▼
split_bundle.mjs list · dump · chunk ──▶ per-module { src, bindings }
│
▼
naming (you / parallel agents) ──▶ mod_k.renames.json (a map ρ_k)
│
▼
split_bundle.mjs merge ρ = ⨆ₖ ρ_k ──▶ renames_all.json
│
▼
finish_bundle.mjs apply_ρ ▸ rename_exports ▸ detect_ts_classes ▸ verify
│ (α-safe binding) (export ids) (class IIFEs)
▼
readable.js + readable.js.remaining.json
│
▼
convergence loop extract_remaining ▸ name ▸ apply (repeat until gain < ε)
The design separates correctness (owned by deterministic AST scripts) from naming judgment (owned by the model). Everything below makes that split precise.
Let a source program be a string
A rename map is a partial function keyed by offset,
where
i.e. the program is identical up to consistent renaming of bound identifiers, so its
observable behavior is unchanged. This holds because each binding is renamed together
with its whole reference set
Why text replacement is unsound. A naive substitution
Capture avoidance. When applying _), define
the freshening operator over the names visible in the scope,
— i.e. prepend _ until free. This makes the applied name unique in scope, preserving
α-equivalence. (apply_renames.mjs realizes scope.generateUid.)
Normalization. Model output is arbitrary text; a normalizer maps it to a legal
identifier [A-Za-z_$][\w$]* that are not reserved words — collapsing separators to
camelCase, dropping illegal characters, and prefixing _ when the result starts with a
digit or is a reserved word.
1 — Order largest-scope-first. With scope span
Outer, longer-lived names are decided first and inform the inner ones.
2 — Context window. Each binding is presented with a windowed slice of its scope,
budgeted to
clamped to the scope and snapped to UTF‑8 boundaries.
3 — Name, 4 — normalize, 5 — freshen, 6 — apply. The model returns
A webpack/browserify bundle is one file whose module array exposes functions
Each module is named independently into a map
This is exactly what makes the one-agent-per-module fan-out safe to run fully in
parallel — see references/agent-prompt.md.
Webpack export-ids (rename_exports.mjs). Webpack mangles each module's exports to
single letters via __webpack_require__.d(exports, "a", () => X). Define the per-module
export map
For any import binding point.x is never altered).
TypeScript class IIFEs (detect_ts_classes.mjs). tsc lowers class X extends B {}
to var X = (function (_super) { __extends(e, _super); function e(){} return e })(B). The
holder X gets named but the inner constructor e and _super stay cryptic. The pass
recognizes this shape and emits _super) applied through the same α-safe machinery.
After the deterministic passes, let
Because
Coverage. With
On a real TypeScript-compiled bundle this saturates near __extends/__values
temporaries) and is intentionally left unnamed — naming it reduces readability.
The whole pipeline is the composition
with finish_bundle.mjs runs in order.
| Script | Input → Output | Role |
|---|---|---|
extract_identifiers.mjs |
file.js → {identifiers:[{pos,name,kind,line,context}]}
|
build |
apply_renames.mjs |
file.js, out.js
|
α-safe apply: scope.rename + freshening |
split_bundle.mjs |
bundle.js list|dump|chunk|merge
|
module survey / per-module dump / chunking / disjoint merge |
rename_exports.mjs |
file.js → file.js
|
rewrite |
detect_ts_classes.mjs |
file.js → |
emit class-IIFE rename map |
extract_remaining.mjs |
file.js → {remaining:[…]}
|
surface |
finish_bundle.mjs / .sh
|
bundle.js, readable.js
|
run |
All scripts are pure Node + Babel (@babel/parser, @babel/traverse, @babel/generator)
with zero native binaries, so they run identically on Linux/macOS/Windows.
Single file
node scripts/extract_identifiers.mjs input.js > identifiers.json # build B + context
# model writes renames.json : { "<pos>": "<name>", ... }
node scripts/apply_renames.mjs input.js renames.json readable.js # apply_ρ (α-safe)
node --check readable.js # verifyBundle, end to end
node scripts/split_bundle.mjs bundle.js list # survey modules (size, bindings, domain hits)
node scripts/split_bundle.mjs bundle.js dump work 73 102 … # per-module {src,bindings}
node scripts/split_bundle.mjs bundle.js chunk work 400 73 # split giant modules
# one agent per module/chunk → work/mod_<k>.renames.json (ρ_k)
node scripts/split_bundle.mjs bundle.js merge work renames_all.json # ρ = ⨆ ρ_k
node scripts/finish_bundle.mjs bundle.js renames_all.json readable.js # apply_ρ ▸ exp ▸ ts ▸ verifyConvergence loop (repeat until a round names
node scripts/rename_exports.mjs readable.js readable.js
node scripts/detect_ts_classes.mjs readable.js > ts.json
node scripts/apply_renames.mjs readable.js ts.json readable.js
node scripts/extract_remaining.mjs readable.js > remaining.json # S_j (name the real ones, skip scaffolding)SKILL.md carries YAML frontmatter — a kebab-case name and a trigger-rich
description — which Claude indexes. Progressive disclosure keeps the body lean and
defers detail to references/ and the scripts. The skill fires on requests like
"deobfuscate this bundle," "make this minified file readable," or "what does
function a(e,t){…} do," then drives the procedures above. Install:
npx skills add ajsb85/humanify-deobfuscate -gMIT © Alexander Salas Bastidas