Decompiler, deminifier, and deobfuscator for Bun-compiled standalone JavaScript binaries.
Extracts embedded JavaScript source from Bun's binary format, splits it into individual modules, identifies vendor (npm) packages, recovers original identifiers, and organizes everything into a readable, structured codebase — from a single compiled binary.
Bun binary (opaque executable)
→ extract embedded JS + assets
→ split into individual modules (1 file per module)
→ classify vendor vs app modules (fingerprint DB + flood-fill)
→ deobfuscate (structural transforms + name recovery + formatting)
→ cluster modules by dependency graph (Louvain community detection)
→ organize into semantic directories
→ readable, organized source code
→ reassemble back into a working binary (round-trip)
The deobfuscated code is not just readable — it's runnable. You can modify individual module files and reassemble them back into a working Bun binary.
npm install# 1. Extract JS from a Bun binary
node src/extract.mjs /path/to/bun-binary extracted/
# 2. Split into individual modules
node src/resplit.mjs extracted/bundle.js resplit/
# 3. Classify vendor packages via fingerprint DB
node src/match-vendors.mjs resplit/ --db data/vendor-fingerprints-1000.json --classify
# 4. Deobfuscate (structural transforms + auto-extract renames + format)
cp -r resplit/ decoded/
node src/deobfuscate.mjs --dir decoded/
# 5. Build dependency graph
node src/extract-deps.mjs decoded/manifest.json --out deps-graph.json
# 6. Cluster modules (sweep resolutions, pick the best one)
node src/cluster-graph.mjs deps-graph.json --sweep
node src/cluster-graph.mjs deps-graph.json --pick 1.5 --png
# → clusters-core.json (cluster membership)
# → clusters.png (visualization)
# 7. Label clusters (create cluster-labels.json with directory names)
# Use the PNG + top functions per cluster to name them.
# See "Organizing modules" below for the format.
# 8. Organize into semantic directories
node src/organize.mjs decoded/
# → decoded-organized/ with named directories + INDEX.mdSteps 1-4 are fully automated. Steps 5-8 organize the output into directories — step 7 requires manual labeling (or an LLM).
After deobfuscating (and optionally modifying) individual module files, reassemble them back into a working binary:
# Reassemble deobfuscated modules into a runnable bundle
node scripts/build.mjs <version-dir> --source decoded --no-bun-cjs
# Run it
cd <version-dir>/extracted && bun run.js --versionThe --no-bun-cjs flag wraps the code as a self-executing IIFE instead of relying on Bun's strict @bun-cjs CJS loader, which requires byte-exact formatting incompatible with any code transformation.
After step 4, before organizing:
# Batch rename: send each module to an LLM, get all renames at once
node src/ai-rename.mjs --dir decoded/ --out renames-ai.json
node src/deobfuscate.mjs --dir decoded/ --batch renames-ai.json --only rename
# Scoped rename: per-identifier with AST context (higher quality, more API calls)
node src/ai-rename-scoped.mjs --dir decoded/ --out renames-scoped.json
node src/deobfuscate.mjs --dir decoded/ --batch renames-scoped.json --only renameIf you already have a decoded version and want to update to a newer binary:
# Transfer all artifacts (renames, vendor flags, layout) via content-based matching
node src/transfer-artifacts.mjs new-version/ --reference old-version/
# Or apply the old version's file layout directly
node src/apply-reference-layout.mjs new-decoded/ --reference old-decoded-organized/- Node.js (v18+)
- Bun (optional — only needed to run the extracted code, not to decompile it)
Bun standalone executables embed all JavaScript in a __BUN Mach-O section (macOS), appended ELF data (Linux), or .bun PE section (Windows). The section contains a StandaloneModuleGraph:
[8-byte size header (u64 LE)]
[data buffer: source code, bytecode, native addons, wasm modules...]
[module table: array of CompiledModuleGraphFile structs (52 bytes each)]
[Offsets struct (32 bytes)]
[\n---- Bun! ----\n]
Even with --bytecode compilation, the full JavaScript source is always stored alongside the bytecode (JSC requires it). The --bytecode flag is a startup optimization, not obfuscation.
See docs/BUN.md for comprehensive Bun bundler internals.
resplit.mjs detects Bun's module wrapper patterns:
y((exports, module) => { ... })— CJS modules (__commonJS)h(() => { ... })— ESM lazy initializers (__esm)
Outputs one flat file per module with a dependency graph and export mappings.
match-vendors.mjs --classify identifies npm packages using a fingerprint database of property names and string literals (these survive minification). A reverse-caller flood-fill then propagates: if all callers of a module are already vendor, that module is vendor too.
The fingerprint DB (data/vendor-fingerprints-1000.json, 26MB) covers 23,746 files from 1,668 npm packages. It can be extended with --rebuild-db --npm-dir /path/to/node_modules.
- wakaru — structural transforms:
!0→true,void 0→undefined, comma splitting - lebab — ES5→ES6+ modernization (skipped by default —
var→let/constcauses cross-file collisions when reassembled) - extract — auto-generate rename maps from
MR()export mappings +this.name/displayNamepatterns - rename — format-preserving AST rename via recast (only identifier bytes change, everything else stays byte-identical)
- prettier — consistent formatting
Runtime files (00-runtime.js, 99-main.js) are automatically excluded from wakaru/lebab/prettier. Identifiers declared in runtime files (Bun's module system globals like h, v, y, MR) are auto-excluded from renames — they're referenced by all modules including vendor.
Bun's __export() pattern — MR(target, { exportName: () => minifiedVar }) — preserves original export names as string keys. This is the single biggest source of name recovery. Additional patterns: this.name = "X" in class constructors, displayName = "X" assignments, and 20+ mapped Bun runtime helpers.
For remaining identifiers, AI-assisted renaming (ai-rename.mjs, ai-rename-scoped.mjs) uses LLMs to infer semantic names from context.
After deobfuscation, modules are flat numbered files. The organization pipeline turns them into a meaningful directory structure:
extract-deps.mjsbuilds a function-level dependency graph (which module calls which)cluster-graph.mjs --sweepruns Louvain community detection at 14 resolutions and shows a comparison table — modularity score, cluster count, size distributioncluster-graph.mjs --pick <r>picks a resolution and outputsclusters-core.json(membership) + a PNG visualization- You create
cluster-labels.json— naming each cluster based on its top functions:{ "clusters": { "0": { "directory": "ui", "label": "UI rendering" }, "1": { "directory": "api", "label": "API client" } } } organize.mjsassigns modules to directories using a 4-layer algorithm:- Layer 1: Direct cluster membership
- Layer 2: Function-level affinity (majority vote from cross-module callers/callees)
- Layer 3: Manifest-level affinity (majority vote from import/export deps)
- Layer 4: Remaining modules go to
uncategorized/
diff-versions.mjs and transfer-artifacts.mjs use content-based fingerprinting (strings + properties + exports — never variable names) to match modules across versions and carry over rename artifacts, vendor classifications, and file layout.
| Script | Description |
|---|---|
src/extract.mjs |
Parse Bun binary format, extract all embedded modules |
src/resplit.mjs |
Split bundle into 1-module-per-file |
src/match-vendors.mjs |
Vendor classification: fingerprint DB + flood-fill. --classify, --no-move |
src/deobfuscate.mjs |
Full pipeline: wakaru → lebab → extract → rename → prettier |
build.mjs (in project) |
Reassemble modules into runnable bundle. --no-bun-cjs, --run |
| Script | Description |
|---|---|
src/rename.mjs |
Format-preserving AST rename (recast). Single, batch JSON, --dry-run |
src/extract-exports.mjs |
Auto-extract MR() export mappings → rename JSON |
src/extract-names.mjs |
Extract renames from this.name="X" + displayName="X" patterns |
src/extract-tools.mjs |
Extract names from userFacingName() patterns |
src/extract-errors.mjs |
Extract error class names from factory patterns |
src/ai-rename.mjs |
AI semantic renaming (Gemini). Resumable, concurrent |
src/ai-rename-scoped.mjs |
Scope-ordered per-identifier AI rename (humanify-style) |
| Script | Description |
|---|---|
src/extract-deps.mjs |
Function-level dependency graph. --query, --stats, --dot |
src/cluster-graph.mjs |
Louvain clustering + PNG visualization. --sweep, --pick, --png |
src/organize.mjs |
Assign modules to directories via cluster membership + affinity voting |
| Script | Description |
|---|---|
src/diff-versions.mjs |
Cross-version diffing: fingerprint → match → normalize → diff |
src/transfer-artifacts.mjs |
Transfer artifacts to new versions via content-based module matching |
src/apply-reference-layout.mjs |
Apply reference version's file layout (dirs, filenames, vendor flags) |
- macOS:
__BUN/__bunMach-O segment. Code signature after trailer (~1.7MB padding). - Linux: Data appended to ELF binary. Trailer at very end.
- Windows:
.bunPE section. No 8-byte size header. - Native
.nodeaddons are platform-specific. JS and WASM are portable.
src/ 18 pipeline scripts (all standalone, no cross-imports)
data/
vendor-fingerprints-1000.json Vendor fingerprint DB (26MB, 1,668 packages)
docs/
BUN.md Bun bundler internals & deobfuscation strategies
MIT