Skip to content

node: NODE_COMPAT env var — project-wide augmentation opt-out (= --node, tree-wide)#34

Merged
colinhacks merged 5 commits into
mainfrom
node-compat-env-optout
Jun 21, 2026
Merged

node: NODE_COMPAT env var — project-wide augmentation opt-out (= --node, tree-wide)#34
colinhacks merged 5 commits into
mainfrom
node-compat-env-optout

Conversation

@colinhacks

Copy link
Copy Markdown
Contributor

What

A truthy NODE_COMPAT env var forces compat mode (zero runtime augmentation — identical to the --node flag) across every nub runtime entrypoint, project/tree-wide. Because env vars inherit to descendants, setting it once (shell, .envrc, CI) covers the whole process tree — the persistent form of --node. Version resolution/provisioning stays ON (compat = no augmentation, not no-pinning).

Semantics

  • Truthy = 1 / true / yes (case-insensitive, trimmed). Empty / 0 / false / unset = off.
  • Composes with --node: either one forces compat. No --no-node re-enable (deferred per the design recommendation).
  • Brand-clean: NODE_ prefix (Node doesn't claim the name), not NUB_* / AUBE_*.

Call sites

A single node_compat_env() helper is OR-ed into the compat/compat_mode bit at each entrypoint where compat is decided:

Entrypoint Covers
run_file_in_dir nub <file> and the node-PATH-hijack (run_as_noderun_file → here)
run_script nub run <script>
run_exec_with_dlx nub exec / nubx (native-bin path)
run_workspace_target -r / --filter workspace runs

So NODE_COMPAT=1 node foo.ts, NODE_COMPAT=1 nub foo.ts, and NODE_COMPAT=1 nub run x all run vanilla.

Test

node_compat_env_forces_vanilla_tree_wide (unix-only) asserts via the .env-eager-load discriminator:

  1. default (unset) via the hijack stays augmented (.env loads);
  2. NODE_COMPAT=1 via the hijack → empty NODE_OPTIONS/execArgv, no .env;
  3. NODE_COMPAT=1 on a direct nub <file> run → vanilla too;
  4. truthy variant (TRUE) forces compat; falsy (0) stays augmented.

Docs

Reconciles the prior NODE_COMPAT=1 dismissal in AGENTS.md (that was a file-run compat var, superseded by --node; this is a different thing — the tree-wide augmentation opt-out) and the stale "no NODE_COMPAT=1 env var" line in site/public/skill.md.

Verification

cargo build/clippy/fmt clean; the new test + existing node_hijack_node_flag_opts_out_of_augmentation pass. (The 33 augmentation-transpile test failures in the local run are pre-existing — they require the runtime/ asset dir adjacent to the binary, which a bare test build lacks; confirmed identical failures on the base branch.)

https://claude.ai/code/session_01YRvztkcr4fzfg9rD5edUwa

…de, tree-wide)

A truthy NODE_COMPAT (1/true/yes, case-insensitive; empty/0/false/unset = off)
forces compat mode — zero runtime augmentation, identical to the --node flag —
across every runtime entrypoint. Because env vars inherit to descendants, setting
it once covers the whole process tree: the persistent form of --node. Version
resolution/provisioning stays on (compat = no augmentation, not no-pinning).

Mechanism: a node_compat_env() helper OR-ed into the compat bit at each entrypoint
that decides compat — run_file_in_dir (nub <file> + the node-hijack), run_script
(nub run), run_exec_with_dlx (nub exec / nubx), and run_workspace_target (-r). So
NODE_COMPAT=1 node foo.ts / nub foo.ts / nub run x all run vanilla. Composes with
--node (either forces compat); no --no-node re-enable (deferred).

Brand-clean: NODE_ prefix (Node doesn't claim the name), not NUB_/AUBE_.

Reconciles the prior NODE_COMPAT=1 dismissal in AGENTS.md (that was about a
file-run compat var, superseded by --node) and the stale "no NODE_COMPAT env var"
line in site/public/skill.md.

Claude-Session: https://claude.ai/code/session_01YRvztkcr4fzfg9rD5edUwa
Copilot AI review requested due to automatic review settings June 20, 2026 22:01
@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nub Ready Ready Preview, Comment Jun 21, 2026 12:44am

Request Review

…eft uncovered)

