Skip to content

cli: declare Node's value-taking flags so their value is not parsed as the entrypoint - #34100

Open
robobun wants to merge 3 commits into
mainfrom
farm/e584128b/cli-node-value-flags
Open

cli: declare Node's value-taking flags so their value is not parsed as the entrypoint#34100
robobun wants to merge 3 commits into
mainfrom
farm/e584128b/cli-node-value-flags

Conversation

@robobun

@robobun robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

bun --experimental-loader ./hooks.mjs app.mjs silently runs hooks.mjs as the program and never executes app.mjs. The command exits 0, so CI wrappers and tooling that still emit the legacy loader flag appear to succeed while running the wrong file.

$ bun --experimental-loader ./hooks.mjs app.mjs   # app.mjs: console.log('APP_RAN')
# hooks.mjs top-level runs instead; APP_RAN never prints; exit 0

Node runs app.mjs here (and registers the loader).

Cause

The streaming arg parser in src/clap/streaming.rs skips unrecognized long flags and resumes at the next argument. When the unrecognized flag takes a value (--experimental-loader ./hooks.mjs), the value is the next argument and gets parsed as the first positional, i.e. the entrypoint. The comment above the existing --trace-event-* declarations in Arguments.rs already describes this exact hazard: value-taking Node flags must be declared so the parser consumes the value.

--experimental-loader was missing from that set, along with ~50 other value-taking options from Node's src/node_options.cc.

Fix

Declare the missing value-taking Node.js options in RUNTIME_PARAMS_ with no help text (so they remain hidden from --help). Because RUNTIME_PARAMS_ feeds AUTO_PARAMS, RUN_PARAMS, and the run-as-node table, this covers bun, bun run, and bun-as-node. The flags are otherwise no-ops in Bun; the only behavior change is that their value argument is consumed instead of becoming the entrypoint, and they show up in process.execArgv (matching Node).

The --flag=value form already worked because an unrecognized --flag=value is skipped as a single token; only the space-separated form was affected.

Verification

New parameterized tests in test/cli/run/as-node.test.ts spawn bun <flag> ./value.mjs app.mjs for each declared flag (and again with bun run) and assert app.mjs runs with process.argv.slice(2) == ['scriptarg'] and process.execArgv == [flag, './value.mjs']. Before the fix 111/112 of these cases ran value.mjs as the program instead.


