Skip to content

cli(run): export INIT_CWD to package.json scripts - #36127

Closed
robobun wants to merge 4 commits into
mainfrom
claude/farm/f48df7e0/run-init-cwd
Closed

robobun wants to merge 4 commits into
mainfrom
claude/farm/f48df7e0/run-init-cwd

Conversation

@robobun

@robobun robobun commented Jul 27, 2026 •

Copy link
Copy Markdown
Collaborator

What

bun run <script> now exports INIT_CWD (the directory the command was invoked from) to the script's environment, matching npm and matching what bun install lifecycle scripts already do.

Part of #21088 (the INIT_CWD portion; npm_command and npm_config_local_prefix left for follow-up).

Repro

mkdir -p p/sub/deep && cd p
echo '{"name":"p","version":"1.0.0","scripts":{"envcheck":"echo IC=[$INIT_CWD]"}}' > package.json
cd sub/deep
bun run --silent envcheck

Before: IC=[]
After: IC=[/.../p/sub/deep] (same as npm run --silent envcheck)

Cause

configure_env_for_run_impl seeds npm_lifecycle_event, npm_config_local_prefix, npm_package_*, etc., but never INIT_CWD. The install path (PackageManager::configure_env_for_scripts_run) sets it separately after the fact, which is why bun install lifecycle scripts already had it but bun run did not.

Fix

Set INIT_CWD in configure_env_for_run_impl unconditionally (.put()), sourced from top_level_dir which at that point is the invocation cwd. Unconditional write matches npm (which does process.env.INIT_CWD = process.cwd() at CLI entry) and the npm_lifecycle_event precedent in the same function, so a nested bun run resets INIT_CWD to its own invocation directory rather than inheriting the outer value. The trailing-slash trim uses without_trailing_slash_windows_path so a Windows drive-root cwd C:\ is preserved instead of becoming drive-relative C:.

This covers bun run, bunx, and --filter since they share this env setup. (bun pm pack and install lifecycle hooks go through PackageManager::init which chdirs to the package root before this runs, so they continue to see the package root as INIT_CWD; that is a pre-existing limitation tracked separately.)

Verification

(pass) process.env > INIT_CWD is set to the directory bun run was invoked from
(pass) process.env > INIT_CWD is reset by a nested bun run

Both fail on canary, pass with the fix.

Not in this PR

The other two discrepancies from #21088, left for follow-up:

  • npm_command is run-script (npm uses run)
  • npm_config_local_prefix is the invocation cwd rather than the package root

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

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/run/run-process-env.test.ts
bun test v1.4.0 (5db4a15d5)

test/cli/run/run-process-env.test.ts:
(pass) process.env > npm_lifecycle_event [419.71ms]
(pass) process.env > npm_lifecycle_event should have the value of the last call [523.84ms]
49 |       stderr: "pipe",
50 |     });
51 |     const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
52 | 
53 |     expect(stderr).toBe("");
54 |     expect(JSON.parse(stdout)).toEqual({
                                    ^
error: expect(received).toEqual(expected)

  {
-   "INIT_CWD": "/tmp/init-cwd_94VNtf/sub/deep",
+   "INIT_CWD": "/should/be/overwritten",
    "cwd": "/tmp/init-cwd_94VNtf",
  }

- Expected  - 1
+ Received  + 1

      at <anonymous> (/workspace/bun/test/cli/run/run-process-env.test.ts:54:32)
(fail) process.env > INIT_CWD is set to the directory bun run was invoked from [1237.75ms]
78 |       stderr: "pipe",
79 |     });
80 |     const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (6f9897d99)

test/cli/run/run-process-env.test.ts:
(pass) process.env > npm_lifecycle_event [14.57ms]
(pass) process.env > npm_lifecycle_event should have the value of the last call [15.29ms]
(pass) process.env > INIT_CWD is set to the directory bun run was invoked from [25.14ms]
(pass) process.env > INIT_CWD is reset by a nested bun run [28.97ms]

 4 pass
 0 fail
 8 expect() calls
Ran 4 tests across 1 file. [239.00ms]
__F:0:S:0
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/run-process-env.test.ts
bun test v1.4.0 (5db4a15d5)

test/cli/run/run-process-env.test.ts:
(pass) process.env > npm_lifecycle_event [428.87ms]
(pass) process.env > npm_lifecycle_event should have the value of the last call [524.92ms]
(pass) process.env > INIT_CWD is set to the directory bun run was invoked from [1215.91ms]
(pass) process.env > INIT_CWD is reset by a nested bun run [1344.65ms]

 4 pass
 0 fail
 8 expect() calls
