From b531a71f7f1c2845ad591028f21cc2390fca712e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:01:58 +0000 Subject: [PATCH 1/3] install: make the security scanner child follow --no-env-file, --env-file and bunfig env = false The scanner runs in a child `bun --no-install -e` in the project root. That child loads the default .env files on its own and gets the raw process environment, so it ignored the .env decision of the install. PackageManager::init now records which .env files it loaded. When the install loaded none, the child gets --no-env-file. When the install loaded --env-file files, the child gets --no-env-file and the environment of the install. A default install spawns the child as before. --- src/install/PackageManager.rs | 13 +- .../PackageManager/PackageManagerOptions.rs | 16 +++ .../PackageManager/security_scanner.rs | 120 ++++++++++-------- 3 files changed, 96 insertions(+), 53 deletions(-) diff --git a/src/install/PackageManager.rs b/src/install/PackageManager.rs index 75718a7f4cc9..6f15ff26c249 100644 --- a/src/install/PackageManager.rs +++ b/src/install/PackageManager.rs @@ -1926,7 +1926,7 @@ pub fn init( fs::EntriesOption::Err(e) => return Err(e.canonical_error.into()), }; - let env: &mut dot_env::Loader = if cli.env_files.is_empty() { + let (env, env_files): (&mut dot_env::Loader, options::EnvFiles) = if cli.env_files.is_empty() { let env = new_env_loader(); env.load_process()?; let env_suffix = default_env_file_suffix(env); @@ -1944,11 +1944,17 @@ pub fn init( ) }; env.load(&env_probe_keys, &[], env_suffix, skip_default_env)?; - env + let env_files = if skip_default_env { + options::EnvFiles::Disabled + } else { + options::EnvFiles::Default + }; + (env, env_files) } else { // SAFETY: `load_explicit_env_files` stored this loader when `init()` // started; nothing else holds a reference to it yet. - unsafe { &mut *holder::ENV_LOADER.load() } + let env = unsafe { &mut *holder::ENV_LOADER.load() }; + (env, options::EnvFiles::Explicit) }; initialize_store(); @@ -2008,6 +2014,7 @@ pub fn init( max_concurrent_lifecycle_scripts: cli .concurrent_scripts .unwrap_or((cpu_count * 2) as usize), + env_files, ..Default::default() }; diff --git a/src/install/PackageManager/PackageManagerOptions.rs b/src/install/PackageManager/PackageManagerOptions.rs index 03ad74e180cd..65950dd3b01c 100644 --- a/src/install/PackageManager/PackageManagerOptions.rs +++ b/src/install/PackageManager/PackageManagerOptions.rs @@ -21,6 +21,18 @@ pub enum OfflineMode { Offline, } +/// Which `.env` files this install loaded. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +pub enum EnvFiles { + /// The default files of the project root. + #[default] + Default, + /// `--no-env-file` / bunfig `env = false`: none. + Disabled, + /// `--env-file`: only the named files. + Explicit, +} + // `string` fields are `[]const u8` borrowed from CLI args / bunfig config, // which live for the process lifetime. There is no `deinit` on Options. Mapped to // `&'static [u8]` per PORTING.md (no lifetime params on structs). @@ -103,6 +115,9 @@ pub struct Options { // Security scanner module path pub security_scanner: Option<&'static [u8]>, + /// The security scanner child follows this: it is a `bun` that loads `.env` files itself. + pub(crate) env_files: EnvFiles, + // Minimum release age in ms (security feature) // Only install packages published at least N ms ago pub minimum_release_age_ms: Option, @@ -178,6 +193,7 @@ impl Default for Options { hoist: true, offline: OfflineMode::Online, security_scanner: None, + env_files: EnvFiles::Default, minimum_release_age_ms: None, minimum_release_age_excludes: None, cpu: Npm::Architecture::CURRENT, diff --git a/src/install/PackageManager/security_scanner.rs b/src/install/PackageManager/security_scanner.rs index fbf68d7c5c13..29a6549d6a81 100644 --- a/src/install/PackageManager/security_scanner.rs +++ b/src/install/PackageManager/security_scanner.rs @@ -9,6 +9,9 @@ use bstr::BStr; // `bun_io::Loop` is the trait's nominal: `us_loop_t` on POSIX, `uv_loop_t` // on Windows. The inherent `loop_()` projects `.uv_loop` from the uws wrapper // on Windows so `BufferedReaderParent::loop_` returns the libuv loop directly. +use core::ffi::CStr; +use std::ffi::CString; + use crate::Error; use crate::bun_fs::FileSystem; use crate::bun_json::{Expr, ExprData}; @@ -29,14 +32,14 @@ use bun_ptr::{RefCount, RefPtr}; use bun_spawn::SpawnResultExt as _; use bun_spawn::subprocess::{self, StdioResult}; use bun_spawn::{ - self as spawn, Exited, Process, ProcessExit, ProcessExitKind, ProcessHandle, Rusage, + self as spawn, Exited, Process, ProcessExit, ProcessExitKind, ProcessHandle, Rusage, SpawnEnv, SpawnOptions, Status, Stdio, }; use bun_sys::{self, Fd, FdExt as _}; use crate::hoisted_install as HoistedInstall; use crate::isolated_install as IsolatedInstall; -use crate::package_manager_real::package_manager_options::Do; +use crate::package_manager_real::package_manager_options::{Do, EnvFiles}; #[inline] fn signal_name(raw: u8) -> &'static str { @@ -255,6 +258,33 @@ pub fn perform_security_scan_for_all( ) } +/// What the scanner child, a `bun -e` in the project root, is told about `.env` files. +enum ScannerEnv { + /// Nothing: it loads the default files itself. + Default, + /// `--no-env-file`. + NoEnvFile, + /// `--no-env-file`, and the environment with the `--env-file` values. + Resolved(bun_dotenv::NullDelimitedEnvMap), +} + +impl ScannerEnv { + fn new(manager: &PackageManager) -> Result { + Ok(match manager.options.env_files { + EnvFiles::Default => Self::Default, + EnvFiles::Disabled => Self::NoEnvFile, + EnvFiles::Explicit => { + let env = manager.env_mut(); + // `--env-file` copies the environment before bunfig `[run] noOrphans` sets this. + if bun_io::parent_death_watchdog::is_enabled() { + env.map.put(b"BUN_FEATURE_FLAG_NO_ORPHANS", b"1")?; + } + Self::Resolved(env.map.create_null_delimited_env_map()?) + } + }) + } +} + fn scan_installing_scanner_if_needed( manager: &mut PackageManager, security_scanner: &[u8], @@ -263,6 +293,9 @@ fn scan_installing_scanner_if_needed( command_ctx: CommandContext, original_cwd: &[u8], ) -> Result, Error> { + // Built once: the partial install below can add script variables to the loader. + let env = ScannerEnv::new(manager)?; + let result = attempt_security_scan( manager, security_scanner, @@ -270,6 +303,7 @@ fn scan_installing_scanner_if_needed( seeds, command_ctx, original_cwd, + &env, )?; match result { @@ -288,6 +322,7 @@ fn scan_installing_scanner_if_needed( command_ctx, original_cwd, true, + &env, )?; match retry_result { ScanAttemptResult::Success(scan_results) => Ok(Some(scan_results)), @@ -793,6 +828,7 @@ fn attempt_security_scan( seeds: &[PackageID], command_ctx: CommandContext, original_cwd: &[u8], + env: &ScannerEnv, ) -> Result { attempt_security_scan_with_retry( manager, @@ -802,6 +838,7 @@ fn attempt_security_scan( command_ctx, original_cwd, false, + env, ) } @@ -813,6 +850,7 @@ fn attempt_security_scan_with_retry( command_ctx: CommandContext, original_cwd: &[u8], is_retry: bool, + env: &ScannerEnv, ) -> Result { if manager.options.log_level == crate::package_manager::Options::LogLevel::Verbose { bun_core::pretty_errorln!( @@ -910,7 +948,7 @@ fn attempt_security_scan_with_retry( }); // Cleanup of code/json_data/process handled by `Drop for SecurityScanSubprocess` when Box drops. - scanner.spawn()?; + scanner.spawn(env)?; // `sleep_until` takes `*mut PackageManager` + `fn(&mut C) -> bool`; pass the // boxed scanner as the closure context and a fn pointer that probes `is_done`. @@ -1004,7 +1042,7 @@ bun_io::impl_buffered_reader_parent! { } impl<'a> SecurityScanSubprocess<'a> { - pub(crate) fn spawn(&mut self) -> Result<(), Error> { + fn spawn(&mut self, env: &ScannerEnv) -> Result<(), Error> { self.ipc_data = Vec::new(); let parent: *mut Self = self; self.ipc_reader.set_parent(parent.cast()); @@ -1025,36 +1063,32 @@ impl<'a> SecurityScanSubprocess<'a> { let exec_path = bun_core::self_exe_path()?; - // Build - // owned NUL-terminated buffers so the pointers stay valid across the - // `spawn_process` FFI boundary. - let mut argv0_buf: Vec = exec_path.as_bytes().to_vec(); - argv0_buf.push(0); - let mut argv3_buf: Vec = self.code.to_vec(); - argv3_buf.push(0); - // Element type MUST be bare `*const c_char` (null sentinel), never - // `Option<*const c_char>`: raw pointers are already nullable, and - // `Option<*const T>` is a 2-word (tag, ptr) pair — casting that to - // `Argv` interleaves discriminant words and EFAULTs in the kernel. - let mut argv: [*const core::ffi::c_char; 5] = [ - argv0_buf.as_ptr().cast(), - c"--no-install".as_ptr(), - c"-e".as_ptr(), - argv3_buf.as_ptr().cast(), - core::ptr::null(), - ]; - const _: () = assert!( - core::mem::size_of::<[*const core::ffi::c_char; 5]>() - == 5 * core::mem::size_of::() - ); + let code = CString::new(&*self.code).map_err(|_| crate::Error::InvalidCharacter)?; + let mut argv: Vec<&CStr> = Vec::with_capacity(5); + argv.push(exec_path.as_cstr()); + argv.push(c"--no-install"); + if !matches!(env, ScannerEnv::Default) { + argv.push(c"--no-env-file"); + } + argv.push(c"-e"); + argv.push(&code); + + let resolved: Vec<&CStr>; + let env = match env { + ScannerEnv::Default | ScannerEnv::NoEnvFile => SpawnEnv::Inherit, + ScannerEnv::Resolved(envp) => { + resolved = envp.iter().collect(); + SpawnEnv::Strings(&resolved) + } + }; #[cfg(windows)] { - self.spawn_windows(&mut argv, ipc_output_fds)?; + self.spawn_windows(&argv, env, ipc_output_fds)?; } #[cfg(not(windows))] { - self.spawn_posix(&mut argv, ipc_output_fds)?; + self.spawn_posix(&argv, env, ipc_output_fds)?; } Ok(()) @@ -1067,7 +1101,8 @@ impl<'a> SecurityScanSubprocess<'a> { #[cfg(unix)] fn spawn_posix( &mut self, - argv: &mut [*const core::ffi::c_char; 5], + argv: &[&CStr], + env: SpawnEnv<'_>, ipc_output_fds: [Fd; 2], ) -> Result<(), Error> { let extra_fds: Box<[Stdio]> = Box::new([ @@ -1084,16 +1119,8 @@ impl<'a> SecurityScanSubprocess<'a> { ..Default::default() }; - // SAFETY: `argv` is a local null-terminated C-string array with a - // non-null argv[0]; `environ_ptr()` is the process environ block. - let mut spawned = unsafe { - spawn::spawn_process( - &spawn_options, - argv.as_mut_ptr().cast(), - bun_sys::environ_ptr(), - ) - }? - .map_err(|e| e.to_zig_err())?; + let mut spawned = + spawn::spawn_process_cstr(&spawn_options, argv, env)?.map_err(|e| e.to_zig_err())?; // `defer spawned.extra_pipes.deinit()` — drops at scope exit. ipc_output_fds[1].close(); @@ -1117,7 +1144,8 @@ impl<'a> SecurityScanSubprocess<'a> { #[cfg(windows)] fn spawn_windows( &mut self, - argv: &mut [*const core::ffi::c_char; 5], + argv: &[&CStr], + env: SpawnEnv<'_>, ipc_output_fds: [Fd; 2], ) -> Result<(), Error> { use bun_sys::ReturnCodeExt as _; @@ -1196,16 +1224,8 @@ impl<'a> SecurityScanSubprocess<'a> { ..Default::default() }; - // SAFETY: `argv` is a local null-terminated C-string array with a - // non-null argv[0]; `environ_ptr()` is the process environ block. - let mut spawned = unsafe { - spawn::spawn_process( - &spawn_options, - argv.as_mut_ptr().cast(), - bun_sys::environ_ptr(), - ) - }? - .map_err(|e| e.to_zig_err())?; + let mut spawned = + spawn::spawn_process_cstr(&spawn_options, argv, env)?.map_err(|e| e.to_zig_err())?; // `defer spawned.extra_pipes.deinit()` — drops at scope exit. ipc_output_fds[1].close(); From 44ba821dddb5e1e21cd1112ae8e6d508ad044679 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:25:28 +0000 Subject: [PATCH 2/3] test: the security scanner child follows the .env decision of install --- test/cli/install/bun-install-env-file.test.ts | 258 +++++++++++++++++- 1 file changed, 257 insertions(+), 1 deletion(-) diff --git a/test/cli/install/bun-install-env-file.test.ts b/test/cli/install/bun-install-env-file.test.ts index a97253946f3b..933589006970 100644 --- a/test/cli/install/bun-install-env-file.test.ts +++ b/test/cli/install/bun-install-env-file.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; import { join } from "node:path"; // https://github.com/oven-sh/bun/issues/12011 @@ -290,3 +290,259 @@ describe("bun install .env loading and lifecycle scripts (#31450)", () => { expect(exitCode).toBe(0); }); }); + +// The security scanner runs in a child `bun` that starts in the project root. +// That child loads the default `.env*` files by itself, so install passes its +// own decision on to it. +const scannerProbeKeys = [ + "SCAN_DOTENV", + "SCAN_MODE", + "SCAN_CUSTOM", + "SCAN_SHELL", + "BUN_FEATURE_FLAG_NO_ORPHANS", + // Install sets this one for lifecycle scripts only. + "npm_config_user_agent", +]; + +// Records which of the probe variables the scanner process has, and whether a +// postinstall script ran before it. +const scannerSource = ` +import { existsSync, writeFileSync } from "node:fs"; +export const scanner = { + version: "1", + async scan() { + const seen = Object.fromEntries(${JSON.stringify(scannerProbeKeys)}.map(key => [key, process.env[key]])); + if (existsSync("postinstall-ran.txt")) seen.afterPostinstall = true; + writeFileSync("scanner-env.json", JSON.stringify(seen)); + return []; + }, +}; +`; + +const scannerBunfig = `[install] +cache = false + +[install.security] +scanner = "./scanner.ts" +`; + +// SCAN_SHELL is also in the real environment, and that value wins over a file. +const scannerProject: Files = { + "package.json": JSON.stringify({ + name: "env-file-scanner", + version: "1.0.0", + dependencies: { dep: "file:./dep" }, + }), + "dep/package.json": JSON.stringify({ name: "dep", version: "1.0.0" }), + "other/package.json": JSON.stringify({ name: "other", version: "1.0.0" }), + "bunfig.toml": scannerBunfig, + "scanner.ts": scannerSource, + ".env": "SCAN_DOTENV=from-dotenv\nSCAN_SHELL=from-dotenv\n", + ".env.development": "SCAN_MODE=development\n", + ".env.production": "SCAN_MODE=production\n", + "custom.env": "SCAN_CUSTOM=from-custom\nSCAN_SHELL=from-custom\n", +}; + +type ScanOptions = { + argv?: string[]; + /** A `sh -c` script to run in place of `bun `. `$0` is bun. */ + sh?: string; + /** Added to `scannerProject`. */ + files?: Files; + /** Relative to the temp dir. */ + cwd?: string; + env?: (root: string) => Record; +}; + +/** Runs `bun ` and returns what the scanner process recorded. */ +async function scannerEnv(opts: ScanOptions) { + using dir = tempDir("install-env-file-scanner", { ...scannerProject, ...opts.files }); + const root = String(dir); + + await using proc = Bun.spawn({ + cmd: opts.sh ? ["sh", "-c", opts.sh, bunExe()] : [bunExe(), ...(opts.argv ?? [])], + cwd: join(root, opts.cwd ?? "."), + env: { + ...bunEnv, + NODE_ENV: undefined, + BUN_ENV: undefined, + SCAN_DOTENV: undefined, + SCAN_MODE: undefined, + SCAN_CUSTOM: undefined, + SCAN_SHELL: "from-shell", + BUN_FEATURE_FLAG_NO_ORPHANS: undefined, + npm_config_user_agent: undefined, + ...opts.env?.(root), + }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // No probe file means the scanner did not run. stderr says why. + const probe = Bun.file(join(root, "scanner-env.json")); + const seen: unknown = (await probe.exists()) ? await probe.json() : stderr; + return { seen, stdout, exitCode }; +} + +describe("bun install .env loading and the security scanner", () => { + const shell = { SCAN_SHELL: "from-shell" }; + const custom = { SCAN_CUSTOM: "from-custom", ...shell }; + + const rows: [label: string, opts: ScanOptions, seen: Record][] = [ + // Not a decided default: the scanner loads the runtime's files, install loads the production ones. + [ + "without a flag the scanner loads the .env files itself", + { argv: ["install"] }, + { SCAN_DOTENV: "from-dotenv", SCAN_MODE: "development", ...shell }, + ], + ["--no-env-file", { argv: ["install", "--no-env-file"] }, shell], + ["--env-file", { argv: ["install", "--env-file=custom.env"] }, custom], + [ + "`env = false` in the global bunfig", + { + argv: ["install"], + files: { "xdg/.bunfig.toml": "env = false\n" }, + env: root => ({ XDG_CONFIG_HOME: join(root, "xdg") }), + }, + shell, + ], + [ + "`env = false` in a --config bunfig", + { + argv: ["install", "--config=ci/bunfig.toml"], + files: { "ci/bunfig.toml": `env = false\n${scannerBunfig}` }, + }, + shell, + ], + // The scanner starts in the workspace root, not where the path is relative to. + [ + "a relative --env-file from a workspace member", + { + argv: ["install", "--env-file", "member.env"], + cwd: "packages/foo", + files: { + "package.json": JSON.stringify({ name: "root", version: "1.0.0", workspaces: ["packages/*"] }), + "packages/foo/package.json": JSON.stringify({ name: "foo", version: "1.0.0" }), + "packages/foo/member.env": "SCAN_CUSTOM=from-member\n", + }, + }, + { SCAN_CUSTOM: "from-member", ...shell }, + ], + // Install sets this variable in its own environment after it has read the + // --env-file files. The scanner reads the project bunfig.toml, not the global one. + [ + "--env-file keeps `[run] noOrphans` of the global bunfig", + { + argv: ["install", "--env-file=custom.env"], + files: { "xdg/.bunfig.toml": "[run]\nnoOrphans = true\n" }, + env: root => ({ XDG_CONFIG_HOME: join(root, "xdg") }), + }, + { ...custom, BUN_FEATURE_FLAG_NO_ORPHANS: "1" }, + ], + ["bun add --env-file", { argv: ["add", "--env-file=custom.env", "./other"] }, custom], + ["bun update --env-file", { argv: ["update", "--env-file=custom.env"] }, custom], + ["bun remove --env-file", { argv: ["remove", "--env-file=custom.env", "dep"] }, custom], + [ + "bun pm scan --env-file", + { + argv: ["pm", "scan", "--env-file=custom.env"], + files: { + "bun.lock": JSON.stringify({ + lockfileVersion: 1, + workspaces: { "": { name: "env-file-scanner", dependencies: { dep: "file:./dep" } } }, + packages: { dep: ["dep@file:dep", {}] }, + }), + }, + }, + custom, + ], + ]; + + // Each case starts two bun processes, install and then the scanner. They run + // one at a time to stay inside the default timeout on a debug build. + test.each(rows)("%s", async (_label, opts, expected) => { + const { seen, exitCode } = await scannerEnv(opts); + expect(seen).toEqual(expected); + expect(exitCode).toBe(0); + }); + + // A pipe can be read once. Install reads it, so the scanner must not be sent to the path. + test.skipIf(isWindows)("--env-file from a pipe", async () => { + const { seen, exitCode } = await scannerEnv({ + sh: `printf 'SCAN_CUSTOM=from-pipe\\n' | "$0" install --env-file=/dev/stdin`, + }); + expect(seen).toEqual({ SCAN_CUSTOM: "from-pipe", ...shell }); + expect(exitCode).toBe(0); + }); + + // Open: a bun child reads BUN_OPTIONS again. This scanner also gets the root + // member.env, and a FIFO named there blocks it. The case fails until that is closed. + test.failing("BUN_OPTIONS=--env-file from a workspace member is not read again by the scanner", async () => { + const { seen } = await scannerEnv({ + argv: ["install"], + cwd: "packages/foo", + files: { + "package.json": JSON.stringify({ name: "root", version: "1.0.0", workspaces: ["packages/*"] }), + "packages/foo/package.json": JSON.stringify({ name: "foo", version: "1.0.0" }), + "packages/foo/member.env": "SCAN_CUSTOM=from-member\n", + "member.env": "SCAN_DOTENV=from-root-member\n", + }, + env: () => ({ BUN_OPTIONS: "--env-file=member.env" }), + }); + expect(seen).toEqual({ SCAN_CUSTOM: "from-member", ...shell }); + }); + + // The scanner package is not installed yet, so the first child fails to + // import it. Install then adds the package, runs its postinstall, and starts + // a second child. That child must not get the variables of the script run. + test("the scanner that install adds from npm gets the --env-file values", async () => { + const postinstall = `${bunExe()} --no-env-file -e 'await Bun.write(process.env.INIT_CWD + "/postinstall-ran.txt", "1")'`; + const tarball = await new Bun.Archive( + { + "package/package.json": JSON.stringify({ + name: "probe-scanner", + version: "1.0.0", + type: "module", + scripts: { postinstall }, + }), + "package/index.js": scannerSource, + }, + { compress: "gzip" }, + ).bytes(); + await using registry = Bun.serve({ + port: 0, + fetch(req) { + if (new URL(req.url).pathname.endsWith(".tgz")) return new Response(tarball); + const dist = { tarball: `http://localhost:${registry.port}/probe-scanner-1.0.0.tgz` }; + return Response.json({ + name: "probe-scanner", + "dist-tags": { latest: "1.0.0" }, + versions: { "1.0.0": { name: "probe-scanner", version: "1.0.0", scripts: { postinstall }, dist } }, + }); + }, + }); + + const { seen, stdout, exitCode } = await scannerEnv({ + argv: ["install", "--env-file=custom.env"], + files: { + "package.json": JSON.stringify({ + name: "env-file-scanner", + version: "1.0.0", + dependencies: { "probe-scanner": "1.0.0" }, + trustedDependencies: ["probe-scanner"], + }), + "bunfig.toml": `[install] +cache = false +registry = "http://localhost:${registry.port}/" + +[install.security] +scanner = "probe-scanner" +`, + }, + }); + expect(seen).toEqual({ ...custom, afterPostinstall: true }); + expect(stdout).toContain("Security scanner installed successfully"); + expect(exitCode).toBe(0); + }); +}); From 6192197cb198606b098b1decc6d05523b9a3f654 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:26:09 +0000 Subject: [PATCH 3/3] docs: say which .env files the security scanner gets --- docs/pm/security-scanner-api.mdx | 7 +++++++ docs/runtime/environment-variables.mdx | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/pm/security-scanner-api.mdx b/docs/pm/security-scanner-api.mdx index 57abac7c2f63..0d51721805ce 100644 --- a/docs/pm/security-scanner-api.mdx +++ b/docs/pm/security-scanner-api.mdx @@ -82,6 +82,13 @@ bun install Consult your scanner's documentation for which environment variables to set and any other required configuration. +Bun runs the scanner in a separate process. That process gets the environment variables of your shell. The `--no-env-file` and `--env-file` flags of `bun install` and the other package manager commands also apply to the scanner: + +- `--no-env-file`: Bun loads no [`.env` file](/runtime/environment-variables) for the scanner. `env = false` in `bunfig.toml` does the same. +- `--env-file`: Bun passes the values of the files you name to the scanner process as environment variables. + +The scanner can still read a `.env` file itself. If your scanner takes its token from `.env`, set the token in your shell or name the file with `--env-file`. + ### Authoring your own scanner For a complete example with tests and CI setup, see the official template: diff --git a/docs/runtime/environment-variables.mdx b/docs/runtime/environment-variables.mdx index 1dcbdde4a627..7665b1e3153f 100644 --- a/docs/runtime/environment-variables.mdx +++ b/docs/runtime/environment-variables.mdx @@ -65,7 +65,7 @@ process.env.FOO = "hello"; ## Manually specifying `.env` files -The `--env-file` flag overrides which `.env` files Bun loads. It works when running a file with `bun`, when running `package.json` scripts, and with `bun install` and the other package manager commands (where the loaded values are used for `$VARIABLE` substitution in `bunfig.toml`). +The `--env-file` flag overrides which `.env` files Bun loads. It works when running a file with `bun`, when running `package.json` scripts, and with `bun install` and the other package manager commands (where the loaded values are used for `$VARIABLE` substitution in `bunfig.toml`, and are passed to lifecycle scripts and the security scanner as environment variables). ```sh bun --env-file=.env.1 src/index.ts