`nub watch` was the one runtime entrypoint whose Node spawn never flowed through
the NODE_COMPAT OR — it has no --node flag and always injected nub's flags +
preload + eager .env*. Wire the ambient opt-out in: when NODE_COMPAT is truthy,
run the pinned Node with `--watch` and only the user's argv (no flag injection,
no preload, no .env), matching the zero-augmentation contract everywhere else.
Version provisioning stays on.

Self-review audit of every compat-deciding / Node-spawn path in cli.rs confirms
no remaining bypass: the four OR-ed funnels (run_file_in_dir, run_script,
run_exec_with_dlx, run_workspace_target) plus run_watch now cover every entry —
`nub <file>`, the node-hijack, run/exec/nubx, -r workspace runs, stdin/-flag
short-circuits, and the launch_bin native-bin + node-bin branches (the latter
routes back through run_file_in_dir; the former gates apply_exec_augmentation on
the already-OR-ed compat bit). The only direct `node` spawns are the two
run_watch branches (both handled) and run_file_in_dir's spawn_node (post-OR).

Test node_compat_env_forces_vanilla_under_watch: spawns `NODE_COMPAT=1 nub watch`,
the probe writes its augmentation snapshot to a sentinel file (the watch loop
never self-exits), asserts no preload, empty NODE_OPTIONS, no injected execArgv
flag (only --watch/--watch-preserve-output), and no eager .env.

Claude-Session: https://claude.ai/code/session_01YRvztkcr4fzfg9rD5edUwa

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds a project/tree-wide “compat mode” opt-out via a truthy NODE_COMPAT environment variable, making runtime augmentation consistently disable-able without repeating --node across process trees.

Changes:

  • Introduces node_compat_env() and ORs it into compat decisions across all runtime entrypoints.
  • Adds an integration test ensuring NODE_COMPAT forces vanilla behavior for both direct nub <file> and the node PATH-hijack path (unix).
  • Updates documentation to reflect the new NODE_COMPAT behavior and removes prior “no env var” statements.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
crates/nub-cli/src/cli.rs Implements NODE_COMPAT parsing and applies it across runtime entrypoints.
crates/nub-cli/tests/integration.rs Adds integration coverage asserting vanilla vs augmented behavior under NODE_COMPAT.
AGENTS.md Updates architectural/decision docs to mark NODE_COMPAT as live and clarify semantics.
site/public/skill.md Updates user-facing docs to describe using NODE_COMPAT as persistent --node.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +3974 to +3984
let run = |bin: &std::path::Path, args: &[&str], compat: Option<&str>| {
let mut cmd = Command::new(bin);
cmd.args(args)
.current_dir(&proj)
.env("XDG_CACHE_HOME", unique_test_cache())
.env_remove("NODE_COMPAT");
if let Some(v) = compat {
cmd.env("NODE_COMPAT", v);
}
cmd.output().expect("spawn")
};
Comment on lines +3994 to +4009
assert!(
!s.contains("preload"),
"{ctx} must run vanilla (no preload): {s}"
);
assert!(
s.contains("NODE_OPTIONS=\n") || s.contains("NODE_OPTIONS=$"),
"{ctx} must not inject NODE_OPTIONS: {s:?}"
);
assert!(
s.contains("execArgv=\n"),
"{ctx} must have empty execArgv: {s:?}"
);
assert!(
s.contains("COMPAT_ENV=\n") || s.contains("COMPAT_ENV=$"),
"{ctx} must NOT load `.env` (compat mode): {s:?}"
);
Comment on lines +3943 to +3945
#[cfg(unix)]
#[test]
fn node_compat_env_forces_vanilla_tree_wide() {
Comment thread crates/nub-cli/src/cli.rs
Comment on lines +2065 to +2070
fn node_compat_env() -> bool {
match env::var("NODE_COMPAT") {
Ok(v) => matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes"),
Err(_) => false,
}
}
The clippy fix in 894e6ef left `&& s.contains(...) {` on one line, which
fails `cargo fmt --check` (CI's fmt job). Run rustfmt: brace on its own line.

Claude-Session: https://claude.ai/code/session_01YRvztkcr4fzfg9rD5edUwa
@colinhacks colinhacks merged commit f55fc83 into main Jun 21, 2026
26 checks passed
@colinhacks colinhacks deleted the node-compat-env-optout branch June 21, 2026 02:36
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.

2 participants