Ran 4 tests across 1 file. [5.49s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 735ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/5] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 240 extern-C blocks audited
[1/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_runtime v0.0.0 (/workspace/bun/src/runtime)
�[1m�[92m   Compiling�[0m bun_bin v0.0.0 (/workspace/bun/src/bun_bin)
�[1m�[92m    Finished�[0m `release` profile [optimized + debuginfo] target(s) in 4m 06s
[2/5] link bun-profile
[3/5] bun-profile --revision
1.4.0-canary.1+5db4a15d5
[5/5] strip bun
[build] done
bun test v1.4.0-canary.1 (5db4a15d5)

test/cli/run/run-process-env.test.ts:
(pass) process.env > npm_lifecycle_event [13.78ms]
(pass) process.env > npm_lifecycle_event should have the value of the last call [14.92ms]
(pass) process.env > INIT_CWD is set to the directory bun run was invoked from [25.99ms]
(pass) process.env > INIT_CWD is reset by a nested bun run
... (truncated)
diff hotspot
src/runtime/cli/run_command.rs       |  8 +++++
 test/cli/run/run-process-env.test.ts | 60 +++++++++++++++++++++++++++++++++++-
 2 files changed, 67 insertions(+), 1 deletion(-)

gate history · 3 passed · 0 rejected · iteration 1

evidence per changed file
file                                  reads  edits  tests
src/runtime/cli/run_command.rs            3      2      0
test/cli/run/run-process-env.test.ts      3      5      0

bun run <script> was not setting INIT_CWD in the script's environment,
while npm sets it to the directory the command was invoked from and
bun's own install lifecycle scripts already do the same. This broke
monorepo idioms like 'cd $INIT_CWD' and tools that locate the caller's
directory.

Set INIT_CWD in configure_env_for_run_impl alongside the other npm_*
env vars, using the same put_default semantics as the install path
(PackageManager.rs) so an explicitly-set INIT_CWD is preserved.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The CLI runner now sets INIT_CWD from the normalized top-level directory. Integration tests cover overriding inherited values and resetting the variable for nested Bun commands.

Changes

INIT_CWD runtime behavior

Layer / File(s) Summary
Set INIT_CWD during runtime environment setup
src/runtime/cli/run_command.rs
configure_env_for_run_impl adds normalized top_level_dir to the process environment as INIT_CWD.
Validate direct and nested CLI runs
test/cli/run/run-process-env.test.ts
CLI tests verify that INIT_CWD overrides inherited values and resets to the inner package directory for nested Bun commands.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: exporting INIT_CWD for bun run scripts.
Description check ✅ Passed The description covers the PR purpose and verification, and includes the required template sections.

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

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. inconsistent env variables between bun and npm npmcommand npm_config_local_prefix and missing INIT_CWD #21088 - This issue reports missing INIT_CWD (among other env var inconsistencies); this PR adds INIT_CWD to bun run script execution, partially addressing the issue.

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

Fixes #21088

🤖 Generated with Claude Code

Comment thread src/runtime/cli/run_command.rs
Comment thread src/runtime/cli/run_command.rs
Comment thread src/runtime/cli/run_command.rs
- put_default -> put so nested bun run resets INIT_CWD to its own
  invocation cwd (matches npm and the npm_lifecycle_event precedent)
- use without_trailing_slash_windows_path so C:\ stays C:\ instead
  of becoming drive-relative C:
- add nested-run test and make both tests assert the overwrite

@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 — all three earlier nits are addressed (unconditional .put(), Windows-safe without_trailing_slash_windows_path, and the bun pm pack claim dropped from the description).

What was reviewed:

  • .put() matches the npm_lifecycle_event precedent at run_command.rs:275/2482; new nested-run test proves the overwrite and seeds a sentinel INIT_CWD to catch leak-through.
  • without_trailing_slash_windows_path delegates to plain without_trailing_slash on unix and preserves C:\\ on Windows — same helper process.cwd() uses on this field.
  • Tests drain stdout/stderr/exited concurrently, use realpathSync to avoid macOS /private symlink mismatches, and assert exit code last.
Extended reasoning...

Overview

Single 6-line addition to configure_env_for_run_impl in src/runtime/cli/run_command.rs that writes INIT_CWD (the invocation cwd) into the script env, plus two new tests in test/cli/run/run-process-env.test.ts. This is the second review pass — commit 6f9897d9 addressed all three nits from the prior round: put_default → unconditional .put(), strings::without_trailing_slash → the Windows-drive-root-safe without_trailing_slash_windows_path, and the PR description no longer claims bun pm pack coverage.

Security risks

None. This exports a directory path the invoking process already knows (its own cwd) into a child process it is about to spawn. No untrusted-input parsing, no privilege boundary crossed.

Level of scrutiny

Low-to-moderate. It's a small Node/npm-compat env-var addition that mirrors an existing pattern in the same function (npm_lifecycle_event uses .put() a few lines away) and an existing behavior in the install path. The helper used for trailing-slash trimming is the established in-tree one that process.cwd() already applies to this exact top_level_dir field.

Other factors

  • The nested-run test is modeled directly on the existing #3589 npm_lifecycle_event nested test two entries above it, and the sentinel INIT_CWD: "/should/be/overwritten" in the spawn env is a stronger assertion than the previous undefined scrub — it would fail if the write were still put_default.
  • fs.realpathSync on both the invoke dir and the package root guards against macOS tmpdir symlink normalization flaking the equality check.
  • bun_paths is already imported in this file; the fully-qualified helper path resolves without new imports.
  • The remaining known limitation (bun pm pack / install lifecycle scripts see the package root, not the true invocation cwd) is pre-existing, now explicitly called out in the description as out of scope, and unchanged by this PR.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/cli/run/run-process-env.test.ts`:
- Around line 30-85: Extend the INIT_CWD tests alongside the existing bun run
cases to cover both bunx and bun run --filter entry points. Add one assertion
per path verifying INIT_CWD equals the directory from which the command was
invoked, including the expected environment override behavior, while preserving
the existing run and nested-run coverage.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7b0c9e58-24dc-4278-bed7-ece8b0661beb

📥 Commits

Reviewing files that changed from the base of the PR and between 4eb6f99 and 5db4a15.

📒 Files selected for processing (2)
  • src/runtime/cli/run_command.rs
  • test/cli/run/run-process-env.test.ts

Comment thread test/cli/run/run-process-env.test.ts

@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 — all three earlier review points are addressed in 6f9897d.

What was reviewed:

  • .put() (unconditional) so nested bun run resets INIT_CWD; nested-run test locks it in.
  • without_trailing_slash_windows_path on top_level_dir — Windows drive-root C:\ preserved; delegates to plain trim on unix.
  • top_level_dir at run_command.rs:613 is the invocation cwd on the bun run/bunx/--filter paths; bun pm pack limitation now correctly noted in the description.
  • Tests: pipes drained concurrently, fs.realpathSync for symlinked tmpdirs, seed INIT_CWD: "/should/be/overwritten" proves overwrite semantics.
Extended reasoning...

Overview

Adds a single env_loader.map.put(b"INIT_CWD", without_trailing_slash_windows_path(top_level_dir)) call in configure_env_for_run_impl (src/runtime/cli/run_command.rs:679-685), plus two tests in test/cli/run/run-process-env.test.ts. This exports INIT_CWD to package.json scripts under bun run, matching npm's process.env.INIT_CWD = process.cwd() at CLI entry.

Prior review resolved

I left three inline comments on the previous revision; all are addressed in commit 6f9897d and reflected in the current diff:

  • put_default → .put() so nested bun run resets to its own invocation cwd (matching the npm_lifecycle_event precedent at lines 275/278). A dedicated nested-run test now asserts this.
  • strings::without_trailing_slash → bun_paths::string_paths::without_trailing_slash_windows_path, which preserves C:\ on Windows and delegates to the plain helper on unix (verified at src/paths/string_paths.rs:461-469).
  • PR description no longer claims bun pm pack coverage; the PackageManager::init chdir limitation is called out as pre-existing and out of scope.

Security risks

None. This only writes an additional env var (the process's own cwd) into the script environment; no untrusted input parsing, no new I/O, no privilege boundaries crossed.

Level of scrutiny

Low-to-moderate. 8 lines of Rust in a well-understood env-seeding function, following the exact pattern of adjacent npm_config_local_prefix / npm_lifecycle_event writes. The value source (top_level_dir = (*this_transpiler.fs).top_level_dir, set from getcwd() via FileSystem::init) is the same field process.cwd() reads. Type signature matches (&[u8] in, &[u8] out).

Other factors

Tests follow harness conventions: tempDirWithFiles, bunEnv spread, await using for the subprocess, Promise.all over stdout/stderr/exited, stderr asserted empty before stdout, exit code last. fs.realpathSync handles macOS /private/var symlinks. The INIT_CWD: "/should/be/overwritten" seed makes the overwrite semantics load-bearing (test would fail on canary with the old put_default). Evidence block shows both tests fail on canary and pass with the fix under both ASAN debug and release.

@robobun

robobun commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the diff is green. The gate (robobun/evidence) passes on both ASAN and release; test/cli/run/run-process-env.test.ts passes on every lane in builds #83506 and #83527.

The only hard failure across both builds is test/js/bun/http/serve.test.ts "request body backpressure > releases a paused request body when the handler responds without reading it" on darwin-14-x64, an HTTP body-stream test unrelated to this env-var change. The remaining red are retry-passed flakes (webview-chrome, bun-install-registry, bun-add, require-cache, 20144, proxy-stress-protocol, no-orphans, s3, test-tls-session-timeout).

Ready for review/merge.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-27, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

@robobun robobun closed this Sep 13, 2026
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.

2 participants