Skip to content

Fix: Bun bytecode extraction — fall back to npm source for patchable JS#655

Open
LeonFedotov wants to merge 4 commits intoPiebald-AI:mainfrom
LeonFedotov:feat/fix-bytecode-extraction
Open

Fix: Bun bytecode extraction — fall back to npm source for patchable JS#655
LeonFedotov wants to merge 4 commits intoPiebald-AI:mainfrom
LeonFedotov:feat/fix-bytecode-extraction

Conversation

@LeonFedotov
Copy link
Copy Markdown

@LeonFedotov LeonFedotov commented Apr 1, 2026

Summary

Fixes patch failures on native binary installs where the extracted JS contains Bun bytecode (// @bun @bytecode prefix). Function bodies in bytecode are compiled and can't be regex-patched, causing most tweakcc patches to silently fail.

Fix: When bytecode is detected after extraction, fetch the matching version's readable cli.js from the npm registry. When repacking, clear the bytecode field so Bun uses the patched source JS.

Fixes #628, #629, #635, #645, #651.

How it works

Native binary extraction (before this fix):
  LIEF extract → module.contents = "// @bun @bytecode..." → regex fails ✗

Native binary extraction (after this fix):
  LIEF extract → detect bytecode prefix → npm pack @anthropic-ai/claude-code@X.Y.Z →
  extract cli.js (readable) → patch regex succeeds ✓ →
  repack with bytecode field cleared → Bun runs patched source JS

Why bytecode appears

Recent Claude Code native binaries (2.1.81+) are compiled with Bun's bytecode format. The module graph stores:

  • contents: source wrapped with // @bun @bytecode prefix — function bodies are compiled bytecode, not readable JS
  • bytecode: the separate compiled bytecode blob

When repacking with patched source, the stale bytecode field must be cleared (zeroed) so Bun falls back to interpreting the source JS instead of executing the outdated bytecode.

Changes

File Change
src/nativeInstallation.ts fetchNpmSource() — downloads cli.js from npm via npm pack; bytecode detection in extractClaudeJsFromNativeInstallation; clear bytecode on repack
src/nativeInstallationLoader.ts Pass version parameter through to extraction
src/patches/index.ts Pass ccInstInfo.version to extraction call

Edge cases

  • No network: Falls back to returning the bytecode content as-is (same behavior as before this fix)
  • Version not on npm: Same fallback — returns bytecode content
  • npm installs: Unaffected — they already have readable cli.js
  • Version detection callers (installationDetection.ts, lib/content.ts): Don't pass version, so no npm fetch is triggered — they only need the version string from comments, which is readable even in bytecode
  • npm pack latency: ~3-5s on first run (cached by npm afterward)

Testing

  • pnpm lint passes (typecheck + ESLint)
  • pnpm run test passes (221 total, 0 failures)
  • pnpm prettier --check src passes
  • Manual test: native binary 2.1.89 with bytecode → npm fallback → patches apply

Summary by CodeRabbit

  • Bug Fixes

    • Detects non-readable native bytecode and falls back to fetching readable package source when extraction fails.
  • New Features

    • Option to clear embedded bytecode when repacking native installers for a readable output.
    • Read/write content operations now convey and accept a bytecode-clear flag so patching respects native bytecode state.
  • Tests

    • Updated and added tests/mocks to cover the new return shape and repack behavior.

When the native binary contains Bun bytecode (// @Bun @bytecode prefix),
function bodies are compiled and can't be regex-patched. This affects
all tweakcc patches on recent native installs.

Fix: after extracting the claude module, detect the bytecode prefix.
If found, fetch the matching version's readable cli.js from the npm
registry via `npm pack`. When repacking, clear the bytecode field so
Bun uses the patched source JS instead of stale compiled bytecode.

This fixes Piebald-AI#628, Piebald-AI#629, Piebald-AI#635, Piebald-AI#645, Piebald-AI#651 — all reporting patch failures
on native binary installs.

Changes:
- nativeInstallation.ts: Add fetchNpmSource(), bytecode detection in
  extraction, clear bytecode on repack when contents are replaced
- nativeInstallationLoader.ts: Pass version parameter through
- patches/index.ts: Pass ccInstInfo.version to extraction
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7ed428ef-8c7f-4889-bda2-4b88de501fdf

📥 Commits

Reviewing files that changed from the base of the PR and between de07c47 and 0f7847a.

📒 Files selected for processing (1)
  • src/commands.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/commands.ts

📝 Walkthrough

Walkthrough

Adds Bun bytecode detection and an npm-pack fallback when native extraction yields Bun bytecode; changes extraction/repack APIs to return/accept a clearBytecode flag and optional version, and threads that flag through repacking and higher-level content I/O paths.

Changes

Cohort / File(s) Summary
Native extraction & npm fallback
src/nativeInstallation.ts
Detects Bun bytecode via BUN_BYTECODE_PREFIX; adds fetchNpmSource(version) that runs npm pack and extracts package/cli.js; extractClaudeJsFromNativeInstallation now returns `{ data: Buffer
Repack & bytecode handling
src/nativeInstallation.ts
rebuildBunData and repackNativeInstallation updated to accept/propagate clearBytecode; when true, the module's bytecode pointer is replaced with Buffer.alloc(0) instead of preserving original bytecode.
Call-site API updates
src/nativeInstallationLoader.ts, src/patches/index.ts, src/installationDetection.ts, src/lib/content.ts, src/lib/index.ts, src/commands.ts
Call sites updated to consume/forward { data, clearBytecode } and optional version; repackNativeInstallation invocations now include clearBytecode. Adjusted destructuring and null checks where buffer was previously returned directly.
Tests & new coverage
src/tests/config.test.ts, src/tests/content.test.ts
Mocks updated to new extraction return shape; new tests validate readContent/writeContent behaviors including native negative-case and that clearBytecode is forwarded to repacking for native installs.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI Tool
    participant NativeBin as Native Binary
    participant NPM as npm (shell)
    participant FS as Filesystem
    participant Repacker as Repack Logic

    CLI->>NativeBin: extractClaudeJsFromNativeInstallation(path, version?)
    NativeBin-->>CLI: { data: Buffer|null, clearBytecode: boolean }
    alt data is Bun bytecode and version provided
        CLI->>NPM: npm pack `@anthropic-ai/claude-code`@version
        NPM-->>FS: write tarball
        FS-->>NPM: extract package/cli.js -> Buffer
        NPM-->>CLI: cli.js Buffer
        CLI->>Repacker: repackNativeInstallation(binPath, modifiedJs, outPath, clearBytecode=true)
    else data is readable JS or no bytecode clear needed
        CLI->>Repacker: repackNativeInstallation(binPath, modifiedJs, outPath, clearBytecode=false)
    end
    Repacker->>FS: write repacked binary
    FS-->>CLI: repack completed
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • bl-ue
  • vianch

Poem

🐰 In a burrow of bytes I sniff the trail,
If Bun hides the script behind a veil,
I hop to npm, fetch a friendly file,
Patch the bytes with a tinkered smile,
Back to the burrow—code runs without fail.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: fixing Bun bytecode extraction by implementing a fallback to npm source for patchable JavaScript.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/nativeInstallation.ts (1)

707-714: Please drop the new explanatory comments in this file.

These additions mostly restate the control flow and go against the repo's comment-light rule.

As per coding guidelines, "Do not add comments unless explicitly requested".

Also applies to: 724-724, 732-732, 752-756, 802-803, 878-879

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nativeInstallation.ts` around lines 707 - 714, Remove the new explanatory
comments that were added to nativeInstallation.ts (specifically the block
comment starting "/** Fetches the readable cli.js source from the npm
package..." and the subsequent short explanatory lines added at the other listed
locations). Delete those comment blocks/lines so the code follows the repo's
comment-light rule; do not change the implementation or behavior of the
function(s) (leave the function body and any identifiers intact).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/nativeInstallation.ts`:
- Around line 761-764: The function extractClaudeJsFromNativeInstallation
currently returns a Buffer|null but loses the information whether we purposely
cleared bytecode (npm fallback) or are returning the original bytecode; thread a
clearBytecode flag through the API so callers can distinguish fallback vs
original. Specifically, change extractClaudeJsFromNativeInstallation (and the
related code around the other occurrence at 812-827) to propagate a
boolean/metadata (e.g., {data: Buffer|null, clearBytecode: boolean}) or
accept/return a clearBytecode parameter; update nativeInstallationLoader.ts,
patches/index.ts, and rebuildBunData() to accept and forward that flag, and when
fetchNpmSource() returns null or version is missing ensure clearBytecode is
false (preserve original module.bytecode), while when npm fallback is used set
clearBytecode true so callers know to strip module.bytecode.
- Around line 719-737: Replace the two execSync calls with execFileSync using
argument arrays to avoid shell interpolation: call execFileSync('npm', ['pack',
`@anthropic-ai/claude-code@${version}`, '--pack-destination', tmpDir], {
stdio:'pipe', timeout:30000, cwd: tmpDir }) and call execFileSync('tar', ['xzf',
path.join(tmpDir, tgz), 'package/cli.js'], { stdio:'pipe', timeout:30000, cwd:
tmpDir }); keep the same options (stdio, timeout, cwd), import/ensure path is
available for path.join, and reference the tmpDir, version and tgz variables
used in the original block so no shell interpolation is performed.

---

Nitpick comments:
In `@src/nativeInstallation.ts`:
- Around line 707-714: Remove the new explanatory comments that were added to
nativeInstallation.ts (specifically the block comment starting "/** Fetches the
readable cli.js source from the npm package..." and the subsequent short
explanatory lines added at the other listed locations). Delete those comment
blocks/lines so the code follows the repo's comment-light rule; do not change
the implementation or behavior of the function(s) (leave the function body and
any identifiers intact).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 396b1fd5-d0bc-4202-ac5b-0d3462e38f75

📥 Commits

Reviewing files that changed from the base of the PR and between bc5cdbe and bdee648.

📒 Files selected for processing (3)
  • src/nativeInstallation.ts
  • src/nativeInstallationLoader.ts
  • src/patches/index.ts

Comment thread src/nativeInstallation.ts Outdated
Comment thread src/nativeInstallation.ts Outdated
- Use execFileSync with arg arrays instead of execSync (no shell)
- Thread clearBytecode flag so bytecode is only cleared on npm fallback
- Remove explanatory comments (repo style)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/content.ts (1)

30-44: ⚠️ Potential issue | 🟠 Major

Thread the clearBytecode flag from readContent to writeContent to prevent stale bytecode from persisting.

The readContent function receives clearBytecode from extractClaudeJsFromNativeInstallation but only destructures the data field, discarding the flag. Consequently, writeContent always passes false to repackNativeInstallation, which prevents bytecode clearing even when needed (when npm fallback is used).

Since these functions are always paired in the read-modify-write flow (e.g., src/commands.ts lines 403–451, 474–534, 600–674, 692–745), the flag must be threaded through. Otherwise, a native binary with Bun bytecode will retain stale compiled code after patching.

Solution: Return clearBytecode from readContent and pass it to writeContent:

  1. Modify readContent to return both content and flag
  2. Accept clearBytecode parameter in writeContent and pass it to repackNativeInstallation

Compare with src/nativeInstallation.ts which correctly handles this flag.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/content.ts` around lines 30 - 44, readContent currently discards the
clearBytecode flag returned by extractClaudeJsFromNativeInstallation; change
readContent to return both the content string and the clearBytecode boolean
(e.g., { content, clearBytecode }) when installation.kind === 'native', and
update callers accordingly. Update writeContent to accept a clearBytecode
parameter and forward that boolean to repackNativeInstallation instead of always
passing false. Use the symbols readContent, writeContent,
extractClaudeJsFromNativeInstallation, and repackNativeInstallation to locate
the changes and mirror the handling in nativeInstallation.ts so the
read-modify-write flow preserves the clearBytecode flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/lib/content.ts`:
- Around line 30-44: readContent currently discards the clearBytecode flag
returned by extractClaudeJsFromNativeInstallation; change readContent to return
both the content string and the clearBytecode boolean (e.g., { content,
clearBytecode }) when installation.kind === 'native', and update callers
accordingly. Update writeContent to accept a clearBytecode parameter and forward
that boolean to repackNativeInstallation instead of always passing false. Use
the symbols readContent, writeContent, extractClaudeJsFromNativeInstallation,
and repackNativeInstallation to locate the changes and mirror the handling in
nativeInstallation.ts so the read-modify-write flow preserves the clearBytecode
flag.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b0d2564c-d041-45a9-9868-7ae3b4ab068f

📥 Commits

Reviewing files that changed from the base of the PR and between bdee648 and a44b127.

📒 Files selected for processing (6)
  • src/installationDetection.ts
  • src/lib/content.ts
  • src/nativeInstallation.ts
  • src/nativeInstallationLoader.ts
  • src/patches/index.ts
  • src/tests/config.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/nativeInstallationLoader.ts

@LeonFedotov
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/tests/content.test.ts (1)

25-42: Add an assertion that installation.version is passed to extraction.

These tests validate return shape well, but they don’t verify the call contract that drives npm-version fallback selection.

✅ Suggested assertion
   const result = await readContent(installation);
   expect(result).toEqual({ content: jsContent, clearBytecode: true });
+  expect(
+    nativeInstallation.extractClaudeJsFromNativeInstallation
+  ).toHaveBeenCalledWith('/usr/bin/claude', '1.0.0');

As per coding guidelines, "Test edge cases and error conditions in test files".

Also applies to: 44-61

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tests/content.test.ts` around lines 25 - 42, The tests for readContent
need to assert that installation.version is passed through to the native
extraction call: update the two native-installation tests (the one using
nativeInstallation.extractClaudeJsFromNativeInstallation at lines shown) to
include an expectation like
vi.mocked(nativeInstallation.extractClaudeJsFromNativeInstallation).toHaveBeenCalledWith(expect.objectContaining({
version: installation.version })) or an equivalent call-arg assertion after
invoking readContent; ensure you reference the same installation object used in
the test and assert the mock was called with that version so the npm-version
fallback contract is verified.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands.ts`:
- Line 403: The unpack→repack flow drops the clearBytecode signal (readContent
currently assigned at const { content } = await readContent(installation)) and
later hardcodes false, letting bytecode-native installs keep executing embedded
bytecode; modify readContent usage to destructure and preserve the clearBytecode
flag (e.g., const { content, clearBytecode } = await readContent(installation))
and pass that flag through to the repack/write step instead of hardcoding false
so the repack operation removes embedded bytecode when clearBytecode is true
(ensure the repack/pack/write function that currently receives false uses the
propagated clearBytecode value).

---

Nitpick comments:
In `@src/tests/content.test.ts`:
- Around line 25-42: The tests for readContent need to assert that
installation.version is passed through to the native extraction call: update the
two native-installation tests (the one using
nativeInstallation.extractClaudeJsFromNativeInstallation at lines shown) to
include an expectation like
vi.mocked(nativeInstallation.extractClaudeJsFromNativeInstallation).toHaveBeenCalledWith(expect.objectContaining({
version: installation.version })) or an equivalent call-arg assertion after
invoking readContent; ensure you reference the same installation object used in
the test and assert the mock was called with that version so the npm-version
fallback contract is verified.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a5952118-9163-40d2-ba02-03be08304607

📥 Commits

Reviewing files that changed from the base of the PR and between a44b127 and de07c47.

📒 Files selected for processing (4)
  • src/commands.ts
  • src/lib/content.ts
  • src/lib/index.ts
  • src/tests/content.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/lib/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/content.ts

Comment thread src/commands.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Patching failures on Claude Code v2.1.81

1 participant