Unpack pkg, nexe, caxa, Node.js SEA and Bun single-file binaries.
Keep in mind that this tool doesn't give you the full source code if the application was compiled into bytecode (pkg V8 bytecode, sea snapshots, bun --bytecode bundles). See How it works.
As of this writing, pkg-unpacker supports the following tools:
| Tool | Supported | Versions | Comment |
|---|---|---|---|
| vercel/pkg | ✅ | 5.0.0 - 5.8.1 | N/A |
| yao-pkg/pkg | ✅ | 5.0.0 - 6.23.0 | Does not include experimental support for Node SEA . |
| AngaBlue/exe | ❌ | - | SEA-based output; should unpack via SeaUnpacker (no Windows sample to verify). |
| nexe/nexe | ✅ | 5.0.0-beta.4 | ZIP payload via sentinel footer; latest prebuilt base is Node 14. |
| leafac/caxa | ✅ | 3.0.1 | Self-extracting gzip-tar; Go-stub binaries (shell-stub variant unsupported). |
| bun compile | ✅ | 1.4.3 | Layout may shift per release; --bytecode bundles not recoverable. |
| Node.js SEA | ✅ | Node 20+ blobs | Main script if stored as source, assets always. |
- pkg: trailer holds the virtual filesystem, entrypoint, symlinks,
file dictionary and compression (
NONE,GZip,Brotli,Zstd). Directories, symlinks and stat modes are restored. JavaScript comes out as V8 bytecode unless the binary was built with--no-bytecode. - Node SEA: blob holds the main script (source, unless
useSnapshot) plus raw keyed assets. No modes or symlinks exist in the format. Snapshot mains are reported, not written. - Bun: file table with content-hashed names under
/$bunfs/. Bundled JS comes out as source (or bytecode with--bytecode, which is reported, not written usefully). The table layout is Bun-internal and may shift per release. - nexe: ZIP payload located via the sentinel footer, plus the older JSON-manifest layout. Files come out as stored (usually source).
- caxa: gzip-tar archive plus JSON footer. Files, dirs, symlinks and
modes are restored. Only Go-stub binaries; the shell-stub (
.sh) variant is a shell script around the same tarball.
- Install Bun.
- Download or clone the project.
- Navigate to the project directory.
- Install the dependencies:
bun install
- (Optional) Verify the project:
bun run typecheck bun test - (Optional) Build the project:
Or compile to a single executable:
bun run build
bun run compile
Without cloning, add it from GitHub, then use the linked binary.
With Bun:
bun add github:LockBlock-dev/pkg-unpacker
./node_modules/.bin/pkg-unpacker -i ./pkg_app -o ./unpackedOr in one shot:
bunx github:LockBlock-dev/pkg-unpacker -i ./pkg_app -o ./unpackedWith pnpm:
pnpm add github:LockBlock-dev/pkg-unpacker
./node_modules/.bin/pkg-unpacker -i ./pkg_app -o ./unpackedOr in one shot:
pnpm dlx github:LockBlock-dev/pkg-unpacker -i ./pkg_app -o ./unpackedWith npm (git fetches need allowing):
NPM_CONFIG_ALLOW_GIT=all npm install github:LockBlock-dev/pkg-unpacker
./node_modules/.bin/pkg-unpacker -i ./pkg_app -o ./unpackedThe binary format is auto-detected by default; pass --format to force one
(pkg, sea, bun, nexe, caxa).
To start the application with Bun, run:
bun run start -- -i ./pkg_app -o ./unpackedOr run the source directly:
bun src/main.ts -i ./sea_app -o ./unpacked --format seaThe src/ code (excluding tests, which are Bun-only via bun:test) also runs on Node.js. Relative imports use .js extensions so both runtimes resolve them.
npm run build:node # or: bun run build:node -> emits to dist-node/
node dist-node/main.js -i ./pkg_app -o ./unpacked
node dist-node/main.js -i ./bun_app -o ./unpacked --format bunAs a library on Node.js:
import { createUnpacker } from "pkg-unpacker";
import PkgUnpacker from "pkg-unpacker/pkg/unpacker";--run executes the entrypoint for pkg binaries on Node.js only: it
requires the host version to match the binary's target, otherwise it fails
with an explicit version error. Under Bun it is refused (V8 bytecode can't
run on JavaScriptCore), and for sea, bun, nexe and caxa binaries it
warns and unpacks without executing.
Here’s an overview of the help command output:
Usage: pkg-unpacker [options]
Unpack pkg, nexe, caxa, Node.js SEA and Bun single-file binaries. The format is
auto-detected by default.
Options:
-V, --version output the version number
-i, --input <file> Specify the input binary file path
-o, --output <folder> Specify the output folder path (default: ".")
--run Run the unpacked entrypoint (pkg binaries on Node.js
only) (default: false)
--json Output the unpack result as JSON (default: false)
--format <format> Binary format to unpack (default: auto) (choices:
"pkg", "sea", "bun", "nexe", "caxa", "auto", default:
"auto")
-h, --help display help for command- Unpack a UNIX application (format auto-detected):
$ bun run start -- -i ./pkg_app -o ./unpacked- Unpack a Windows application:
$ bun run start -- -i ./pkg_app.exe -o ./unpacked- Unpack a Node.js SEA binary with an explicit format:
$ bun src/main.ts -i ./sea_app -o ./unpacked --format sea- Unpack a Bun binary on Node.js:
$ node dist-node/main.js -i ./bun_app -o ./unpacked --format bun- Unpack a UNIX application and run it (pkg on Node.js only):
$ bun run start -- -i ./pkg_app -o ./unpacked --runThe main logic of pkg unpacker lies in the PkgUnpacker
class, with multi-format dispatch via createUnpacker,
re-exported from the package root (src/index.ts) so
import "pkg-unpacker" never runs the CLI.
- Unpack a UNIX application:
import PkgUnpacker from "pkg-unpacker/pkg/unpacker";
const main = async () => {
const unpacker = await PkgUnpacker.create("./pkg_app");
await unpacker.unpack("./unpacked");
};
main();- Unpack a Windows application and run it:
import PkgUnpacker from "pkg-unpacker/pkg/unpacker";
const main = async () => {
const unpacker = await PkgUnpacker.create("./pkg_app.exe");
await unpacker.unpack("./unpacked", true);
};
main();- Unpack any supported binary (auto-detected format):
import { createUnpacker } from "pkg-unpacker";
const main = async () => {
const unpacker = await createUnpacker("./app.bin");
await unpacker.unpack("./unpacked");
};
main();- Unpack a Node.js SEA binary with an explicit format:
import { createUnpacker } from "pkg-unpacker";
const main = async () => {
const unpacker = await createUnpacker("./sea_app", "sea");
await unpacker.unpack("./unpacked");
};
main();- Use a format-specific unpacker directly:
import SeaUnpacker from "pkg-unpacker/sea/unpacker";
const main = async () => {
const unpacker = await SeaUnpacker.create("./sea_app");
await unpacker.unpack("./unpacked");
};
main();The ./dispatch subpath (pkg-unpacker/dispatch) also exposes
createUnpacker/detectFormat directly. Per-format subpaths
(pkg-unpacker/sea/unpacker, pkg-unpacker/bun/unpacker,
pkg-unpacker/nexe/unpacker, pkg-unpacker/caxa/unpacker) expose each
unpacker class.
Feed the output directory to REA for analysis:
bun run start -- -i ./pkg_app -o ./unpacked --json > result.json
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/unpacked --json- Auto-detects the binary format (
pkg,sea,bun,nexe,caxa) or unpack a forced format with--format - Detects compression formats (Gzip, Brotli, Zstd) in pkg binaries
- Supports code evaluation for pkg binaries on Node.js (
--run) - Handles symlinks and restores directories and stat modes where the format stores them
- Extracts binaries from all operating systems
- Runs on Bun and Node.js (via
dist-node/)
This application does not decompile code. By default, pkg compiles JavaScript into V8 bytecode. Extracted files will remain in this format, except for assets.
Code evaluation (--run) works best with small pkg applications as dependencies might be broken, and requires the host Node.js version to match the binary's target.
Each format stores its payload differently, see Format notes for the per-format layout:
- pkg stores metadata about file names, paths, offsets, lengths, and compression at the end of each binary. This application analyzes those fields to extract and decompress (if necessary) all embedded files.
- Node.js SEA embeds a blob with the main script and assets.
- Bun embeds a file table with content-hashed names under
/$bunfs/. - nexe embeds a ZIP payload located via the sentinel footer (plus an older JSON-manifest layout).
- caxa embeds a self-extracting gzip-tar archive plus a JSON footer.
Examples:
// UNIX app
{"/snapshot/pkg/index.js":{"0":[0,568],"3":[568,118]},"/snapshot/pkg":{"2":[686,12],"3":[698,117]},"/snapshot":{"2":[815,7],"3":[822,117]}} // virtual file system
,
"/snapshot/pkg/index.js" // entrypoint
,
{} // symlinks
,
{} // files dictionnary
,
0 // 0: no compression, 1: Gzip, 2: Brotli, 3: Zstd// Windows app
{"C:\\snapshot\\pkg\\index.js":{"0":[0,568],"3":[568,118]},"C:\\snapshot\\pkg":{"2":[686,12],"3":[698,117]},"C:\\snapshot":{"2":[815,7],"3":[822,117]}} // virtual file system
,
"C:\\snapshot\\pkg\\index.js" // entrypoint
,
{} // symlinks
,
{} // files dictionnary
,
0 // 0: no compression, 1: Gzip, 2: Brotli, 3: ZstdSee the license.