[review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 117 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/run/as-node.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (3ddbe6019)

test/cli/run/as-node.test.ts:
(pass) fake node cli > the node cli actually works [643.88ms]
(pass) fake node cli > doesnt resolve bins [546.82ms]
(pass) fake node cli > doesnt resolve scripts [620.58ms]
(pass) fake node cli > can run a script named run.js [618.25ms]
(pass) fake node cli > entrypoint file extension picking > picks tsx over any other ext [634.41ms]
(pass) fake node cli > entrypoint file extension picking > picks jsx over ts [640.30ms]
(pass) fake node cli > entrypoint file extension picking > picks mts over ts [627.44ms]
(pass) fake node cli > entrypoint file extension picking > picks ts over js/cjs/etc [874.01ms]
(pass) fake node cli > node -e  [825.12ms]
(pass) fake node cli > process args work [673.79ms]
(pa
... (truncated)

release without fix: 4 FAILED
bun test v1.4.0-canary.1 (130f04c72)

test/cli/run/as-node.test.ts:
(pass) fake node cli > the node cli actually works [15.93ms]
(pass) fake node cli > doesnt resolve bins [13.32ms]
(pass) fake node cli > doesnt resolve scripts [12.86ms]
(pass) fake node cli > can run a script named run.js [12.75ms]
(pass) fake node cli > entrypoint file extension picking > picks tsx over any other ext [12.91ms]
(pass) fake node cli > entrypoint file extension picking > picks jsx over ts [12.74ms]
(pass) fake node cli > entrypoint file extension picking > picks mts over ts [12.61ms]
(pass) fake node cli > entrypoint file extension picking > picks ts over js/cjs/etc [13.26ms]
(pass) fake node cli > node -e  [13.17ms]
(pass) fake node cli > process args work [13.61ms]
(pass) fake node cli > no args is exit code zero for now [4.42ms]
(pass) node value-taking CLI flags do not eat the entrypoint > --experimental-loader ./hooks.mjs app.mjs runs app.mjs, not hooks.mjs [12.71ms]
(pass) node value-taking CLI flags do not eat the entrypoint > bun [] > --allow-fs-write <value> app.mjs runs app.mjs [22.16ms]
(pass) node value-taking CLI flags do not eat the entrypoint > bun [] > --build-sea <va
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/run/as-node.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (3ddbe6019)

test/cli/run/as-node.test.ts:
(pass) fake node cli > the node cli actually works [591.58ms]
(pass) fake node cli > doesnt resolve bins [564.00ms]
(pass) fake node cli > doesnt resolve scripts [631.86ms]
(pass) fake node cli > can run a script named run.js [624.47ms]
(pass) fake node cli > entrypoint file extension picking > picks tsx over any other ext [558.63ms]
(pass) fake node cli > entrypoint file extension picking > picks jsx over ts [625.01ms]
(pass) fake node cli > entrypoint file extension picking > picks mts over ts [633.20ms]
(pass) fake node cli > entrypoint file extension picking > picks ts over js/cjs/etc [554.96ms]
(pass) fake node cli > node -e  [696.71ms]
(pass) fake node cli > process args work [645.27ms]
(pa
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 715ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/21] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 243 extern-C blocks audited
[2/21] gen JS modules (bundle-modules)
Preprocess modules (6720ms)
Bundle modules (28ms)
Postprocesss modules (157ms)
Bundle Functions (763ms)
Generate Code (107ms)

[7.79s] Bundled "src/js" for production
  1912 kb
  162 internal modules
  12 native modules
  90 internal functions across 19 files
[2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

info: c
... (truncated)
diff hotspot
src/runtime/cli/Arguments.rs |  65 +++++++++++++++++++++++
 test/cli/run/as-node.test.ts | 122 ++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 186 insertions(+), 1 deletion(-)

gate history · 3 passed · 0 rejected · iteration 0

evidence per changed file
file                          reads  edits  tests
src/runtime/cli/Arguments.rs      7      4      0
test/cli/run/as-node.test.ts      2      5      0

…s the entrypoint

`bun --experimental-loader ./hooks.mjs app.mjs` silently ran hooks.mjs as the
program and never executed app.mjs: the unknown-flag skip in the arg parser
moved past --experimental-loader but treated its value as the first positional.
The comment above the trace-event declarations already documents this hazard.

Declare --experimental-loader and the rest of Node's value-taking CLI options
(with no help text, so they stay hidden from --help) in RUNTIME_PARAMS_ so the
value is consumed. This covers `bun`, `bun run`, and bun-as-node.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a6e04ef5-6af1-4f52-ab72-38bd1c35aded

📥 Commits

Reviewing files that changed from the base of the PR and between 73b6c14 and af4cdf5.

📒 Files selected for processing (2)
  • src/runtime/cli/Arguments.rs
  • test/cli/run/as-node.test.ts

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

@robobun

robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 8:06 PM PT - Jul 13th, 2026

@robobun, your commit 3ddbe60 has 1 failures in Build #72611 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34100

That installs a local version of the PR into your bun-34100 executable, so you can run:

bun-34100 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. process.execArgv includes user options with the same names as bun's options #25387 - PR correctly populates process.execArgv for declared Node.js flags, fixing incorrect partitioning of user-passed flags
  2. --sourcemap external doesn't work without = (--sourcemap=external)? #12779 - Same root cause: space-separated flag values (e.g., --sourcemap external) are treated as the entrypoint instead of the flag's value

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #25387
Fixes #12779

🤖 Generated with Claude Code

@robobun

robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Neither suggested issue is addressed here (verified against this branch's build):

This PR only declares Node's value-taking flags so their value is consumed instead of becoming the entrypoint.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — mechanical extension of the existing "declare Node value-taking flags" list, following the exact pattern documented at lines 305-310, with a full test matrix. The one inline nit (--test-name-pattern omitted) is an incomplete fix, not a regression, and can be a follow-up.

Extended reasoning...

Overview

Adds ~55 parse_param! declarations to RUNTIME_PARAMS_ in src/runtime/cli/Arguments.rs for Node.js value-taking CLI options that Bun does not implement. Each entry has no help text so it stays hidden from --help. This is the same pattern already used for --trace-event-categories, --trace-event-file-pattern, and --stack-trace-limit immediately above the new block, and the existing comment at lines 305-310 explicitly documents the rationale. Also adds ~120 lines of tests in test/cli/run/as-node.test.ts covering bun, bun run, and the --help hiding.

Security risks

None introduced. The flags are consumed and ignored — no new code paths read their values. A few flags are security-relevant in Node (--allow-fs-read, --disable-proto), but Bun already silently accepts the --flag=value form of these; this PR only makes the space-separated form consistent. That is the pre-existing design, not a new decision.

Level of scrutiny

Low-to-medium. The change is purely additive to a static const table and follows an established, commented pattern. I verified:

  • No collisions with existing declarations: the new --test-* flags (--test-timeout, --test-reporter, --test-shard, etc.) are distinct from TEST_ONLY_PARAMS entries (--timeout, --reporter, --shard), so TEST_PARAMS = TEST_ONLY_PARAMS + RUNTIME_PARAMS_ has no duplicates.
  • --inspect-port/--debug-port don't collide with Bun's existing --inspect <STR>?; --watch-path/--watch-kill-signal don't collide with --watch; --env-file-if-exists is distinct from --env-file; --experimental-loader is distinct from -l/--loader.
  • RUNTIME_PARAMS_ feeds AUTO_PARAMS, RUN_PARAMS, TEST_PARAMS, and BASE_RUNTIME_TRANSPILER_PARAMS, so all relevant subcommands are covered.

Other factors

  • Test coverage is thorough: 55 flags × 2 (bun and bun run) = 110 concurrent spawns asserting the correct entrypoint runs, correct process.argv, and correct process.execArgv; plus a dedicated --experimental-loader case and a --help visibility check.
  • The one inline finding (--test-name-pattern missing) is a nit: that flag was omitted because it already exists in TEST_ONLY_PARAMS, but that table doesn't feed AUTO_PARAMS/RUN_PARAMS. Adding it naively would create a duplicate in TEST_PARAMS, so it needs a slightly different fix — reasonable as a follow-up. It's not a regression; behavior for that one flag is unchanged from before this PR.
  • The stderr: "" exact-match assertion in the primary --experimental-loader test was flagged by finders and ruled out by verifiers; the parameterized suite uses .not.toContain(...) instead, which is the load-bearing coverage.

Comment thread src/runtime/cli/Arguments.rs
…nsumed

--test-name-pattern lives in TEST_ONLY_PARAMS (for `bun test`), which does not
feed AUTO_PARAMS or RUN_PARAMS, so `bun --test-name-pattern foo app.mjs` still
parsed foo as the entrypoint. Declare it in AUTO_OR_RUN_PARAMS (hidden from
--help) rather than RUNTIME_PARAMS_ to avoid a duplicate in TEST_PARAMS.

Also drop the exact stderr match in the primary test in favor of the same
.not.toContain check the parameterized suite uses.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — the --test-name-pattern gap from the last pass is fixed correctly (placed in AUTO_OR_RUN_PARAMS to avoid the TEST_PARAMS collision). The remaining --security-revert note below is a minor completeness gap, not a regression, and can be a follow-up.

Extended reasoning...

Overview

Adds ~55 Node.js value-taking CLI flags as hidden parse_param! entries in RUNTIME_PARAMS_ (plus --test-name-pattern in AUTO_OR_RUN_PARAMS) so the streaming arg parser consumes their value instead of treating it as the entrypoint. Purely additive table entries following the existing documented pattern above --trace-events-enabled. 112 new parameterized tests cover every flag under both bun and bun run, plus a --help visibility check.

Security risks

None. The flags are no-ops in Bun — they're only declared so the parser knows to swallow the following token. No new codepaths execute based on the flag values.

Level of scrutiny

CLI arg parsing is load-bearing, but this change is mechanical: const-table additions with no logic changes. I verified there are no long-name collisions with existing Bun flags in the tables these feed — TEST_ONLY_PARAMS uses --timeout/--reporter/--shard/--coverage-* (not the --test--prefixed Node forms), and existing --inspect*/--watch/--env-file don't overlap the new --inspect-port/--watch-path/--env-file-if-exists. The --test-name-pattern follow-up is correctly placed in AUTO_OR_RUN_PARAMS, which feeds AUTO_PARAMS/RUN_PARAMS but not TEST_PARAMS, so it doesn't duplicate the -t/--grep entry.

Other factors

The prior review round's finding was addressed and verified. The one remaining nit (--security-revert + a couple of newer Node-main aliases) is an incomplete-coverage note, not a correctness issue — those flags behave exactly as they did before this PR, and their typical values don't resolve to files so the failure mode is loud. Test coverage is thorough and the evidence block shows fails-on-main / passes-on-PR for both debug+ASAN and release.

Comment thread src/runtime/cli/Arguments.rs
… is consumed

Node's --security-revert (and the --security-reverts alias) binds to
PerProcessOptions::security_reverts (a string vector) and requires a value.
Verified against Node v26.3.0. --experimental-default-config-file is boolean
there and --experimental-package-map does not exist yet, so neither is added.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — both prior review comments (--test-name-pattern and --security-revert/--security-reverts) are addressed, and no new issues found.

Checked that none of the ~58 new entries collide with existing Bun flags in any table they flow into (AUTO_PARAMS, RUN_PARAMS, TEST_PARAMS); --test-name-pattern in AUTO_OR_RUN_PARAMS correctly stays out of TEST_PARAMS. Verified the empty-description-hides-from-help convention is the documented pattern at line 305. The --experimental-default-config-file / --experimental-package-map omissions are justified against Node v26.3.0.

Extended reasoning...

Overview

Adds ~58 hidden parse_param! declarations to RUNTIME_PARAMS_ (plus one in AUTO_OR_RUN_PARAMS) in src/runtime/cli/Arguments.rs for Node.js CLI options that take a value, so the streaming arg parser consumes the value token instead of treating it as the entrypoint. Adds a parameterized test matrix in test/cli/run/as-node.test.ts covering every declared flag under both bun and bun run, plus a --help visibility check.

Security risks

None. These flags are declared but not acted upon — the only behavior change is that their value argument is consumed rather than becoming the first positional. No new code paths process the flag values; --allow-fs-read/--allow-fs-write/--openssl-config/--tls-* etc. remain no-ops in Bun. Consuming a previously-misparsed argument cannot expand attack surface.

Level of scrutiny

Low-medium. This is a mechanical extension of an already-documented pattern (the existing comment at Arguments.rs:305-310 for --trace-event-* describes exactly this hazard and remedy). The change is additive to a static declaration table with no logic. I verified no collisions: grepped every new flag name against the file — --inspect-port/--inspect-publish-uid are distinct from Bun's --inspect/--inspect-wait/--inspect-brk; --watch-path/--watch-kill-signal distinct from --watch; --env-file-if-exists distinct from --env-file; the new --test-* entries (--test-timeout, --test-reporter, --test-shard, etc.) are distinct from TEST_ONLY_PARAMS' --timeout/--reporter/--shard. The one genuine collision (--test-name-pattern) was correctly routed to AUTO_OR_RUN_PARAMS, which feeds AUTO_PARAMS/RUN_PARAMS but not TEST_PARAMS.

Other factors

Two prior review rounds from me were addressed in follow-up commits (130f04c, 3ddbe60). The PR description's evidence block shows the test file failing 111/113 cases on main and passing on the branch under both ASAN-debug and release. The author verified against Node v26.3.0 that the two flags I flagged as version-dependent (--experimental-default-config-file, --experimental-package-map) are correctly excluded. Tests use the harness conventions (tempDir, bunEnv, bunExe, concurrent pipe drain, exit-code asserted last) and test.concurrent.each for the 58×2 matrix.

@robobun

robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

CI status for 3ddbe60 (build #72611): the diff's test file (test/cli/run/as-node.test.ts) passed on every lane. The remaining reds are all tagged [pre-existing] or [flaky] by ci:errors and unrelated to CLI arg parsing:

  • test-worker-message-port-transfer-terminate.js (pre-existing WebKit assertion, debian x64-asan)
  • node-http-connect.test.ts, zlib/leak.test.ts, napi.test.ts (flaky, Windows x64)
  • 30205.test.ts (flaky LeakSanitizer), test-fs-read-stream-pos.js (flaky)

The cargo clippy failure is in src/runtime/napi/napi_body.rs:2811 from 73b6c14 (#34067) on main, not this diff.

Ready for review/merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant