Deobfuscate JavaScript
Reverse-engineer minified/obfuscated JavaScript back to readable code. Three stages: Stage 1 (only if obfuscated) and Stage 2 (rename + polish) do the readable restore; Stage 3 adds the typed rewrite + acceptance review and is the deep-tier add-on.
- Stage 1 — Deobfuscation: unwind obfuscation transforms (Packer/AAEncode unpack, Obfuscator.IO string arrays, hex/unicode/base64 escapes, dead code, opaque predicates). Pure Babel passes, no LLM. Run only on obfuscated input. Detail: stage-1-deobfuscate.md.
- Stage 2 — Restore to readable (two phases). (a) Rename cryptic identifiers (
a, e, t, _0x4f3, …) to meaningful names — a TS+Bun port of humanify where you play the renamer, running smart-rename.ts first so you only hand-name the residue. (b) Polish undoes bundler/compiler transforms — the reading-aid subset (--fast: React Compiler cache[N] strip, (0, fn)(...) unwrap, backtick → string, JSX-runtime → JSX, destructure defaults, aliased exports) is the default; the import-resolution tail (npm/shim/dead-shim elimination) is deep mode only. Detail: stage-2-restore.md.
- Stage 3 — Finalize (deep mode only) (two phases). (a) Semantic rewrite turns the mechanical checkpoint into idiomatic typed TypeScript — semantic filenames, provenance header, import map, types/
.tsx recipes, directory splits, import repair. (b) Acceptance review — the host reads every delivered file end-to-end against the four-category quality bar and iterates until each passes; no sub-agent and no authorization required (an independent reviewer sub-agent is optional). The default tier ships the Stage 2 readable file without Stage 3. Detail: stage-3-finalize.md.
Default scope and depth
Two independent axes: scope (one file vs the whole import tree) and depth (readable vs deep). Pick scope from the input shape. For a whole-tree restore the default depth is deep ("restore the code" means complete + typed); a lone snippet defaults to readable. Drop a whole tree to readable only on an explicit "quick"/"readable"/"快速" request.
- Default scope — whole tree from
index.html. For an app (index.html + sibling-chunk asset tree), read index.html, auto-discover the entry (check-entry.ts --discover --root <assets-dir>, or omit the positional to build-import-graph.ts), then recursively restore every reachable project-local chunk. Per-chunk pipeline: Stage 1 (if obfuscated) → wakaru pre-pass (wakaru-normalize.ts; recovers ES6 classes, async/await, optional chaining, destructuring, TS enums, …; default-on, auto-skips if @wakaru/cli absent) → Stage 2 rename (smart-rename first, then hand-name residue) → reading-aid polish (polish.ts --fast) → format → organize in staging, then promote into shared restored/ (semantic subfolders, kebab filenames, provenance headers, one restored/IMPORT_MAP.json); script/batch output never goes straight into restored/. Bundled vendor data (Shiki grammars + themes, data libs like 3Dmol) is detected by content and treated as npm-leaf — NOT restored; consumers import the bare specifier (@shikijs/langs/<id>, @shikijs/themes/<id>), so restored/ holds app code only. Readable depth's hard bar is naming quality; deep depth adds complete types + every reachable chunk promoted.
- Fallback scope — single file. When the input is a lone pasted snippet, or there is no
index.html / asset tree, restore just that one file — workflows/small-minified.md. Same per-file pipeline, no import graph.
- Depth — deep (default for whole tree) vs readable (quick opt-out). Deep = meaningful names plus Stage 3 (typed
.tsx + acceptance review), full npm-import resolution (Stage 2's deep polish tail), and import-graph/ledger orchestration draining every reachable chunk to promoted; also triggered on "deep"/"full"/"完整"/"深度"/"production"/"typed"/"restore the whole tree". Readable = meaningful names, reading-aid polish, untyped — only on "quick"/"readable"/"快速"/"粗略" or a lone snippet. Both organize and promote; promote-organized.ts defaults --tier deep (enforces Props/param types), --tier readable relaxes the typing gate.
Deep is a completion bar, not an upsell: a whole-tree restore is "done" only when every reachable project-local chunk reaches stages.promoted and quality-gate.ts <target> passes (Restoration contract). Readable is a downgrade the user asks for, not the resting state.
Output conventions
restored/ is a clean-only deliverable zone — the canonical staging → organize → promote rule (referenced everywhere below). Anything a batch or script restore emits (auto-restore-full.ts checkpoints, a swept polish.ts, --write-target-checkpoints files, any hash-basename .tsx) is a mechanical checkpoint, not a deliverable; it goes into the gitignored staging tree under restored/.deobfuscate-javascript/ (_full/checkpoints/<basename>.tsx for the batch executor, per-chunk $WS/ otherwise), never straight into restored/. Promote only after the host organizes it. The promotion bar, in every tier: good readability (semantic names, no mechanical fallbacks like buttonValue3 / contextParam14), friendly kebab filenames, clear semantic-domain structure, and prettier-formatted (promote-organized.ts runs format.ts on every deliverable, so restored/ is never raw @babel/generator output) — plus complete types (Props interfaces on exported components, param/return types where they help) whenever the restore is deep/full (where all auto-restore-full.ts output lives). Only genuinely hand-restored single readable-tier deliverables may promote untyped, and even those draft in $WS, get organized, then land — never raw script output. For a whole-tree batch restore this is driven, not eyeballed: plan-organize.ts proposes a domain + kebab path per chunk; promote-organized.ts drains the promote frontier (typed deliverable → quality gate → copy into restored/<domain>/ → IMPORT_MAP update → import rewrite). Complete only when every reachable chunk reaches stages.promoted and quality-gate.ts <target-dir> passes — see full-restoration.md → Step 4.
- One shared restore root —
restored/ — mirrors the source assets dir (e.g. ref/webview/assets). Every entry restores into it; no per-entry folder (no restored/app-main/).
- Semantic-domain subfolders group output (
app-shell/, composer/, utils/, icons/, …); original chunk identity lives in each file's provenance header, not the layout.
- One shared import map —
restored/IMPORT_MAP.json — reused regardless of entry. Never per-chunk/per-session/per-entry maps (no APP_MAIN_IMPORT_MAP.json).
- kebab-case file and directory names (
button.tsx, download-icon.tsx, app-shell/), but React component + type names stay PascalCase (JSX requires it) — button.tsx exports Button, download-icon.tsx exports DownloadIcon. quality-gate.ts enforces this (non-kebab-filename).
While restoring, if you hit a script bug, a step worth automating, or an unrecognized npm package, update the skill itself and commit it separately — see Maintaining this skill.
Vendor/npm hard stop
Any task that will create or edit restored/vendor/* (including nested compat-*, stable-exports/*, or runtime barrels) is gated. Before the first edit, read reference/vendor-npm.md in full and run the directory audit:
bun .agents/skills/deobfuscate-javascript/scripts/vendor-npm-preflight.ts restored/vendor
The non-negotiable core: public vendor files are npm-first — the default deliverable is a thin npm-backed re-export shim, never a hand-written "compatible subset" of a stock package. A missing dependency means add it to the nearest package.json, not evidence for a local body. The intent gate (vendor-npm-preflight.ts <target> --decision --intent npm-shim|local-body) must exit 0 before writing code; public vendor files fail closed to npm-shim unless recorded Codex-fork or app/runtime-wrapper proof exists. The full evidence order, touch protocol, aggregator-barrel rules, and shim recipe live in reference/vendor-npm.md.
Restoration contract
Default tier (readable restore)
- Find the entry (whole tree is the default). With an
index.html + asset tree, auto-discover: bun scripts/check-entry.ts --discover --root <assets-dir> (or omit the positional to build-import-graph.ts). Restore every reachable project-local chunk (per-chunk steps run across the tree, orchestrated by full-restoration.md). No index.html/tree (a lone snippet) → single-file flow, small-minified.md.
- Analyze first. Always run
sourcemap-check; use detect / extract per chunk.
- Restore to readable. Stage 1 if obfuscated → wakaru-normalize (
bun scripts/wakaru-normalize.ts "$WS/original.js" -o "$WS/normalized.js"; auto-skips when absent, always leaves a normalized.js) → Stage 2 rename (smart-rename first, then hand-name residue until single-letter density is low) → reading-aid polish (polish.ts --rename --fast) → format. One-shot: polish.ts "$WS/normalized.js" --rename --fast --source <original-path> --out draft.tsx --format (file operand is the normalized output; --source points at the original for the provenance header); then hand-name what smart-rename left.
- Stage, organize, then promote (canonical rule: Output conventions). The one-shot writes its draft into
$WS/; promote into restored/ (semantic subfolders, kebab filenames, // Restored from <path> header, shared restored/IMPORT_MAP.json) only after organizing — never copy a mechanical checkpoint in. The hard bar at readable depth is naming quality — meaningful identifiers, no generated fallback names (buttonValue3, contextParam14); types, npm-import resolution, and the reviewer loop stay optional (an optional naming-only self-review is available for extra confidence).
Deep / production mode only
When the user asks for deep/full/typed/production output, continue past the readable draft:
- Build the graph (full mode). Build
manifest.json + ledger.json with build-import-graph.ts and build-symbol-ledger.ts so renaming is resumable and parallel-safe across the chunk tree.
- Create a mechanical checkpoint. Stage 2 scripts or
auto-restore-full.ts produce a broad readable checkpoint (may keep hash basenames + fallback names; never the final deliverable). wakaru-normalize may run per chunk body here too, with two guards: never --unpack an already-split tree (it re-derives module boundaries, forking the restore root), and treat wakaru's import/export specifiers as untrusted — build-import-graph.ts must rebuild the manifest from on-disk files after wakaru, and resolve-npm-imports.ts + CHUNK_NAME_REGISTRY stay the authoritative rewriter (wakaru's un_esm doesn't substitute).
- Rewrite semantically as the host agent. Read the checkpoint with the original source, graph/ledger, producer/consumer imports, and project conventions; rewrite into maintainable code — meaningful filenames/identifiers, typed props, semantic exports, resolved npm imports, clear structure.
- Pre-filter only. Run prettier and
quality-gate.ts for cheap failures. Passing does not mean done.
- Acceptance-review LOOP. The host agent reads every delivered file end-to-end against the four-category quality bar in
scripts/acceptance-checklist.md, rewrites each NEEDS_FIX, and re-reads until all pass — no sub-agent and no authorization required. An independent reviewer sub-agent is optional (extra eyes only, never a blocker, never fabricate verdicts or skim). Complete only after every file passes.
- Full-target completion audit. For whole-tree restores, run
quality-gate.ts <target-dir> after the loop. It checks the manifest + shared import map and fails if any reachable local/oversized-local app-feature chunk is still missing, mechanical, @ts-nocheck, empty placeholder, typed facade, or lacks Stage 3 acceptance evidence. Not a boundaries/ grep or IMPORT_MAP.status === "done" scan.
Dual-track output (deep mode): hidden checkpoints preserve original hash basenames; public files use semantic names, with chunk identity in provenance headers and the one shared restored/IMPORT_MAP.json (reuse + append; see Output conventions).
Full restoration mode is not a new stage — it's an orchestration layer on top of Stages 1–3 coordinating the per-file pipeline across a connected component of the import graph (entry chunk + every sibling it pulls in, transitively to the leaves). It adds manifest.json (file-level dependency graph + stage status) and ledger.json (per-file symbol checklist + cross-file binding table) so renaming is resumable and parallelizable at file granularity. "deep"/"full"/"完整"/"深度"/"restore referenced chunks" ⇒ every reachable project-local sibling is in scope by default. Detail: workflows/full-restoration.md.
Delta / boundary replacement inside an existing restore
When the target restore root already has a shared IMPORT_MAP.json and .deobfuscate-javascript/_full/manifest.json, first check whether the requested chunk already maps to a public file or typed boundary facade. If it does, treat the task as a delta restore unless the user explicitly asks to rebuild the whole reachable graph.
Delta restore contract:
- Reuse the existing target, manifest, ledger, and import map; no parallel restore root.
- Restore the requested chunk in its existing
_full/files/<basename>/ workspace. Restore additional chunks only when missing from the import map or explicitly requested.
- Rewrite imports through the shared
IMPORT_MAP.json: finalized local deps → semantic public paths; npm deps → bare specifiers; unresolved runtime/vendor deps → documented boundary facade paths.
- Replace the mapped boundary/public file with the semantic candidate; update the import-map entry (
restored, exports, status, drop boundary when it's no longer a facade).
- Validate: format,
quality-gate.ts on changed public path(s), the target TypeScript check when the root has a tsconfig.json, and Stage 3 acceptance on changed public files only. All-tree acceptance only when the whole public tree changed or the user asked.
Vendor/npm preflight — before restoring restored/vendor/*
Blocking preflight; the full protocol (evidence order, intent gates, aggregator-barrel resolution, the npm-shim deliverable recipe) is in reference/vendor-npm.md — read it before any restored/vendor/* edit. See Vendor/npm hard stop above for the audit command.
Boundaries and terminal nodes
Full-restoration mode records npm-leaf, oversized-local, external, and faced-boundary chunks as terminal nodes — referenced by consumers but never renamed or given a workspace. restored/boundaries/*.ts has exactly three terminal states — known npm package → bare re-export shim (done), genuine app/host runtime → typed facade or opt-in passthrough (an open boundary, never done), restore + promote out of boundaries/ (the only completion for runtime facades) — a permanent any-facade is never acceptable, and project/feature chunks are never faced ("big" or "imported by many" is not a facing reason). Deep/full mode recurses the entire reachable project-local graph to the leaves; final proof is quality-gate.ts <target-dir> over the whole public target. The terminal-node trigger table, boundary lifecycle detail, and build-import-graph.ts depth flags (--max-lines, --include) are in reference/boundaries.md — read it when classifying a chunk as a boundary or resolving one.
Run Stage 1 before Stage 2 on obfuscated input — extract.ts byte offsets are invalidated by Stage 1 rewrites. On purely minified (not obfuscated) input, skip Stage 1.
Current project profile — codex-app-code ./ref
When the workspace contains ref/package.json with name: "openai-codex-electron" or the user asks to restore ./ref, load reference/codex-ref.md before choosing a workflow.
Default assumptions for this repo:
- Primary bundled-code root:
ref/webview/assets.
- Main app entry: read
ref/webview/index.html; current builds usually route through index-*.js, app-main-*.js, and app-shell-*.js.
- Default restore root:
restored/ (mirrors ref/webview/assets), with semantic-domain subfolders (app-shell/, composer/, utils/, icons/). Reuse the single shared restored/IMPORT_MAP.json before creating any new map. Do not create a per-entry folder such as restored/app-main/. Auto-discover the entry with scripts/check-entry.ts --discover --root ref/webview/assets.
- Treat
ref/node_modules/**, ref/native-menu-locales/**, CSS, WASM, images, and obvious vendor/data chunks as references or terminal boundaries unless the user explicitly asks to restore them.
- Vendored UI packages →
@pierre/trees / @pierre/diffs boundaries, not app code. The file-tree and diff-view subsystems are bundled @pierre/trees (Preact) + @pierre/diffs (Shiki). Codex FORKED them (React fork of @pierre/trees; settings-entangled @pierre/diffs gate), so a clean bare-import swap is usually infeasible — keep the forked wrapper, relabel provenance, boundary-ize (quality-gate.ts --vendored); only self-contained leaves are clean bare imports (worker-* → @pierre/diffs/worker, parsePatchFiles-* → @pierre/diffs). Fingerprints: --trees-*/--diffs-* CSS vars, data-file-tree-*/data-diffs-* attrs, pierre-light/dark(-soft) themes, the @pierre/truncate css here… comment. Engine chunks: file-tree-search-input-*, shiki-highlight-provider-gate-*, file-diff-*, diff-unified-*, parsePatchFiles-*, worker-*. NOT-Pierre traps: diff-stats-*, diff-view-mode-*, use-diff-annotations-* (PDF.js), parse-diff-* (Codex), diff-* grammar (Shiki), treeView-SZITEDCU-*/treemap-* (Mermaid), worktree-* (git). Detail: codex-ref.md → Boundary classification.
- Before starting fresh, inspect existing
restored/**/README.md, IMPORT_MAP.json, .deobfuscate-javascript/_full/manifest.json, and .deobfuscate-javascript/_full/ledger.json; resume rather than duplicating semantic modules.
When to use this skill
Trigger whenever the user:
- Provides a
.min.js, bundle.js, dist/ file, or any JS with cryptic short names and wants to understand or work with it.
- Provides obviously obfuscated code:
_0x identifiers, eval(function(p,a,c,k,e,d){...}), AAEncode emoji glyphs, String.fromCharCode(72,101,...) walls, hex/unicode-escaped literals.
- Says "humanify", "deobfuscate", "deminify", "unminify", "unpack", "decode", "make readable", "rename variables", "reverse engineer"; or in Chinese "反混淆", "美化代码", "看懂这段 JS", "重命名变量", "反编译".
- Asks "what does bundle.js do?" while pointing at minified/obfuscated code, or works in a project with a
modules//og/ folder of previously deobfuscated chunks.
Don't trigger if the code is already readable — just read and explain. Nor for plain beautification — npx prettier --write <file> when there are no cryptic names and no obfuscation. wakaru alone is not a substitute — npx @wakaru/cli un-minifies but its smart_rename is the same deterministic heuristic as smart-rename.ts, not semantic naming; use it as a pre-pass inside Stage 2, not as the deliverable.
Routing — three questions, then load one workflow
Step 0 (always): run scripts/sourcemap-check.ts first. If a .map exists, recover originals via npx source-map-explorer instead — that beats any rename. For the current repo's ./ref tree, load reference/codex-ref.md first.
Step 0.5 (whole tree / ./ref / any multi-chunk app — the default): auto-discover the entry: scripts/check-entry.ts --discover --root <assets-dir> reads index.html, picks + sanity-checks the app entry, prints its path (build-import-graph.ts does the same when you omit the positional). If discovery (or manual check-entry.ts <entry> --root <assets-dir>) exits 3, the entry is a transitive vendor leaf, not the app — restoring from it yields a tiny closure that looks complete (the "got 6 files, called it done" trap). A real app entry has large local fan-out and ~nobody imports it; a leaf is the inverse. Switch to the index.html <script> root (or a high-fan-out app-main-* chunk) first.
Step 0.6 (existing restore delta): if the target has IMPORT_MAP.json + _full/manifest.json, check whether the requested chunk is already a boundary/public output — prefer the delta/boundary-replacement contract above before a new whole-tree restore. "Complete" then means complete replacement for that scoped chunk unless the user asks to rebuild the reachable graph.
Step 0.7 (mechanical normalization — readable tier, default-on): on non-obfuscated minified/transpiled input, run wakaru-normalize.ts before extract.ts. Byte-rewriting like Stage 1, so the order is sourcemap-check → detect → (Stage 1 if obfuscated) → wakaru-normalize → extract — extract/rename from the normalized output, never original.js. Skip when a usable .map exists (recover via source-map-explorer). Auto-skips when @wakaru/cli is absent. In deep/full mode it's guarded, not default-on (Restoration contract → deep step 2).
Then answer in order:
| Question |
Then |
Obfuscated? (_0x arrays, Packer, AAEncode, hex/unicode walls, opaque predicates) |
Stage 1 first via full-obfuscation.md, then continue below |
Has an index.html + asset tree? (the default) — auto-discover the entry, restore the whole reachable tree |
whole-tree restore → full-restoration.md at deep depth by default (typed .tsx + acceptance + drain every chunk to promoted); only "quick"/"readable"/"快速" downgrades to the untyped readable pass |
Otherwise — a lone pasted snippet or single chunk, no index.html/tree |
fallback: single file → small-minified.md |
The single-file fallback workflow handles the sub-cases inline (it links the right technique): React/JSX → recover JSX in --fast polish; ≥ 3 exports / registry → multi-export-bundle.md split (a deep-tier step); ≥ 500 KB or > 1000 symbols → huge-single-file.md batching; webpack id:(e,t,n)=>{} modules → webpack-bundle.md webcrack pre-split; stdin/stdout chaining → piped.md. Load a technique doc only when its sub-case applies.
For end-to-end worked traces, see reference/examples.md. For caveats, gotchas, and troubleshooting, see reference/caveats.md.
Quality bar — anti-patterns to refuse before declaring done
Before declaring any restore done, read reference/quality-bar.md and check the output against it. The hard bar in every tier is naming/readability: no program-scope-only renames (bodies still let k = …, [A, M] = useState(false)), no generated fallback names (buttonValue3, contextParam14), no React Compiler scaffolding in load-bearing functions, no lowercase JSX tags that are really component aliases, no known npm package rewritten as a local implementation, no mechanical/batch checkpoint promoted into restored/, no bundle body merely moved/renamed into a semantic folder, and never checkpoints-built-but-nothing-promoted. Deep/typed/production output additionally must pass the typing/structure list (typed .tsx, split multi-export bundles, no faced app chunks, Stage 3 acceptance actually performed) — all itemized with cures in reference/quality-bar.md.
Tools
Every script's purpose + "Run when" routing, external-tool notes, the shared import-map convention, and exit codes live in reference/tools.md — load it when you need a flag or aren't sure which script fits. The ones used in nearly every run:
| Tool |
Role |
scripts/sourcemap-check.ts |
Always first — a recoverable .map beats any rename |
scripts/check-entry.ts |
--discover --root <assets-dir> finds + sanity-checks the app entry (exit 3 = vendor leaf, wrong entry) |
scripts/build-import-graph.ts |
BFS the chunk tree → _full/manifest.json (full-restoration mode) |
scripts/wakaru-normalize.ts |
Default-on mechanical pre-pass (readable tier); auto-skips when absent |
scripts/polish.ts |
One-shot Stage 2: --rename --fast --format is the readable-tier pipeline; full chain (no --fast) resolves npm imports in deep |
scripts/quality-gate.ts |
Hard gate before promote; quality-gate.ts <target-dir> --check-format is the whole-tree completion proof |
scripts/vendor-npm-preflight.ts |
Blocking guard for any restored/vendor/* edit — see Vendor/npm hard stop |
scripts/format.ts |
Prettier wrapper; every deliverable is formatted |
Workspace convention (TL;DR)
Every intermediate file lives in a hidden per-chunk workspace under the target output directory:
INPUT=ref/webview/assets/spinner-D37df5tU.js
TARGET=restored
WS="$TARGET/.deobfuscate-javascript/$(basename "$INPUT" .js)"
mkdir -p "$WS"
cp "$INPUT" "$WS/original.js"
Then run every script against $WS/original.js and write every output back into $WS/. Everything a script or batch run emits stays in the .deobfuscate-javascript/ staging tree ($WS/ per chunk, _full/checkpoints/ for the batch executor); $TARGET/ (restored/) receives only organized deliverables — this is the staging → organize → promote rule in full at Output conventions. Add .deobfuscate-javascript/ to .gitignore — one entry covers every chunk in the target.
For full restoration mode, a shared _full/ subdir sits next to the per-chunk ones, holding manifest.json, ledger.json, and locks/ so chunks coordinate as one tree. The public import map is shared at the restore root, not inside each $WS. Full convention — multi-module sub-workspaces, plan files, split-plan layout — at stages/workspace.md.
Maintaining this skill (self-improvement protocol)
The skill is a living asset. When a run surfaces (1) a script bug or misfiring gate, (2) a manual step worth automating, or (3) a new/missed npm package identity, fix the skill itself — registry entry, script + test, or doc — and commit it separately from restoration output (skill(deobfuscate-javascript): <what changed>). The finding→home routing table, safety guard for mid-run edits, and commit discipline are in reference/maintenance.md — read it before making the skill change.
Files in this skill
Load sub-files on demand — each entry says when.
Routing & overviews
- SKILL.md — this file. Routing + contracts + workspace TL;DR; always loaded.
- reference/codex-ref.md — project profile for restoring this repo's extracted Codex.app
./ref tree. Load before any ./ref work.
- reference/vendor-npm.md — vendor/npm preflight, intent gates, touch protocol, shim recipe. Load before any
restored/vendor/* edit.
- reference/boundaries.md — terminal-node kinds, boundary lifecycle, graph depth flags. Load when classifying or resolving a boundary.
- reference/quality-bar.md — the anti-pattern checklist. Load before declaring any restore done.
- reference/tools.md — every script with purpose + "Run when", external tools, import-map default, exit codes. Load when picking a tool or flag.
- reference/maintenance.md — self-improvement protocol detail. Load when routing a discovered fix back into the skill.
- stages/workspace.md — the
$WS convention in full.
- reference/examples.md — nine end-to-end worked traces.
- reference/caveats.md — gotchas, ordering rules, troubleshooting FAQ.
- reference/naming-heuristics.md — the mechanical rename rules (React props, event handlers, iteratee callbacks, hook returns) that
smart-rename.ts automates.
- reference/bundler-runtimes.md — esbuild vs Rolldown vs Vite runtime helper signatures (
__esm/__commonJS/__export/__toESM/__require); read before hand-converting __export/__esm call sites.
Per-stage detail
- stages/stage-1-deobfuscate.md — detect → unpack → string-array → decode-strings → simplify → control-flow-report → orchestrator, + ordering rules.
- stages/stage-2-restore.md — Phase A (rename): sourcemap-check → setup → webcrack → extract → decide names → Step 2.5 → apply → verify. Phase B (polish): strip-react-compiler → simplify → jsx-runtime → inline-defaults → normalize-exports → (deep-mode import tail) →
polish.ts → format.
- stages/stage-3-finalize.md — deep mode only. Phase A (rewrite): D0 gate → D0.5 filenames → D1 provenance → D2 imports → D3–D4 cleanup → D5 typed
.tsx → D6 format → D7 gate. Phase B (acceptance): read each file E1 naming / E2 readability / E3 formatting / E4 other → rewrite → re-read until pass (sub-agent optional). Default tier: optional naming-only (E1) self-review.
Workflows by scenario
- workflows/full-restoration.md — entry chunk + every transitive sibling chunk → coordinated restoration tree (resumable across sessions, parallel across agents).
- workflows/small-minified.md — < 500 KB, no obfuscation, direct rename.
- workflows/full-obfuscation.md — Stage 1 + Stage 2 end-to-end (includes eval-gating for Packer-wrapped untrusted input).
- workflows/webpack-bundle.md — webcrack pre-split, rename per module.
- workflows/huge-single-file.md —
plan.ts + checklist + batched renames.
- workflows/multi-export-bundle.md — ESM chunks with many exports → directory of small files.
- workflows/react-vite.md — single-component React/Vite/Rollup chunks → JSX recovery.
- workflows/piped.md — stdin/stdout chains for quick experimentation.
Scripts
Every script is described once, with its "Run when" routing, in reference/tools.md. All scripts live under scripts/; each behavior-changing script has a sibling scripts/<name>.test.ts.
Tests + fixtures + deps
scripts/*.test.ts — bun test suite (run from skill dir).
fixtures/ — one per technique (packed.dean-edwards.min.js, aaencode.min.js, url-encoded.min.js, string-array-obfuscator.min.js, from-char-code.min.js, dead-code.min.js, opaque-predicates.min.js, control-flow-flat.min.js, composite.min.js, example.min.js).
- package.json — bun deps (
@babel/parser, @babel/traverse, @babel/generator, @babel/types).
1---2name: deobfuscate-javascript3description: Reverse-engineer minified, obfuscated, packed, or bundled JavaScript into readable or typed TS/TSX, including Codex.app `./ref/webview/assets` restoration. Use for .min.js, dist bundles, Vite/Rollup/Webpack chunks, obfuscated snippets, or requests to humanify, deobfuscate, deminify, unpack, decode, make readable, rename variables, reverse engineer, 完整还原, 深度还原, 反混淆, 美化代码, 看懂这段 JS, 重命名变量, 反编译. Whole-tree restores (index.html + assets, "restore the code/whole tree") default to deep: deobfuscate if needed, smart-rename, polish, resolve npm imports, typed semantic TSX rewrite, acceptance review, import graph/ledger, and full-target quality gate draining every reachable project-local chunk. Quick/readable/快速 or lone snippets use readable naming-quality restore. Vendor data/packages become npm leaves or re-export shims, not restored package bodies.4---56# Deobfuscate JavaScript78Reverse-engineer minified/obfuscated JavaScript back to readable code. Three stages: **Stage 1** (only if obfuscated) and **Stage 2** (rename + polish) do the readable restore; **Stage 3** adds the typed rewrite + acceptance review and is the deep-tier add-on.910- **Stage 1 — Deobfuscation:** unwind obfuscation transforms (Packer/AAEncode unpack, Obfuscator.IO string arrays, hex/unicode/base64 escapes, dead code, opaque predicates). Pure Babel passes, no LLM. Run only on obfuscated input. Detail: [stage-1-deobfuscate.md](stages/stage-1-deobfuscate.md).11- **Stage 2 — Restore to readable** (two phases). **(a) Rename** cryptic identifiers (`a`, `e`, `t`, `_0x4f3`, …) to meaningful names — a TS+Bun port of [humanify](https://github.com/jehna/humanify) where _you_ play the renamer, running `smart-rename.ts` **first** so you only hand-name the residue. **(b) Polish** undoes bundler/compiler transforms — the **reading-aid subset** (`--fast`: React Compiler `cache[N]` strip, `(0, fn)(...)` unwrap, backtick → string, JSX-runtime → JSX, destructure defaults, aliased exports) is the default; the **import-resolution tail** (npm/shim/dead-shim elimination) is **deep mode only**. Detail: [stage-2-restore.md](stages/stage-2-restore.md).12- **Stage 3 — Finalize (deep mode only)** (two phases). **(a) Semantic rewrite** turns the mechanical checkpoint into idiomatic _typed_ TypeScript — semantic filenames, provenance header, import map, types/`.tsx` recipes, directory splits, import repair. **(b) Acceptance review** — the host reads every delivered file end-to-end against the four-category quality bar and iterates until each passes; no sub-agent and no authorization required (an independent reviewer sub-agent is optional). The default tier ships the Stage 2 readable file without Stage 3. Detail: [stage-3-finalize.md](stages/stage-3-finalize.md).1314## Default scope and depth1516Two independent axes: **scope** (one file vs the whole import tree) and **depth** (readable vs deep). Pick scope from the input shape. **For a whole-tree restore the default depth is deep** ("restore the code" means complete + typed); a lone snippet defaults to readable. Drop a whole tree to readable only on an explicit "quick"/"readable"/"快速" request.1718- **Default scope — whole tree from `index.html`.** For an app (`index.html` + sibling-chunk asset tree), read `index.html`, **auto-discover the entry** (`check-entry.ts --discover --root <assets-dir>`, or omit the positional to `build-import-graph.ts`), then **recursively restore every reachable project-local chunk**. Per-chunk pipeline: Stage 1 (if obfuscated) → **wakaru pre-pass** (`wakaru-normalize.ts`; recovers ES6 classes, async/await, optional chaining, destructuring, TS enums, …; default-on, **auto-skips** if `@wakaru/cli` absent) → Stage 2 rename (`smart-rename` first, then hand-name residue) → reading-aid polish (`polish.ts --fast`) → format → **organize in staging, then promote** into shared `restored/` (semantic subfolders, kebab filenames, provenance headers, one `restored/IMPORT_MAP.json`); script/batch output never goes straight into `restored/`. **Bundled vendor data (Shiki grammars + themes, data libs like 3Dmol) is detected by content and treated as `npm-leaf` — NOT restored; consumers import the bare specifier (`@shikijs/langs/<id>`, `@shikijs/themes/<id>`), so `restored/` holds app code only.** Readable depth's hard bar is naming quality; deep depth adds complete types + every reachable chunk promoted.19- **Fallback scope — single file.** When the input is a lone pasted snippet, or there is no `index.html` / asset tree, restore just that one file — [workflows/small-minified.md](workflows/small-minified.md). Same per-file pipeline, no import graph.20- **Depth — deep (default for whole tree) vs readable (quick opt-out).** **Deep** = meaningful names **plus** Stage 3 (typed `.tsx` + acceptance review), full npm-import resolution (Stage 2's deep polish tail), and import-graph/ledger orchestration draining every reachable chunk to `promoted`; also triggered on **"deep"/"full"/"完整"/"深度"/"production"/"typed"/"restore the whole tree"**. **Readable** = meaningful names, reading-aid polish, untyped — only on "quick"/"readable"/"快速"/"粗略" or a lone snippet. Both **organize and promote**; `promote-organized.ts` defaults `--tier deep` (enforces `Props`/param types), `--tier readable` relaxes the typing gate.2122Deep is a completion bar, not an upsell: a whole-tree restore is "done" only when every reachable project-local chunk reaches `stages.promoted` and `quality-gate.ts <target>` passes ([Restoration contract](#restoration-contract)). Readable is a downgrade the user asks for, not the resting state.2324## Output conventions2526- **`restored/` is a clean-only deliverable zone — the canonical staging → organize → promote rule (referenced everywhere below).** Anything a _batch or script_ restore emits (`auto-restore-full.ts` checkpoints, a swept `polish.ts`, `--write-target-checkpoints` files, any hash-basename `.tsx`) is a **mechanical checkpoint, not a deliverable**; it goes into the gitignored staging tree under `restored/.deobfuscate-javascript/` (`_full/checkpoints/<basename>.tsx` for the batch executor, per-chunk `$WS/` otherwise), **never** straight into `restored/`. Promote only after the host organizes it. The promotion bar, in **every tier**: **good readability** (semantic names, no mechanical fallbacks like `buttonValue3` / `contextParam14`), **friendly kebab filenames**, **clear semantic-domain structure**, and **prettier-formatted** (`promote-organized.ts` runs `format.ts` on every deliverable, so `restored/` is never raw `@babel/generator` output) — **plus complete types** (`Props` interfaces on exported components, param/return types where they help) whenever the restore is **deep/full** (where all `auto-restore-full.ts` output lives). Only genuinely hand-restored single **readable-tier** deliverables may promote untyped, and even those draft in `$WS`, get organized, then land — never raw script output. For a whole-tree batch restore this is driven, not eyeballed: `plan-organize.ts` proposes a domain + kebab path per chunk; `promote-organized.ts` drains the promote frontier (typed deliverable → quality gate → copy into `restored/<domain>/` → IMPORT_MAP update → import rewrite). Complete only when every reachable chunk reaches `stages.promoted` and `quality-gate.ts <target-dir>` passes — see [full-restoration.md → Step 4](workflows/full-restoration.md).27- **One shared restore root — `restored/`** — mirrors the source assets dir (e.g. `ref/webview/assets`). Every entry restores into it; **no per-entry folder** (no `restored/app-main/`).28- **Semantic-domain subfolders** group output (`app-shell/`, `composer/`, `utils/`, `icons/`, …); original chunk identity lives in each file's provenance header, not the layout.29- **One shared import map — `restored/IMPORT_MAP.json`** — reused regardless of entry. Never per-chunk/per-session/per-entry maps (no `APP_MAIN_IMPORT_MAP.json`).30- **kebab-case file and directory names** (`button.tsx`, `download-icon.tsx`, `app-shell/`), but **React component + type names stay PascalCase** (JSX requires it) — `button.tsx` exports `Button`, `download-icon.tsx` exports `DownloadIcon`. `quality-gate.ts` enforces this (`non-kebab-filename`).3132While restoring, if you hit a script bug, a step worth automating, or an unrecognized npm package, update the skill itself and commit it separately — see [Maintaining this skill](#maintaining-this-skill-self-improvement-protocol).3334## Vendor/npm hard stop3536Any task that will create or edit `restored/vendor/*` (including nested `compat-*`, `stable-exports/*`, or runtime barrels) is gated. **Before the first edit, read [reference/vendor-npm.md](reference/vendor-npm.md) in full** and run the directory audit:3738```bash39bun .agents/skills/deobfuscate-javascript/scripts/vendor-npm-preflight.ts restored/vendor40```4142The non-negotiable core: public vendor files are **npm-first** — the default deliverable is a thin npm-backed re-export shim, never a hand-written "compatible subset" of a stock package. A missing dependency means add it to the nearest `package.json`, not evidence for a local body. The intent gate (`vendor-npm-preflight.ts <target> --decision --intent npm-shim|local-body`) must exit 0 before writing code; public vendor files fail closed to `npm-shim` unless recorded Codex-fork or app/runtime-wrapper proof exists. The full evidence order, touch protocol, aggregator-barrel rules, and shim recipe live in [reference/vendor-npm.md](reference/vendor-npm.md).4344## Restoration contract4546### Default tier (readable restore)47480. **Find the entry (whole tree is the default).** With an `index.html` + asset tree, auto-discover: `bun scripts/check-entry.ts --discover --root <assets-dir>` (or omit the positional to `build-import-graph.ts`). Restore **every reachable project-local chunk** (per-chunk steps run across the tree, orchestrated by [full-restoration.md](workflows/full-restoration.md)). No `index.html`/tree (a lone snippet) → single-file flow, [small-minified.md](workflows/small-minified.md).491. **Analyze first.** Always run `sourcemap-check`; use `detect` / `extract` per chunk.502. **Restore to readable.** Stage 1 if obfuscated → **wakaru-normalize** (`bun scripts/wakaru-normalize.ts "$WS/original.js" -o "$WS/normalized.js"`; auto-skips when absent, always leaves a `normalized.js`) → Stage 2 rename (`smart-rename` first, then hand-name residue until single-letter density is low) → reading-aid polish (`polish.ts --rename --fast`) → `format`. One-shot: `polish.ts "$WS/normalized.js" --rename --fast --source <original-path> --out draft.tsx --format` (file operand is the normalized output; `--source` points at the **original** for the provenance header); then hand-name what `smart-rename` left.513. **Stage, organize, then promote** (canonical rule: [Output conventions](#output-conventions)). The one-shot writes its draft into `$WS/`; promote into `restored/` (semantic subfolders, kebab filenames, `// Restored from <path>` header, shared `restored/IMPORT_MAP.json`) only after organizing — never copy a mechanical checkpoint in. The hard bar at readable depth is naming quality — meaningful identifiers, no generated fallback names (`buttonValue3`, `contextParam14`); types, npm-import resolution, and the reviewer loop stay optional (an optional naming-only self-review is available for extra confidence).5253### Deep / production mode only5455When the user asks for deep/full/typed/production output, continue past the readable draft:56571. **Build the graph (full mode).** Build `manifest.json` + `ledger.json` with `build-import-graph.ts` and `build-symbol-ledger.ts` so renaming is resumable and parallel-safe across the chunk tree.582. **Create a mechanical checkpoint.** Stage 2 scripts or `auto-restore-full.ts` produce a broad readable checkpoint (may keep hash basenames + fallback names; never the final deliverable). wakaru-normalize may run **per chunk body** here too, with two guards: never `--unpack` an already-split tree (it re-derives module boundaries, forking the restore root), and treat wakaru's import/export specifiers as untrusted — `build-import-graph.ts` must rebuild the manifest from on-disk files **after** wakaru, and `resolve-npm-imports.ts` + `CHUNK_NAME_REGISTRY` stay the authoritative rewriter (wakaru's `un_esm` doesn't substitute).593. **Rewrite semantically as the host agent.** Read the checkpoint with the original source, graph/ledger, producer/consumer imports, and project conventions; rewrite into maintainable code — meaningful filenames/identifiers, typed props, semantic exports, resolved npm imports, clear structure.604. **Pre-filter only.** Run prettier and `quality-gate.ts` for cheap failures. Passing does not mean done.615. **Acceptance-review LOOP.** The host agent reads every delivered file end-to-end against the four-category quality bar in `scripts/acceptance-checklist.md`, rewrites each `NEEDS_FIX`, and re-reads until all pass — **no sub-agent and no authorization required**. An independent reviewer sub-agent is optional (extra eyes only, never a blocker, never fabricate verdicts or skim). Complete only after every file passes.626. **Full-target completion audit.** For whole-tree restores, run `quality-gate.ts <target-dir>` after the loop. It checks the manifest + shared import map and fails if any reachable local/oversized-local app-feature chunk is still missing, mechanical, `@ts-nocheck`, empty placeholder, typed facade, or lacks Stage 3 acceptance evidence. Not a `boundaries/` grep or `IMPORT_MAP.status === "done"` scan.6364**Dual-track output (deep mode):** hidden checkpoints preserve original hash basenames; public files use semantic names, with chunk identity in provenance headers and the one shared `restored/IMPORT_MAP.json` (reuse + append; see [Output conventions](#output-conventions)).6566**Full restoration mode** is not a new stage — it's an orchestration layer on top of Stages 1–3 coordinating the per-file pipeline across a connected component of the import graph (entry chunk + every sibling it pulls in, transitively to the leaves). It adds `manifest.json` (file-level dependency graph + stage status) and `ledger.json` (per-file symbol checklist + cross-file binding table) so renaming is resumable and parallelizable at file granularity. "deep"/"full"/"完整"/"深度"/"restore referenced chunks" ⇒ every reachable project-local sibling is in scope by default. Detail: [workflows/full-restoration.md](workflows/full-restoration.md).6768### Delta / boundary replacement inside an existing restore6970When the target restore root already has a shared `IMPORT_MAP.json` and `.deobfuscate-javascript/_full/manifest.json`, first check whether the requested chunk already maps to a public file or typed boundary facade. If it does, treat the task as a **delta restore** unless the user explicitly asks to rebuild the whole reachable graph.7172Delta restore contract:73741. Reuse the existing target, manifest, ledger, and import map; no parallel restore root.752. Restore the requested chunk in its existing `_full/files/<basename>/` workspace. Restore additional chunks only when missing from the import map or explicitly requested.763. Rewrite imports through the shared `IMPORT_MAP.json`: finalized local deps → semantic public paths; npm deps → bare specifiers; unresolved runtime/vendor deps → documented boundary facade paths.774. Replace the mapped boundary/public file with the semantic candidate; update the import-map entry (`restored`, `exports`, `status`, drop `boundary` when it's no longer a facade).785. Validate: format, `quality-gate.ts` on changed public path(s), the target TypeScript check when the root has a `tsconfig.json`, and Stage 3 acceptance on changed public files only. All-tree acceptance only when the whole public tree changed or the user asked.7980### Vendor/npm preflight — before restoring `restored/vendor/*`8182Blocking preflight; the full protocol (evidence order, intent gates, aggregator-barrel resolution, the npm-shim deliverable recipe) is in [reference/vendor-npm.md](reference/vendor-npm.md) — read it before any `restored/vendor/*` edit. See [Vendor/npm hard stop](#vendornpm-hard-stop) above for the audit command.8384### Boundaries and terminal nodes8586Full-restoration mode records `npm-leaf`, `oversized-local`, `external`, and `faced-boundary` chunks as _terminal nodes_ — referenced by consumers but never renamed or given a workspace. `restored/boundaries/*.ts` has exactly three terminal states — **known npm package → bare re-export shim (done)**, **genuine app/host runtime → typed facade or opt-in passthrough (an open boundary, never done)**, **restore + promote out of `boundaries/` (the only completion for runtime facades)** — a permanent `any`-facade is never acceptable, and project/feature chunks are **never** faced ("big" or "imported by many" is not a facing reason). Deep/full mode recurses the **entire** reachable project-local graph to the leaves; final proof is `quality-gate.ts <target-dir>` over the whole public target. The terminal-node trigger table, boundary lifecycle detail, and `build-import-graph.ts` depth flags (`--max-lines`, `--include`) are in [reference/boundaries.md](reference/boundaries.md) — read it when classifying a chunk as a boundary or resolving one.8788Run Stage 1 _before_ Stage 2 on obfuscated input — `extract.ts` byte offsets are invalidated by Stage 1 rewrites. On purely minified (not obfuscated) input, skip Stage 1.8990## Current project profile — `codex-app-code` `./ref`9192When the workspace contains `ref/package.json` with `name: "openai-codex-electron"` or the user asks to restore `./ref`, load [reference/codex-ref.md](reference/codex-ref.md) before choosing a workflow.9394Default assumptions for this repo:9596- Primary bundled-code root: `ref/webview/assets`.97- Main app entry: read `ref/webview/index.html`; current builds usually route through `index-*.js`, `app-main-*.js`, and `app-shell-*.js`.98- Default restore root: `restored/` (mirrors `ref/webview/assets`), with semantic-domain subfolders (`app-shell/`, `composer/`, `utils/`, `icons/`). Reuse the single shared `restored/IMPORT_MAP.json` before creating any new map. Do **not** create a per-entry folder such as `restored/app-main/`. Auto-discover the entry with `scripts/check-entry.ts --discover --root ref/webview/assets`.99- Treat `ref/node_modules/**`, `ref/native-menu-locales/**`, CSS, WASM, images, and obvious vendor/data chunks as references or terminal boundaries unless the user explicitly asks to restore them.100- **Vendored UI packages → `@pierre/trees` / `@pierre/diffs` boundaries, not app code.** The file-tree and diff-view subsystems are bundled `@pierre/trees` (Preact) + `@pierre/diffs` (Shiki). **Codex FORKED them** (React fork of `@pierre/trees`; settings-entangled `@pierre/diffs` gate), so a clean bare-import swap is usually infeasible — keep the forked wrapper, relabel provenance, boundary-ize (`quality-gate.ts --vendored`); only self-contained leaves are clean bare imports (`worker-*` → `@pierre/diffs/worker`, `parsePatchFiles-*` → `@pierre/diffs`). Fingerprints: `--trees-*`/`--diffs-*` CSS vars, `data-file-tree-*`/`data-diffs-*` attrs, `pierre-light/dark(-soft)` themes, the `@pierre/truncate css here…` comment. **Engine chunks:** `file-tree-search-input-*`, `shiki-highlight-provider-gate-*`, `file-diff-*`, `diff-unified-*`, `parsePatchFiles-*`, `worker-*`. **NOT-Pierre traps:** `diff-stats-*`, `diff-view-mode-*`, `use-diff-annotations-*` (PDF.js), `parse-diff-*` (Codex), `diff-*` grammar (Shiki), `treeView-SZITEDCU-*`/`treemap-*` (Mermaid), `worktree-*` (git). Detail: [codex-ref.md → Boundary classification](reference/codex-ref.md).101- Before starting fresh, inspect existing `restored/**/README.md`, `IMPORT_MAP.json`, `.deobfuscate-javascript/_full/manifest.json`, and `.deobfuscate-javascript/_full/ledger.json`; resume rather than duplicating semantic modules.102103## When to use this skill104105Trigger whenever the user:106107- Provides a `.min.js`, `bundle.js`, `dist/` file, or any JS with cryptic short names and wants to understand or work with it.108- Provides obviously obfuscated code: `_0x` identifiers, `eval(function(p,a,c,k,e,d){...})`, AAEncode emoji glyphs, `String.fromCharCode(72,101,...)` walls, hex/unicode-escaped literals.109- Says "humanify", "deobfuscate", "deminify", "unminify", "unpack", "decode", "make readable", "rename variables", "reverse engineer"; or in Chinese "反混淆", "美化代码", "看懂这段 JS", "重命名变量", "反编译".110- Asks "what does bundle.js do?" while pointing at minified/obfuscated code, or works in a project with a `modules/`/`og/` folder of previously deobfuscated chunks.111112**Don't trigger if** the code is already readable — just read and explain. **Nor for plain beautification** — `npx prettier --write <file>` when there are no cryptic names and no obfuscation. **wakaru alone is not a substitute** — `npx @wakaru/cli` un-minifies but its `smart_rename` is the same deterministic heuristic as `smart-rename.ts`, not semantic naming; use it as a pre-pass _inside_ Stage 2, not as the deliverable.113114## Routing — three questions, then load one workflow115116**Step 0 (always):** run `scripts/sourcemap-check.ts` first. If a `.map` exists, recover originals via `npx source-map-explorer` instead — that beats any rename. For the current repo's `./ref` tree, load [reference/codex-ref.md](reference/codex-ref.md) first.117118**Step 0.5 (whole tree / `./ref` / any multi-chunk app — the default):** auto-discover the entry: `scripts/check-entry.ts --discover --root <assets-dir>` reads `index.html`, picks + sanity-checks the app entry, prints its path (`build-import-graph.ts` does the same when you omit the positional). If discovery (or manual `check-entry.ts <entry> --root <assets-dir>`) exits `3`, the entry is a transitive **vendor leaf**, not the app — restoring from it yields a tiny closure that _looks_ complete (the "got 6 files, called it done" trap). A real app entry has large local fan-out and ~nobody imports it; a leaf is the inverse. Switch to the `index.html` `<script>` root (or a high-fan-out `app-main-*` chunk) first.119120**Step 0.6 (existing restore delta):** if the target has `IMPORT_MAP.json` + `_full/manifest.json`, check whether the requested chunk is already a boundary/public output — prefer the delta/boundary-replacement contract above before a new whole-tree restore. "Complete" then means complete replacement for that scoped chunk unless the user asks to rebuild the reachable graph.121122**Step 0.7 (mechanical normalization — readable tier, default-on):** on non-obfuscated minified/transpiled input, run `wakaru-normalize.ts` before `extract.ts`. Byte-rewriting like Stage 1, so the order is **sourcemap-check → detect → (Stage 1 if obfuscated) → wakaru-normalize → extract** — extract/rename from the normalized output, never `original.js`. **Skip when a usable `.map` exists** (recover via `source-map-explorer`). Auto-skips when `@wakaru/cli` is absent. In **deep/full** mode it's guarded, not default-on (Restoration contract → deep step 2).123124Then answer in order:125126| Question | Then |127| --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |128| **Obfuscated?** (`_0x` arrays, Packer, AAEncode, hex/unicode walls, opaque predicates) | **Stage 1** first via [full-obfuscation.md](workflows/full-obfuscation.md), then continue below |129| **Has an `index.html` + asset tree?** (the default) — auto-discover the entry, restore the whole reachable tree | **whole-tree restore** → [full-restoration.md](workflows/full-restoration.md) at **deep depth by default** (typed `.tsx` + acceptance + drain every chunk to promoted); only "quick"/"readable"/"快速" downgrades to the untyped readable pass |130| **Otherwise** — a lone pasted snippet or single chunk, no `index.html`/tree | **fallback: single file** → [small-minified.md](workflows/small-minified.md) |131132The single-file fallback workflow handles the sub-cases inline (it links the right technique): React/JSX → recover JSX in `--fast` polish; ≥ 3 exports / registry → [multi-export-bundle.md](workflows/multi-export-bundle.md) split (a deep-tier step); ≥ 500 KB or > 1000 symbols → [huge-single-file.md](workflows/huge-single-file.md) batching; webpack `id:(e,t,n)=>{}` modules → [webpack-bundle.md](workflows/webpack-bundle.md) webcrack pre-split; stdin/stdout chaining → [piped.md](workflows/piped.md). Load a technique doc only when its sub-case applies.133134For end-to-end worked traces, see [reference/examples.md](reference/examples.md). For caveats, gotchas, and troubleshooting, see [reference/caveats.md](reference/caveats.md).135136## Quality bar — anti-patterns to refuse before declaring done137138**Before declaring any restore done, read [reference/quality-bar.md](reference/quality-bar.md) and check the output against it.** The hard bar in **every** tier is naming/readability: no program-scope-only renames (bodies still `let k = …, [A, M] = useState(false)`), no generated fallback names (`buttonValue3`, `contextParam14`), no React Compiler scaffolding in load-bearing functions, no lowercase JSX tags that are really component aliases, no known npm package rewritten as a local implementation, no mechanical/batch checkpoint promoted into `restored/`, no bundle body merely moved/renamed into a semantic folder, and never checkpoints-built-but-nothing-promoted. Deep/typed/production output additionally must pass the typing/structure list (typed `.tsx`, split multi-export bundles, no faced app chunks, Stage 3 acceptance actually performed) — all itemized with cures in [reference/quality-bar.md](reference/quality-bar.md).139140## Tools141142Every script's purpose + "Run when" routing, external-tool notes, the shared import-map convention, and exit codes live in [reference/tools.md](reference/tools.md) — load it when you need a flag or aren't sure which script fits. The ones used in nearly every run:143144| Tool | Role |145| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |146| `scripts/sourcemap-check.ts` | **Always first** — a recoverable `.map` beats any rename |147| `scripts/check-entry.ts` | `--discover --root <assets-dir>` finds + sanity-checks the app entry (exit `3` = vendor leaf, wrong entry) |148| `scripts/build-import-graph.ts` | BFS the chunk tree → `_full/manifest.json` (full-restoration mode) |149| `scripts/wakaru-normalize.ts` | Default-on mechanical pre-pass (readable tier); auto-skips when absent |150| `scripts/polish.ts` | One-shot Stage 2: `--rename --fast --format` is the readable-tier pipeline; full chain (no `--fast`) resolves npm imports in deep |151| `scripts/quality-gate.ts` | Hard gate before promote; `quality-gate.ts <target-dir> --check-format` is the whole-tree completion proof |152| `scripts/vendor-npm-preflight.ts`| Blocking guard for any `restored/vendor/*` edit — see [Vendor/npm hard stop](#vendornpm-hard-stop) |153| `scripts/format.ts` | Prettier wrapper; every deliverable is formatted |154155## Workspace convention (TL;DR)156157Every intermediate file lives in a hidden per-chunk workspace under the target output directory:158159```bash160INPUT=ref/webview/assets/spinner-D37df5tU.js161TARGET=restored162WS="$TARGET/.deobfuscate-javascript/$(basename "$INPUT" .js)"163164mkdir -p "$WS"165cp "$INPUT" "$WS/original.js"166```167168Then run every script against `$WS/original.js` and write every output back into `$WS/`. Everything a script or batch run emits stays in the `.deobfuscate-javascript/` staging tree (`$WS/` per chunk, `_full/checkpoints/` for the batch executor); `$TARGET/` (`restored/`) receives **only** organized deliverables — this is the staging → organize → promote rule in full at [Output conventions](#output-conventions). Add `.deobfuscate-javascript/` to `.gitignore` — one entry covers every chunk in the target.169170For **full restoration mode**, a shared `_full/` subdir sits next to the per-chunk ones, holding `manifest.json`, `ledger.json`, and `locks/` so chunks coordinate as one tree. The public import map is shared at the restore root, not inside each `$WS`. Full convention — multi-module sub-workspaces, plan files, split-plan layout — at [stages/workspace.md](stages/workspace.md#multi-file-restoration-the-_full-workspace).171172## Maintaining this skill (self-improvement protocol)173174The skill is a living asset. When a run surfaces (1) a script bug or misfiring gate, (2) a manual step worth automating, or (3) a new/missed npm package identity, fix the skill itself — registry entry, script + test, or doc — and commit it **separately** from restoration output (`skill(deobfuscate-javascript): <what changed>`). The finding→home routing table, safety guard for mid-run edits, and commit discipline are in [reference/maintenance.md](reference/maintenance.md) — read it before making the skill change.175176## Files in this skill177178Load sub-files on demand — each entry says when.179180### Routing & overviews181182- [SKILL.md](SKILL.md) — this file. Routing + contracts + workspace TL;DR; always loaded.183- [reference/codex-ref.md](reference/codex-ref.md) — project profile for restoring this repo's extracted Codex.app `./ref` tree. Load before any `./ref` work.184- [reference/vendor-npm.md](reference/vendor-npm.md) — vendor/npm preflight, intent gates, touch protocol, shim recipe. Load before any `restored/vendor/*` edit.185- [reference/boundaries.md](reference/boundaries.md) — terminal-node kinds, boundary lifecycle, graph depth flags. Load when classifying or resolving a boundary.186- [reference/quality-bar.md](reference/quality-bar.md) — the anti-pattern checklist. Load before declaring any restore done.187- [reference/tools.md](reference/tools.md) — every script with purpose + "Run when", external tools, import-map default, exit codes. Load when picking a tool or flag.188- [reference/maintenance.md](reference/maintenance.md) — self-improvement protocol detail. Load when routing a discovered fix back into the skill.189- [stages/workspace.md](stages/workspace.md) — the `$WS` convention in full.190- [reference/examples.md](reference/examples.md) — nine end-to-end worked traces.191- [reference/caveats.md](reference/caveats.md) — gotchas, ordering rules, troubleshooting FAQ.192- [reference/naming-heuristics.md](reference/naming-heuristics.md) — the mechanical rename rules (React props, event handlers, iteratee callbacks, hook returns) that `smart-rename.ts` automates.193- [reference/bundler-runtimes.md](reference/bundler-runtimes.md) — esbuild vs Rolldown vs Vite runtime helper signatures (`__esm`/`__commonJS`/`__export`/`__toESM`/`__require`); read before hand-converting `__export`/`__esm` call sites.194195### Per-stage detail196197- [stages/stage-1-deobfuscate.md](stages/stage-1-deobfuscate.md) — detect → unpack → string-array → decode-strings → simplify → control-flow-report → orchestrator, + ordering rules.198- [stages/stage-2-restore.md](stages/stage-2-restore.md) — **Phase A (rename):** sourcemap-check → setup → webcrack → extract → decide names → Step 2.5 → apply → verify. **Phase B (polish):** strip-react-compiler → simplify → jsx-runtime → inline-defaults → normalize-exports → (deep-mode import tail) → `polish.ts` → format.199- [stages/stage-3-finalize.md](stages/stage-3-finalize.md) — **deep mode only. Phase A (rewrite):** D0 gate → D0.5 filenames → D1 provenance → D2 imports → D3–D4 cleanup → D5 typed `.tsx` → D6 format → D7 gate. **Phase B (acceptance):** read each file E1 naming / E2 readability / E3 formatting / E4 other → rewrite → re-read until pass (sub-agent optional). **Default tier:** optional naming-only (E1) self-review.200201### Workflows by scenario202203- [workflows/full-restoration.md](workflows/full-restoration.md) — entry chunk + every transitive sibling chunk → coordinated restoration tree (resumable across sessions, parallel across agents).204- [workflows/small-minified.md](workflows/small-minified.md) — < 500 KB, no obfuscation, direct rename.205- [workflows/full-obfuscation.md](workflows/full-obfuscation.md) — Stage 1 + Stage 2 end-to-end (includes eval-gating for Packer-wrapped untrusted input).206- [workflows/webpack-bundle.md](workflows/webpack-bundle.md) — webcrack pre-split, rename per module.207- [workflows/huge-single-file.md](workflows/huge-single-file.md) — `plan.ts` + checklist + batched renames.208- [workflows/multi-export-bundle.md](workflows/multi-export-bundle.md) — ESM chunks with many exports → directory of small files.209- [workflows/react-vite.md](workflows/react-vite.md) — single-component React/Vite/Rollup chunks → JSX recovery.210- [workflows/piped.md](workflows/piped.md) — stdin/stdout chains for quick experimentation.211212### Scripts213214Every script is described once, with its "Run when" routing, in [reference/tools.md](reference/tools.md). All scripts live under [`scripts/`](scripts/); each behavior-changing script has a sibling `scripts/<name>.test.ts`.215216#### Tests + fixtures + deps217218- `scripts/*.test.ts` — `bun test` suite (run from skill dir).219- `fixtures/` — one per technique (`packed.dean-edwards.min.js`, `aaencode.min.js`, `url-encoded.min.js`, `string-array-obfuscator.min.js`, `from-char-code.min.js`, `dead-code.min.js`, `opaque-predicates.min.js`, `control-flow-flat.min.js`, `composite.min.js`, `example.min.js`).220- [package.json](package.json) — bun deps (`@babel/parser`, `@babel/traverse`, `@babel/generator`, `@babel/types`).