| name | deobfuscate-javascript |
| description | Reverse-engineer minified, obfuscated, packed, or webpack/Vite/Rollup-bundled JavaScript back to readable code, including Codex.app `./ref/webview/assets` restoration. A WHOLE-TREE restore (an index.html + asset tree, or "restore the code/whole tree") DEFAULTS TO DEEP: Stage 1 only if obfuscated โ Stage 2 smart-rename + polish โ Stage 3 typed `.tsx` rewrite + acceptance review, npm-import resolution, import graph/ledger orchestration that drains EVERY reachable project-local chunk to promoted, full-target `quality-gate`; bundled vendor data (Shiki grammars/themes, 3Dmol) is detected and left as bare npm imports, not restored. A "quick"/"readable"/"ๅฟซ้" pass (or a lone pasted snippet) downgrades to the untyped readable restore where the hard bar is just naming quality. Use when the user provides a .min.js / dist bundle / obfuscated script / Vite chunk or asks to "humanify", "deobfuscate", "deminify", "unminify", "unpack", "decode", "make readable", "rename variables", "reverse engineer", "ๅฎๆด่ฟๅ", "ๆทฑๅบฆ่ฟๅ", "ๅๆททๆท", "็พๅไปฃ็ ", "็ๆ่ฟๆฎต JS", "้ๅฝๅๅ้", "ๅ็ผ่ฏ". |
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 dependency. Run only on obfuscated input. Detail: stages/stage-1-deobfuscate.md.
- Stage 2 โ Restore to readable (two phases). (a) Rename cryptic identifiers (
a, e, t, _0x4f3, โฆ) to meaningful names โ a TypeScript+Bun port of humanify where you (the agent) play the renamer, running smart-rename.ts first so you only hand-name the residue; this is where readability is won. (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) only makes imports resolve against node_modules and is deep mode only. Detail: stages/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, TypeScript types/
.tsx recipes, directory splits, import repair. (b) Acceptance review โ the host agent 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, for extra eyes). The default tier ships the readable file from Stage 2 without Stage 3. Detail: stages/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 (the user is asking to "restore the code", which means complete + typed); a lone pasted snippet defaults to readable. Drop to readable for a whole tree only when the user explicitly asks for a "quick" / "readable" / "ๅฟซ้" pass.
- Default scope โ whole tree from
index.html. When the input is an app (an index.html plus a sibling-chunk asset tree), read index.html, auto-discover the entry (src/domain/check-entry.ts --discover --root <assets-dir> prints it; build-import-graph.ts auto-discovers when the positional is omitted), then recursively restore every reachable project-local chunk. The per-chunk pipeline is unchanged: Stage 1 (only if obfuscated) โ wakaru mechanical-normalization pre-pass (wakaru-normalize.ts; recovers ES6 classes, async/await, optional chaining, destructuring, TS enums, โฆ โ runs by default, auto-skips if @wakaru/cli is absent) โ Stage 2 rename (smart-rename first, then hand-name the residue) โ Stage 2 reading-aid polish (polish.ts --fast) โ format โ organize the draft in the staging workspace, then promote into the shared src/ root (FSD layers, kebab filenames, provenance headers, one shared src/IMPORT_MAP.json). Script/batch output stages first and is never written straight into src/. Bundled vendor data (Shiki language grammars + themes, standalone data libs like 3Dmol) is detected by content and treated as npm-leaf โ it is NOT restored into src/; consumers import the bare specifier (@shikijs/langs/<id>, @shikijs/themes/<id>). So src/ holds app code only, never hundreds of grammar/theme dumps. At readable depth the hard bar is naming quality; at the default deep depth it is also 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 is the whole-tree default: meaningful names plus Stage 3 (typed
.tsx rewrite + acceptance review), full npm-import resolution (Stage 2's deep-mode polish tail), and import-graph/ledger orchestration that drains every reachable chunk to promoted. It is also triggered explicitly on . is the quick opt-out (meaningful names, reading-aid polish, untyped output) โ use it only when the user says "quick" / "readable" / "ๅฟซ้" / "็ฒ็ฅ", or for a lone pasted snippet. Both depths ; defaults to (enforces /param types), and 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 (see Restoration contract). Readable is a deliberate downgrade the user asks for, not the resting state.
Output conventions
src/ is a clean-only deliverable zone โ batch/script output never lands here directly. Anything a batch or script restore emits (auto-restore-full.ts checkpoints, a one-shot polish.ts swept over many chunks, --write-target-checkpoints files, any hash-basename .tsx) is a mechanical checkpoint, not a deliverable. It must be written into the temporary, gitignored staging tree under src/.deobfuscate-javascript/ (_full/checkpoints/<basename>.tsx for the batch executor; the per-chunk $WS/ otherwise) โ never copied or written straight into src/. Promote a file into src/ only after the host agent has organized it. The promotion bar is good readability (semantic names throughout, no mechanical fallbacks like buttonValue3 / contextParam14), friendly filenames (semantic kebab-case), clear directory structure (FSD layers), and prettier-formatted (blank lines, wrapped long lines, parenthesized multi-line JSX returns โ promote-organized.ts runs format.ts on every deliverable as it writes it, so src/ is never raw @babel/generator output) in every tier โ plus complete types (Props interfaces on exported components, parameter/return types where they help) whenever the restore is deep/full, which is where all auto-restore-full.ts batch output lives. The only files that may promote untyped are genuinely hand-restored single readable-tier deliverables โ and even those draft in $WS, get organized for names + structure, and only then land in src/; they are never raw script output. This staging โ organize โ promote discipline is the hard rule behind every convention below. For a whole-tree batch restore it is driven, not eyeballed: plan-organize.ts proposes a domain + kebab public path per chunk into the manifest's organize/promote stages, and promote-organized.ts drains the resulting promote frontier (typed deliverable โ quality gate โ copy into src/<domain>/ โ IMPORT_MAP update โ import rewrite). The restore is complete only when every reachable chunk reaches and passes โ see .
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.
Restoration contract
Default tier (readable restore)
- Find the entry (whole tree is the default). When there is an
index.html + asset tree, auto-discover the entry: bun src/domain/check-entry.ts --discover --root <assets-dir> prints it, or omit the positional to build-import-graph.ts. Restore every reachable project-local chunk (the per-chunk steps below run across the tree, orchestrated by workflows/full-restoration.md). If there is no index.html/asset tree (a lone pasted snippet), fall back to the single-file flow โ workflows/small-minified.md.
- Analyze first. Always run
sourcemap-check; use detect / extract per chunk.
- Restore to readable. Stage 1 only if obfuscated โ wakaru-normalize (
bun src/infrastructure/wakaru-normalize.ts "$WS/original.js" -o "$WS/normalized.js"; auto-skips when the binary is absent, and always leaves a normalized.js for the next step) โ Stage 2 rename (smart-rename first, then hand-name the residue until single-letter density is low) โ reading-aid polish (polish.ts --rename --fast) โ format. A one-shot polish.ts "$WS/normalized.js" --rename --fast --source <original-path> --out draft.tsx --format lands the draft (the file operand is the wakaru-normalized output; --source still points at the original input for the provenance header); you then hand-name what smart-rename left.
- Stage, organize, then promote. The rename/polish one-shot writes its draft into the per-chunk
$WS/ staging dir, not into src/. Promote into the shared src/ root (FSD layers, kebab filenames) โ plus a provenance header (// Restored from <path>) and the shared src/IMPORT_MAP.json โ only after organizing the draft (meaningful names, friendly kebab filename, clear structure). Never copy a mechanical checkpoint into src/ (see Output conventions). The hard bar at readable depth is naming quality โ meaningful identifiers, no generated fallback names (buttonValue3, ); types, npm-import resolution, and the reviewer loop stay optional, and an optional single naming-only self-review is available when you want 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. A checkpoint may keep hash basenames and mechanical fallback names; it is never the final deep deliverable. wakaru-normalize may run per chunk body (src/infrastructure/wakaru-normalize.ts on each _full/files/<basename>/original.js) here too, but with two hard guards: never --unpack an already-split chunk tree (it re-derives its own module boundaries/filenames, forking the restore root), and treat any import/export specifiers wakaru emits as untrusted โ build-import-graph.ts must (re)build the manifest from the real on-disk chunk files after wakaru, and resolve-npm-imports.ts + the Codex CHUNK_NAME_REGISTRY remain the authoritative import rewriter (wakaru's un_esm does not substitute for them).
- Rewrite semantically as the host agent. Read the checkpoint together with the original source, graph/ledger, producer/consumer imports, and project conventions. Rewrite the public output into code a human would maintain: meaningful filenames and identifiers, typed props, semantic exports, resolved npm imports, and clear structure.
- Pre-filter only. Run prettier and
quality-gate.ts to catch cheap failures. Passing these scripts does not mean the work is done.
- Acceptance-review LOOP. The host agent reads every delivered file end-to-end against the four-category quality bar in
src/infrastructure/acceptance-checklist.md, rewrites each NEEDS_FIX file, and re-reads until all pass โ no sub-agent and no authorization required. An independent reviewer sub-agent is optional: use it for extra eyes only when the runtime offers one and the user has authorized delegation; never block on it, never ask for authorization to finish, and never fabricate verdicts or substitute a casual skim for a careful read. The deep restore is complete only after every delivered file passes.
- Full-target completion audit. For whole-tree restores, run
quality-gate.ts <target-dir> after the acceptance loop. This target-level audit checks the manifest plus the shared import map; it fails if any reachable local/oversized-local app feature chunk is still missing, mechanical, , an empty placeholder, a typed facade, or lacks Stage 3 acceptance/finalized evidence. Do not substitute a grep or scan.
Dual-track output (deep mode): hidden checkpoints preserve original hash basenames for traceability. Public final files/directories use semantic names without hash suffixes; preserve original chunk identity in provenance headers and, for multi-file/full restorations, an import map or report. Use one shared import map at the restore root โ src/IMPORT_MAP.json โ reused regardless of which entry triggered the restore; never per-chunk, per-session, or per-entry maps. If the restore root already has it, reuse and append; otherwise create it there. Create a separate map only when the user explicitly scopes a different project/root.
Full restoration mode is not a new stage โ it's an orchestration layer on top of Stages 1โ3 that coordinates the same per-file pipeline across an entire connected component of the import graph (e.g. an entry chunk plus all the sibling chunks it pulls in, transitively to the leaves). It adds a manifest.json (file-level dependency graph + per-file imports/exports + per-file stage status) and a ledger.json (per-file symbol-level checklist with cross-file binding table) so renaming is resumable across sessions and parallelizable across agents at file granularity. If the user says "deep", "full", "ๅฎๆด", "ๆทฑๅบฆ", or asks to restore referenced chunks, this means every reachable project-local sibling chunk 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; do not create a parallel restore root.
- Restore the requested chunk in its existing
$TARGET/.deobfuscate-javascript/_full/files/<basename>/ workspace. Restore additional chunks only when they are missing from the import map or explicitly requested.
- Rewrite imports from the chunk's manifest entry through the shared
IMPORT_MAP.json: finalized local deps use semantic public paths; npm deps use bare specifiers; unresolved runtime/vendor deps keep their documented boundary facade paths.
- Replace the mapped boundary/public file with the semantic candidate, and update the import-map entry (
path, exports, status, and remove boundary when the facade is no longer a facade).
- Validate the delta: format, run
quality-gate.ts on the changed public path(s), run the target TypeScript check when the restore root has a tsconfig.json, and run Stage 3's acceptance review on changed public files only. Run the all-tree acceptance review only when the deliverable changed the whole public tree or the user asked for it.
What full-restoration restores โ and what it deliberately doesn't
The mode makes three categories of dependencies terminal nodes in the manifest. They are recorded so consumers can reference them, but never have a ledger entry, never get renamed, and never get a <basename>/ workspace dir:
| Kind | Trigger | What we do |
|---|
npm-leaf | The chunk's basename is in CHUNK_NAME_REGISTRY (e.g. clsx-XXXX.js, react-XXXX.js, tslib.es6-XXXX.js, marked.esm-XXXX.js, floating-ui.react-dom-XXXX.js). | polish.ts โ resolve-npm-imports.ts rewrites consumer imports to the bare npm specifier (import clsx from "clsx"). The chunk file itself is left untouched. |
oversized-local | Only when you explicitly pass --max-lines N and a local sibling exceeds that line cap. This is a quick/targeted mode, not deep restoration. | We parse imports/exports (so cross-file bindings link up) but stop BFS at the file. The consumer's import stays as import { xt as someAlias } from "./setting-storage-XXXX.js" โ the agent renames someAlias based on usage in the consumer, not by following the producer. |
external | Bare specifier (react, vite, etc.) that wasn't seen as a chunk in the manifest. | Same as npm-leaf. |
faced-boundary | A genuinely third-party vendor/runtime-dominated chunk (Zod's 308-export src, a React+react-query scope layer, a host/vscode-api bridge, the Statsig SDK) imported by many feature chunks via cryptic aliases. Project/feature chunks are never faced โ app-shell-*, app-main-*, feature pages/panels/views, components, contexts, and hooks are the app's own code and must be recursively restored even when large or widely imported. "Big" or "imported by many" is not a facing reason; only genuinely vendor/runtime content is. | Run make-facade.ts <chunk> to emit a typed export declare const โฆ : any facade so consumers compile against it; record the alias map under dependencyBoundaryFacades in the project IMPORT_MAP; mark it ledger.ts mark-faced <basename> so its consumers become restorable. A facade is a temporary scaffold and an , not a finished deliverable โ the deep/full restore stays incomplete until the chunk is restored or the user explicitly scopes it out. refuses app-entry basenames unless you pass . Deep-restore the runtime only when the user explicitly scopes it. |
Depth rule (recursion): deep/full restoration recurses the entire reachable project-local import graph, not just the entry's direct imports. When you deep-restore app-main-* and it imports app-shell-*, you restore app-shell-* too โ and everything it imports, transitively, to the leaves. An entry is not done while any project-local chunk it transitively imports is still a facade or placeholder. build-import-graph.ts already enqueues every local sibling (default --max-lines 0, no cap); the restoration loop must drain ledger.ts frontier until it is empty. Facing is reserved for genuine third-party vendor/runtime leaves and is an open boundary to report โ never a way to call an app chunk "done". Use a positive --max-lines only when the user explicitly asks for a quick/targeted restore or accepts a partial dependency boundary. The final proof is quality-gate.ts <target-dir> over the whole public target, not a boundary-only audit and not import-map statuses alone.
Boundary lifecycle โ three terminal states, never a permanent any-facade
src/shared/boundaries/*.ts is scaffolding in transit, not a resting place. A generated export declare const X: any facade type-checks but is dead weight and hides whether a dependency is third-party or unfinished app code. src/domain/chunk-classification.ts โ classifyBoundary() splits every boundary into one of two kinds (driven by the IMPORT_MAP vendor field โ anything other than the literal "runtime" marker names a real npm family), and each has a defined exit:
-
Third-party npm package โ bare re-export shim (DONE). When the chunk is a recognised package (lodash, react-router, react-intl/FormatJS, framer-motion, mdast-util-*, @pierre/diffs, zod, @radix-ui/*, Segmentโฆ), the deliverable is a thin re-export, not a stub: src/infrastructure/make-facade.ts <chunk> --reexport <specifier> [--export-star | --reexport-named a,b,c] [--name-map aliasโreal.json]. It resolves to the package's real @types and is a finished deliverable โ src/shared/boundaries/highlight-code.ts is the model. Verify first: the bundled chunk must be stock, not a Codex fork (the skill already warns @pierre/* and other vendors are forked โ see reference/codex-ref.md), and the specifier must resolve in ref/node_modules. Consumers keep importing from src/shared/boundaries/; when they import cryptic aliases (appScopeC), the --name-map bridges { "<publicAlias>": "<realExport>" } so the re-export matches. quality-gate.ts <target> flags any vendor-npm boundary still left as an any-facade (full-restoration-npm-boundary-not-resolved).
-
Genuine app/host runtime โ typed facade, or an opt-in runnable interim. For the app's own runtime (app-scope, the vscode/host bridge, rpc, host-config, product-logger, persisted-signal stateโฆ), the typed make-facade.ts any-facade is the type-check-only interim, recorded under dependencyBoundaryFacades + ledger.ts mark-faced. To make the app runnable before the runtime is restored, src/infrastructure/make-facade.ts <chunk> --passthrough <ref-relpath> [--name-map] emits a // @ts-nocheck + // TODO: deep-restore module that re-exports the original ref chunk (note: this makes src/ depend on the unrestored tree โ it is a deliberate, clearly-marked stopgap, not a deliverable).
Override flags on build-import-graph.ts:
--max-lines 0 โ default; disable the cap entirely (restore everything project-local).
--max-lines 5000 โ quick/targeted mode; skip local siblings above the cap.
--include foo-HASH,bar-HASH โ force-restore specific basenames even if they exceed the cap. Repeat the flag is fine; comma-separated also works.
The entry is always exempt from the cap.
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:
src/ (mirrors ref/webview/assets), with FSD layers (app/, pages/, widgets/, features/, entities/, shared/). Reuse the single shared src/IMPORT_MAP.json before creating any new map. Do not create a per-entry folder such as src/app-main/. Auto-discover the entry with src/domain/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 the published @pierre/trees (Preact-based) and @pierre/diffs (Shiki-based) packages bundled inline. Don't deep-restore them as Codex features. But Codex FORKED them (a React fork of @pierre/trees; a settings-entangled @pierre/diffs gate), so a clean bare-import swap is usually infeasible โ keep the forked wrapper, relabel provenance, and boundary-ize (quality-gate.ts --vendored); only the self-contained leaves are clean bare imports (highlight worker-* โ @pierre/diffs/worker, parsePatchFiles-* โ @pierre/diffs). Recognize by fingerprint: --trees-* / --diffs-* CSS vars, data-file-tree-* / data-diffs-* DOM attrs, pierre-light/dark(-soft) Shiki themes, the @pierre/truncate css hereโฆ comment. Engine chunks: file-tree-search-input-*, shiki-highlight-provider-gate-* (the @pierre/diffs core), file-diff-*, , , . , , (PDF.js), (Codex wrapper), grammar (Shiki TextMate), / (Mermaid), (git). Full detail + remediation mechanics: .
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 string literals.
- Says "humanify", "deobfuscate", "deminify", "unminify", "unpack", "decode", "make readable", "rename variables", "reverse engineer".
- Asks "what does bundle.js do?" while pointing at obviously minified or obfuscated code.
- Writes in Chinese: "ๅๆททๆท", "็พๅไปฃ็ ", "็ๆ่ฟๆฎต JS", "้ๅฝๅๅ้", "ๅ็ผ่ฏ".
- Works in a project that already has a
modules/ or og/ folder full of previously deobfuscated chunks.
Don't trigger if the code is already readable โ just read and explain. Don't trigger for plain beautification either โ npx prettier --write <file> is the right tool when there are no cryptic names to rename and no obfuscation to unwind. wakaru alone is not a substitute for this skill โ npx @wakaru/cli un-minifies (recovers classes/async-await/etc.) 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 (below), not as the deliverable.
Routing โ three questions, then load one workflow
Step 0 (always): run src/infrastructure/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 from index.html: src/domain/check-entry.ts --discover --root <assets-dir> reads index.html, picks the app entry, and prints its path (it also sanity-checks it). build-import-graph.ts runs the same discovery when you omit the positional entry. If discovery (or a 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 dependency closure that looks complete (the classic "got 6 files, called it done" trap). A real app entry has a large local fan-out and is imported by ~nobody; a leaf is the inverse. Switch to the index.html <script> root (or a high-fan-out app-main-* chunk) before continuing.
Step 0.6 (existing restore delta): if the target already contains IMPORT_MAP.json plus _full/manifest.json, check whether the requested chunk is already represented as a boundary/public output. Prefer the delta/boundary-replacement contract above before launching a new whole-tree restore. In this case, "complete" means complete replacement for that scoped chunk unless the user explicitly asks to rebuild the reachable graph.
Step 0.7 (mechanical normalization โ readable tier, default-on): on non-obfuscated minified/transpiled input the readable tier runs wakaru-normalize.ts as a pre-pass before extract.ts (recovers classes / async-await / optional-chaining / destructuring / enums that the renamer otherwise has to read around). It is 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 it when a usable .map exists (recover via source-map-explorer instead). It auto-skips when @wakaru/cli is unavailable. In deep/full mode it is guarded, not default-on (see Restoration contract โ deep step 2).
Then answer in order:
| Question | Then |
|---|
Obfuscated? (_0x arrays, Packer, AAEncode, hex/unicode walls, opaque predicates) | run Stage 1 first via workflows/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 โ 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 |
Otherwise โ a lone pasted snippet or single chunk, no index.html/tree | fallback: single file โ workflows/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
Both tiers โ naming/readability (the default tier's only hard bar)
These produce "looks OK if you skim it, falls apart on a real read" output. They are the hard bar in every tier, because they are about readability, not compilation:
- Program-scope-only rename. Symptom: top-level exports have meaningful names (
AppShellTabContent, RightPanelTabs) but the function bodies are still let k = useIntl(), [A, M] = useState(false), N = !h, โฆ. Cause: renaming stopped at top-level scope. Cure: keep going into the function bodies (see stage-2-restore.md โ Step 2.5) until single-letter density is low.
- Generated fallback names shipped as "readable". Symptom: code contains
ImportedBinding1, callbackValue1, localValue1, local274, argument8, elementNode1, hookValue1, restoredHelper1, param1, buttonValue3, buttonParam1, contextParam14, or DistO. Cause: a generic fallback rename hid cryptic identifiers without understanding them. Cure: use producer/consumer usage, JSX shape, call graph, and domain context to assign semantic names; quality-gate.ts --allow-flat flags the common mechanical patterns.
- React Compiler scaffolding left in load-bearing functions. Symptom:
let t = compilerCache.c(N), โฆ; if (t[5] !== n || t[6] !== h โฆ) { โฆcomputeโฆ; t[5] = n; โฆ } else { โฆ = t[5]; โฆ } still present. This hurts reading, so fix it in either tier. Cause: strip-react-compiler couldn't unwind a complex cache pattern. Cure: hand-strip the worst offenders โ keep the compute branch, drop the cache slots.
- Lowercase JSX component aliases left as HTML intrinsics. Symptom: JSX conversion produces
<b id="โฆ" defaultMessage="โฆ" /> or another lowercase tag that is actually an imported component alias such as FormattedMessage. Cause: jsx-runtime.ts preserved the minified binding name and React now treats it as an intrinsic element. Cure: recover the imported component name from the producer/import map and rewrite opening/closing tags to the PascalCase component.
- Mechanical/batch checkpoint promoted into
src/. Symptom: auto-restore-full.ts or batch-polish.ts output, hash-basename files (button-bq66r8jD.tsx), or --write-target-checkpoints files sitting directly in . Cause: skipping the staging โ organize โ promote discipline and copying raw script output into the deliverable root. Cure: keep all batch/script output in the staging tree; promote a file into only after it meets the promotion bar โ readable, friendly kebab filename, clear directory structure (and typed in deep/full mode, where batch restores live). See .
Deep / production mode only
These are typing/structure/compilability requirements. They are not failures in the default readable tier โ a readable, well-named untyped file is a valid default deliverable. They apply only when the user asked for deep/typed/production output, where Stage 3's D0 gate and Stage 3's acceptance review enforce them:
- Multi-export bundle delivered as a single file. Symptom: the polished
.tsx is > 1 000 lines, has a export const Foo = { Name1, Name2, โฆ } registry, and lives at <target-dir>/<bundle>.tsx rather than <target-dir>/<bundle>/. Cure: run multi-export-bundle.md. (Default tier may leave a multi-export file flat.)
- Checkpoint shipped as deep completion. If you copy
$WS/polished.tsx, _full/checkpoints/<basename>.tsx, or an auto-restore-full.ts output directly to the public target and call the deep restore done, you are shipping the mechanical checkpoint. The host agent must semantically rewrite it, use semantic final filenames, run the script pre-filter, and then run Stage 3's acceptance review.
- Polished
.js instead of typed .tsx (deep mode). In deep mode, Stage 3's D5 requires a .tsx with a real Props interface (or type alias) on every exported component, parameter/return types where helpful, and ReactNode / ReactElement / event-handler types for JSX slots. function MyRow(props) with no annotation is fine in the default tier; it is a deep-mode fail only. See stage-3-finalize.md โ D5.
- Pure icon module shipped with bundle residue. Symptom: a one-SVG file still has
jsx-runtime, __toESM(loadJsxRuntimeRaw()), var jsxRuntime = โฆ, or an untyped export const i = props => <svg โฆ>. Deep-mode cure: run src/application/semantic-finalize.ts --recipe icon; the typed deliverable uses SVGProps<SVGSVGElement>, IconProps, a semantic *Icon name, and a default export.
- Small multi-icon chunk left as one file. Symptom:
expand-XXXX.tsx exports two or more independent SVG icon components in one flat file. Deep-mode cure: split into <target>/<basename>/types.ts, one kebab *-icon.tsx per icon (e.g. expand-icon.tsx exporting ), and barrel.
Tools at a glance
Full Restoration โ multi-file orchestration (above Stage 1/2/3)
| Tool | Purpose | Run when |
|---|
src/domain/check-entry.ts | Sanity-check a restoration entry before building the graph: reads index.html, the entry's local fan-out (out-degree), and how many siblings import it (in-degree). Flags a transitive vendor leaf mistaken for an app entry (exit 3). build-import-graph.ts runs it automatically and warns; run standalone to confirm the entry first. | Step 0.5 of workflows/full-restoration.md, or any time the entry hash is uncertain. |
src/domain/build-import-graph.ts | BFS from an entry chunk, follow every import/re-export from literal. Stops at npm-leaf chunks recognised by resolve-npm-imports.ts's CHUNK_NAME_REGISTRY. Emits _full/manifest.json (file-level dependency graph + per-file imports/exports + per-file stage status) and stages a _full/files/<basename>/original.js for each local chunk. Calls check-entry.ts first and warns on a vendor-leaf entry (--no-entry-check to skip). | First step of workflows/full-restoration.md. Re-runnable; preserves prior stages + owner fields. |
src/infrastructure/make-facade.ts | Three emitters for a boundary chunk (see Boundary lifecycle): default โ typed export declare const โฆ : any facade (type-check only) for a huge vendored/runtime chunk (308-export Zod src, an app-scope/vscode-api runtime, the Statsig SDK); --reexport <specifier> (--export-star / --reexport-named a,b,c) โ a bare third-party re-export shim when the chunk IS a known npm package (lodash, react-intl, , โฆ), resolving to real โ a finished deliverable; โ a + module re-exporting the original ref chunk so the app runs before the runtime is restored. Handles reserved-word exports (Zod exports ). supplies semantic facade names, or bridges for reexport/passthrough; sets the header. |
Stage 1 โ Deobfuscation
| Tool | Purpose | Run when |
|---|
src/infrastructure/detect.ts | Classify obfuscation techniques + recommend next steps | First, on anything you suspect is obfuscated |
src/infrastructure/unpack.ts | Dean Edwards Packer + AAEncode + URLEncode unwrap (iterates layers) | Packed/encoded input. Uses new Function โ gated by --no-eval |
src/infrastructure/string-array.ts | Inline Obfuscator.IO _0x... string-array references; remove rotation IIFE + dead array | Output has _0x arrays and indexed lookups |
src/infrastructure/decode-strings.ts | Decode \xNN, \uNNNN, String.fromCharCode(...), and literal atob('base64') | Hex/unicode escapes or constant base64 strings present |
src/infrastructure/simplify.ts | Constant folding, dead-code removal, (0, fn)(...) โ fn(...), backtick-template โ string, scope-aware literal inlining (loops to fixed point) | After string-array + decode-strings to clean up the residue; also useful standalone on Rollup/Vite output |
src/infrastructure/control-flow-report.ts | Detect while(true){switch} flatteners + opaque predicates โ report only, no mutation | Last in Stage 1. Read the report; rewrite by hand |
src/infrastructure/deobfuscate.ts | Orchestrator: runs every Stage 1 pass in order with --skip / --stop-after | One-shot Stage 1. Emits final code + JSON report |
Stage 2 (rename)
| Tool | Purpose | Run when |
|---|
src/infrastructure/sourcemap-check.ts | Detect //# sourceMappingURL=โฆ or adjacent .map; list original sources | Always first โ if a sourcemap exists, renaming is wasted work |
npx webcrack <file> | Unpack webpack/browserify bundles into per-module files | Input is one giant bundle (โฅ 500 KB or many id: (e,t,n)=>{} modules) |
src/infrastructure/wakaru-normalize.ts | Pre-rename mechanical decompiler โ wraps external npx @wakaru/cli (Rust). Recovers ES6 classes, async/await from generator state-machines, optional chaining, ??, for-of, destructuring, TS enums, template literals, let/const (~66 rules the skill's polish lacks) so the rename starts from cleaner code. NOT a deobfuscator and NOT a semantic renamer. Auto-skips (passthrough) when the binary is absent. --level standard default; --level minimal for fidelity-critical/untrusted; aggressive only with verification; --dce opt-in; --unpack single-bundle only. | Readable tier: default-on, after sourcemap-check + detect (+ Stage 1 if obfuscated), before extract.ts. Skip when a usable .map exists. Deep/full mode: per chunk body only, never --unpack a split tree (re-derive the graph after). |
src/infrastructure/extract.ts | Parse JS, emit every binding + scope context as JSON. Filter flags (--kind, --scope-kind, --min-refs, --only-cryptic, โฆ) trim the list for huge files. | Step 1 of rename pipeline |
src/application/plan.ts | Group symbols into ordered rename batches + emit CHECKLIST.md for stage-by-stage work | When symbols.json has > ~200 entries โ break work into batches |
src/infrastructure/smart-rename.ts |
External tools (not bun deps): wakaru-normalize.ts shells out to npx @wakaru/cli@1.5.0 (or a global wakaru / prebuilt platform binary). wakaru is a Rust binary, not a Babel library โ it is invoked as a subprocess (like webcrack/prettier) and is not added to package.json. The wrapper degrades gracefully (passthrough + stderr note) when it is unavailable, so the core bun+Babel path never depends on it.
Stage 2 (polish, post-rename)
Reading-aid subset (--fast, default tier): strip-react-compiler, simplify, jsx-runtime, inline-defaults, normalize-exports โ these change how the code reads. Import-resolution tail (deep mode only): react-shim-elim, resolve-npm-imports, npm-cjs-shim-elim, dead-shim-elim โ these only make imports resolve against node_modules. The default tier runs polish.ts --fast and stops at the subset.
| Tool | Purpose | Run when |
|---|
src/infrastructure/strip-react-compiler.ts | Strip the React Compiler memoization scaffolding (let cache = react.c(N) + every cache[i] === x ? readBranch : computeBranch conditional) | Renamed file contains react.c(N) or (0, x.c)(N) cache scaffolding |
src/infrastructure/simplify.ts | Same script as Stage 1; post-rename collapses (0, x.y)(...) โ x.y(...), `svg` โ "svg", restores {a: a} โ {a}, collapses {true:"X",false:""}[cond?"true":"false"] โ cond && "X" | Renamed file still contains (0, fn)(...) patterns, backtick string literals, or redundant long-form properties |
src/infrastructure/jsx-runtime.ts | Convert jsxRuntime.jsx("svg", { ...props, children: ... }) and jsxs / Fragment calls back to JSX syntax | Output of a React build (Vite, Rollup, esbuild) with JSX compiled to runtime calls |
src/infrastructure/inline-defaults.ts | Inline let X = p === undefined ? D : p and let X = p ?? D patterns into the parent destructure pattern as defaults ({ p = D }); merge let A, B; + ({A, B} = expr); into let { A, B } = expr; | Output has separate destructure + resolved-default declarations |
src/infrastructure/normalize-exports.ts | Collapse var X = expr; export { X as Y }; (and function/class variants) into export const Y = expr; | Output has tree-shaken aliased exports โ common in Rollup/esbuild bundles |
src/infrastructure/react-shim-elim.ts | Collapse common React CJS namespace shims (toESM(loadReact())) to import React from "react" and prune the now-unused helper/runtime imports. |
Stage 3 (semantic finalize)
| Tool | Purpose | Run when |
|---|
src/application/semantic-finalize.ts | Recipe-based finalizer for high-confidence semantic modules. --recipe icon turns one or more exported SVG JSX components into typed IconProps modules; multi-icon chunks become a directory plus index.ts barrel. --recipe button turns the common Button/control chunk into named variant tables, typed props, forwardRef, displayName, and default export. --rewrite-imports --import-map map.json updates consumers from bundle aliases (t, n) to semantic named imports and split barrel paths. | After polish.ts, before final formatting. Use it for pure SVG icons, small multi-icon chunks like expand-XXXX, and Button/control chunks like button-XXXX. |
src/application/plan-split.ts | Draft a split-plan.json for multi-export polished modules by reading exports and registry objects. The generated plan is a starting point, not a substitute for semantic grouping. | Before split-bundle.ts on any file with โฅ 3 exports, a registry object, or > 1000 lines. |
src/infrastructure/split-bundle.ts | Execute a reviewed split plan: extract top-level bindings into a directory, generate cross-file imports for simple helper references, emit an index.ts barrel, and rebuild registry-object exports. | After Stage 2 polish / semantic naming and before final formatting/gating for multi-export bundles. |
src/application/promote-final.ts | Gate-before-copy helper: runs quality-gate.ts on a candidate file/dir, writes an optional report, and only then replaces the public target. It is not final acceptance; Stage 3's acceptance review still follows. | Use instead of direct cp/manual copy when moving a host-agent rewritten candidate into the user's target directory. |
src/domain/quality-gate.ts | Hard finalization gate: fails on missing provenance headers, too many cryptic params/bindings, cryptic public export names, generated fallback names, dense one-letter references, stale runtime/compiler residue, suspicious lowercase JSX component tags, or flat files that should be split. On a full-restoration target, it also audits plus / legacy + maps and rejects app-feature chunks that are missing, mechanical, placeholders, facades, , , or not Stage 3 accepted/finalized โ and a (; convert via ). runs and fails unformatted files (soft-skips when prettier is unavailable). (and auto-detected output) relaxes semantic checks only for faithful vendored modules / boundary facades. |
Import map default: when rewriting imports for files that belong to the same restored project, pass the shared project map by default:
IMPORT_MAP="src/IMPORT_MAP.json" # one shared map at the restore root
bun <skill-dir>/src/application/semantic-finalize.ts --rewrite-imports <file-or-dir> \
--import-map "$IMPORT_MAP"
Append new hash-to-semantic entries to that file as the project grows, and reuse it across future chunks from the same app so consumers do not accumulate duplicate import-map files. The map is always src/IMPORT_MAP.json, regardless of which entry triggered the restore.
For delta/boundary replacements, derive import rewrites from the existing project map instead of hand-inventing paths. Semantic local imports take priority over hashed paths; unresolved runtime/vendor boundaries should continue to import the documented facade path until that boundary is explicitly restored.
Stage 3 (acceptance review) โ deep mode; default = optional naming review
| Tool | Purpose | Run when |
|---|
src/application/prepare-stage-e-review.ts | Batch the deep-mode public tree into review packets under <target>/.deobfuscate-javascript/_stage-e: index.json, batches.tsv, README.md, and one batch-XX.md per batch with the acceptance checklist and inlined public source files. Excludes hidden checkpoints. --verify checks the packets still cover the current public tree exactly once. Optional aid โ useful whether the host agent reads the packets itself or hands them to a sub-agent. | On a large deep-mode tree, after quality-gate.ts passes, to batch the read. --max-files / --max-bytes keep batches readable. Not required โ for small targets, read the files directly. |