Claude Code restored for Node.js — extracted from official Bun SEA binaries and patched for Node.js runtime compatibility.
Starting from v2.1.113, Anthropic ships Claude Code as native Bun binaries instead of Node.js-runnable JavaScript. This project restores the npm package format so it can run under Node.js.
Not published to npm — installs from this repo's GitHub Releases. One-shot installer (detects your platform, downloads the latest release, installs globally):
curl -fsSL https://raw.githubusercontent.com/y-cruce/claude-code/main/install.sh | bashOr manually — the main package plus the package for your platform:
VERSION=$(gh release view --repo y-cruce/claude-code --json tagName --jq '.tagName | ltrimstr("v")')
PLATFORM=darwin-arm64 # or darwin-x64 / linux-{x64,arm64}[-musl] / win32-{x64,arm64} / android-arm64
gh release download "v$VERSION" --repo y-cruce/claude-code \
-p "cometix-anthropic-cc-$VERSION.tgz" \
-p "cometix-anthropic-cc-$PLATFORM-$VERSION.tgz"
npm install -g "./cometix-anthropic-cc-$VERSION.tgz" "./cometix-anthropic-cc-$PLATFORM-$VERSION.tgz"The CLI installs as anthropic-cc. Postinstall copies the platform tree and its runtime dependencies into a per-user directory outside npm. If npm's allow-scripts gate skips it, the first launch completes that copy automatically. To install the tree manually:
cd "$(npm root -g)/@cometix/anthropic-cc" && node install.cjs- Downloads official Claude Code binaries from all 8 platforms (darwin/linux/win32 × arm64/x64)
- Extracts the embedded JavaScript and native modules from Bun SEA format
- Patches the code for Node.js compatibility (hardcoded paths, Bun-only APIs, module loading)
- Reassembles into a standard npm package with
vendor/dependencies
The official binary has changed shape twice, so the pipeline detects which one it is looking at before patching (scripts/verify-node-compat.mjs):
| Layout | Versions | Shape |
|---|---|---|
single-cjs |
≤ 2.1.241 | One ~28MB CommonJS bundle, patched as a single cli.js |
split-esm |
≥ 2.1.242 | A ~19KB ESM entry plus ~1400 code-split modules (chunk-*.js, joined by _N.js names since 2.1.246) importing each other through Bun's virtual filesystem (/$bunfs/root/, or B:/~BUN/root/ on Windows) |
Split builds are patched directory-wide by scripts/esm-split-patch.mjs:
| Rewrite | Description |
|---|---|
| E1 | Import specifiers "/$bunfs/root/chunk-x.js" → relative "./chunk-x.js" (~110k per platform), so Node's ESM resolver finds the chunks shipped next to cli.js |
| E2 | The remaining virtual-filesystem literals (~258 at 2.1.242, ~424 at 2.1.246) are runtime paths, not specifiers: native modules resolve through globalThis.__ccVendorNode() to the package's vendor/ copy; assets (mermaid, chart.js, highlight.js, the HTML payload template, and since 2.1.246 embedded .md/.txt prompt files) and the hooks worker resolve through globalThis.__ccAsset() |
| E3 | import.meta.require (Bun-only) → a createRequire wrapper that returns file content for .md/.txt paths, matching Bun's text loader — since 2.1.246 chunks require() embedded prompt files and would otherwise crash at startup compiling markdown as JS |
| E4 | The P6 polyfill ships as bun-polyfill.mjs, imported first by cli.js and by the hooks worker so globalThis.Bun exists before any chunk body runs |
| E5 | Since 2.1.250 chunks import.meta.require() sibling chunks, often at top level and inside import cycles, which Bun tolerates by re-entering evaluation but Node's require(esm) rejects (ERR_REQUIRE_CYCLE_MODULE). Three-layer emulation: top-level chunk requires get a hoisted bare import so the target is usually already evaluated (a plain cache hit); a require that still lands mid-cycle returns a lazy namespace proxy that re-requires on first access; a property grabbed off that proxy while the cycle is still evaluating (var X=require(chunk).Prop) returns a callable lazy value proxy, cached per (module, prop) so every grabber sees the identical object and === still holds |
| E7 | claude plugin test children receive Node's VM Modules flags after environment filtering, so the testing kit can load SourceTextModule without a global NODE_OPTIONS setting |
The patches below still apply to split builds. Only the few chunks carrying their marker strings are AST-walked — parsing all 1385 modules would cost minutes per platform.
Applied by scripts/node-compat-patch.mjs — to the extracted cli.js (~27MB) on single-cjs builds, and to the matching chunks on split-esm builds. Each patch degrades gracefully: a pattern that no longer matches logs a warning instead of aborting, and the result is AST-validated before packaging.
| Patch | Description |
|---|---|
| P1 | Hardcoded CI build paths baked in at compile time (fileURLToPath("file:///home/runner/...") / createRequire("file:///home/runner/...")) → runtime __filename / require |
| P2 | if (typeof Bun > "u") throw Error("Bun required") guard around Bun.Transpiler → graceful null return. Not present since recent versions — the P6 polyfill covers it |
| P3 | require("/$bunfs/root/*.node") — Bun SEA's virtual filesystem paths for native modules → resolved from the package's vendor/ directory |
| P5 | EMBEDDED_SEARCH_TOOLS guard restored (P5a: env var check, P5b: binary availability check) — enables Grep/Glob Tool by default; set EMBEDDED_SEARCH_TOOLS=true to switch to bfs/ugrep Bash shadow mode (falls back to Tool mode if binaries are missing) |
| P6 | Global Bun polyfill shim injected at the top of the file. Implements Bun.spawn (Subprocess-like interface), Bun.file, Bun.listen (TCP), Bun.serve (HTTP/S), Bun.hash, Bun.deepEquals, Bun.stdin, Bun.zstdDecompress/zstdDecompressSync (embedded text assets ship zstd-compressed *.md.zst since 2.1.251; backed by node:zlib's zstd, Node ≥ 22.15); Bun.JSONL.parseChunk is intentionally null so business code takes its own fallback path; Bun.SQL throws a clear not-implemented error; Bun.Terminal/Bun.WebView/heap-snapshot APIs are guarded no-ops. Skipped when the code contains ≥10 typeof Bun guards (dual-runtime fallbacks already present) |
| P7 | Bundled HttpsProxyAgent exposed as globalThis.__HttpsProxyAgent — Node's ws needs an explicit agent to honor HTTP(S) proxies, unlike the Bun runtime |
| P8 | AF_() shadow function patched — the official binary is a multicall executable that impersonates bfs/ugrep via ARGV0; under Node.js the binaries are resolved from PATH via which instead |
| P9 | Package name rebranded: all @anthropic-ai/claude-code references (~250 occurrences) → @cometix/anthropic-cc, so the built-in auto-updater installs this package instead of the official Bun build |
| P10 | CONNECT tunneling for axios behind HTTP(S)_PROXY (ships inside the P6 polyfill). The bundled axios sends HTTPS requests to the proxy as absolute-form cleartext HTTP instead of opening a CONNECT tunnel, so its clients — most visibly claude remote-control registration — die with Registration: Failed with status 400 behind a proxy (upstream bug, the official Bun build fails the same way). The polyfill intercepts http.request calls whose path is an absolute https:// URL (only axios's proxy mode produces those) and reissues them as real HTTPS requests tunneled via CONNECT |
Outside the patcher, the package also ships bun-ink-compat.cjs (precompiled ansi-regex/strip-ansi/string-width/ansi-styles/wrap-ansi for terminal text handling) and install.cjs (postinstall: detects platform incl. musl/Android and installs an isolated runtime tree).
Claude Code has two search paths, controlled by the EMBEDDED_SEARCH_TOOLS environment variable:
| Mode | Env setting | Search method | Requirements |
|---|---|---|---|
| Tool mode (default) | unset | Grep/Glob Tool → ripgrep (bundled) | None |
| Shadow mode | =true |
Bash find → bfs, grep → ugrep |
bfs + ugrep installed |
In Tool mode, the model uses the built-in Grep and Glob tools powered by bundled ripgrep. In Shadow mode, find/grep commands in the Bash tool are redirected to bfs/ugrep for enhanced search.
If EMBEDDED_SEARCH_TOOLS=true is set but bfs/ugrep are not installed, it automatically falls back to Tool mode.
# Tool mode (default, recommended)
claude
# Shadow mode (requires: brew install bfs ugrep)
EMBEDDED_SEARCH_TOOLS=true claudeThe npm main package contains a small cli.js launcher, install.cjs,
runtime.mjs, bun-ink-compat.cjs and SDK types. The launcher selects exactly
its package version. Runtime trees live under
${XDG_DATA_HOME:-~/.local/share}/anthropic-cc/versions/<version>/build-<id>/
on macOS/Linux, or %LOCALAPPDATA%/anthropic-cc/versions/ on Windows
(falling back to ~/AppData/Local). Set ANTHROPIC_CC_DATA_DIR to override
the anthropic-cc data directory, including for temporary installs.
Each build contains its own node_modules/, assets and binaries. Installation
copies into a temporary sibling directory, renames the complete tree into
place, then atomically replaces the version's current.json pointer. A
same-version rebuild gets a new build directory. Running sessions and their
background CLI processes keep their original paths; each runtime entry records
its PID before loading Claude Code. Install-time pruning checks those PIDs,
keeps all live builds plus the current and previous installed versions, and
removes unused older trees. PID reuse can conservatively retain an unused tree.
The main package carries a build ID, so a skipped postinstall for a same-version
re-release still activates that build on the next launch.
The first upgrade from the old in-package layout still requires restarting
sessions launched before this isolation support was installed.
Within each runtime tree:
cli.js PID registration and runtime entry
runtime-entry.js Original Node.js entry point
node_modules/ Private runtime dependencies (including transitive deps)
vendor/
├── ripgrep/ Code search (6 platforms)
├── audio-capture/ Voice input (6 platforms)
└── seccomp/ Linux sandbox (arm64 + x64)
On split-esm builds (2.1.242+) the entry is joined by the rest of the module tree:
runtime-entry.js Original ESM entry point
bun-polyfill.mjs Bun API shim, imported first
chunk-*.js, _*.js ~1400 code-split modules
*.md, *.txt Embedded prompt texts (since 2.1.246)
mermaid.min.js Assets loaded at runtime
chart.umd.min.js
hljsBundle.generated.min.js
payload.template.html.asset
src/plugins/functionHooks/hooks-worker/hooks-worker.js
This fork does not publish to npm — builds produce GitHub Release artifacts only (see Install). A scheduled run at 22:13 UTC daily picks up new upstream versions automatically; a specific version can also be built manually:
gh workflow run release.yml -f version=<x.y.z>Fork changes on top of upstream:
- Support the flattened Bun SEA layout introduced in v2.1.229 (
cli.jsmoved fromsrc/entrypoints/to the extract root) — this is what stalls upstream builds from v2.1.229 on - Support the split-ESM layout introduced in v2.1.242 (one CJS bundle replaced by an ESM entry plus ~1385 chunks) — see Bundle layouts
- Bun text-loader semantics for
require()of embedded.md/.txtprompt assets — without this, ≥ 2.1.246 crashes at startup (see E3) - Daily scheduled build (00:00 UTC) that auto-detects and releases new upstream versions
- CI verify step fails on startup crashes (
pipefail— a crash behind| headused to pass) and smoke-testsmcp list - Retry binary downloads on transient CDN errors (
curl --retry) - npm publish job removed from the release workflow
This project redistributes Claude Code under Anthropic's terms. Vendored dependencies retain their original licenses (ripgrep: Unlicense/MIT, seccomp: Apache-2.0).