Extract source code from Node.js Single Executable Applications (SEA) and Bun standalone binaries — ELF (Linux) and PE (Windows).
Requires Python 3.14+.
Install from source:
git clone https://github.com/L5Z12/seaextract.git
cd seaextract
uv sync# Extract source code from a Node.js SEA binary
seaextract myapp
# Extract to a specific directory
seaextract myapp -o output/
# Show binary info without extracting
seaextract myapp --info
# Dump raw SEA blob to stdout
seaextract myapp --raw > blob.bin
# Repack modified files back into a binary
seaextract myapp --repack output/ -o myapp.repacked
# Pack from scratch — inject a fresh SEA blob into a plain node.exe
seaextract node.exe --repack src/ --format sea -o myapp.exe
# Pack from scratch — inject a fresh Bun payload into a plain bun runtime
seaextract bun.exe --repack src/ --format bun -o myapp.exe
# SEA-only shortcut: replace just the main JS source
seaextract myapp --source patched.js -o myapp.repacked- Legacy (raw JS blob) and v2 (magic + flags) formats
- JavaScript source extraction
- V8 heap snapshot detection
- Code cache extraction
- Embedded asset extraction
- SEA fuse state detection
- UPX compression warning
- Module graph extraction with original file paths
- Loader and module format metadata (ESM/CJS)
- Entry point identification
- Support for all Bun loader types (js, ts, jsx, tsx, json, toml, css, wasm, native, etc.)
uv sync
uv run pytest
uv run ruff check .
uv run ruff format .The --repack DIR flag reads modified files from an extracted directory and produces a new binary. It works in two modes:
- Repack: the base binary already contains a SEA/Bun payload — used as a template so metadata (flags,
code_path, module ordering, loaders, entry index) is preserved. Format is auto-detected. - Pack from scratch: the base binary is a plain runtime (
node.exe,bun, unmodified from official downloads) with no existing payload. Pass--format {sea,bun}to disambiguate. Defaults are chosen to match whatpostjectandbun build --compileproduce.
Behaviour per (format × container):
- PE SEA: repack in place when the new blob fits the original resource slot; otherwise rewrite the entire
.rsrcresource tree and relocate the section past all others (postject's approach). The section table is re-sorted by virtual address and the vacated section is filled to keep the loader happy. The SEA fuse (:0/:1) is flipped automatically on fresh packs. Works end-to-end against a plainnode.exe. - PE Bun: repack in place when a valid
.bunsection already holds a Bun standalone payload; otherwise add a new.bunsection header (needs a free slot in the header page — most bun/node runtimes have that). Content goes at EOF,SizeOfImagegrows to cover it. Works end-to-end against a plainbun.exe. - ELF Bun: repack in place when the existing
.bunsection already holds a standalone; otherwise (fresh bun runtime) append content, repurpose the existing.bunsection header with an updatedsh_offset/sh_addr, and extend the lastPT_LOADsegment so the new virtual address is mapped — while preserving the originalp_memszso Bun's BSS-mapped data isn't shrunk. Writes a pointer at the original.bunfile location so Bun's runtime lookup finds the payload. Works end-to-end against a plain linuxbun. - ELF SEA: repack rewrites the containing
PT_NOTEsegment in place, preserving co-tenant notes (build-id, annobin, etc.) byte-for-byte. Fresh pack requires an existingPT_NOTEwith enough padding to hold the new note; a plainnodebinary typically doesn't have that slack, so the fresh path errors out clearly. Same-size and shrink still work for repack. - Bun bytecode: precompiled bytecode is dropped on any Bun repack (it would be stale against modified source); Bun's runtime re-parses the JS at startup.
- Signed binaries: repacking invalidates Authenticode signatures on PE. Trailing signature overlays are dropped from PE Bun binaries.
- ELF (Linux) and PE (Windows) binaries are supported. Mach-O (macOS) is not yet implemented.
- V8 heap snapshots embedded via
useSnapshotcannot be converted back to JavaScript. - ELF SEA fresh packing is only possible when an existing
PT_NOTEsegment has enough padding (~300 bytes for a minimal blob). A plain Linuxnodebinary does not — postject handles this by extending the containingPT_LOAD, which this tool doesn't yet do. - Repacking SEA to a larger payload than the original resource slot on PE falls back to the fresh-pack path (which relocates
.rsrc). On ELF, PT_NOTE growth is rejected with a clear error.
GNU General Public License 3.0. See LICENSE.