Skip to content

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 17:16
· 1790 commits to main since this release

First published build since v0.2.0-beta. (v0.4.0 was tagged but never published; everything in it is included here.)

Most of this release is Windows containment: making it enforce what it claimed, cover the shell people actually use, and stop hanging on the packages it exists to contain.

Egress is enforced, not requested

A package that ignores HTTP_PROXY now reaches nothing. The sandbox holds no network capability on Windows, so the OS itself refuses direct connections and name lookups fail — the only route out is nvx's allowlist proxy, reached over a UNIX socket and relayed back inside the sandbox by a supervisor process. No administrator rights required.

Services on your own machine are allowlisted like any other destination rather than permitted automatically. To reach a dev server from inside the sandbox, add allow_hosts: ["127.0.0.1:3000"].

nvx setup no longer registers a loopback exemption, and removes an existing one. It is now only about drive-root access and is not needed for egress.

If you ran nvx setup before 0.5.0, read this. That elevated setup registered a Windows loopback exemption, because the proxy then ran on the host's loopback and an AppContainer cannot reach it otherwise. While that exemption is registered, contained code reaches every service on 127.0.0.1 — local databases, daemon ports, other dev servers — with no allow_hosts entry.

Treat the egress allowlist as unenforced until you remove it. Only direct connections to other hosts stay blocked; any reachable loopback service that forwards traffic — a debugging proxy, ssh -D, a dev server's proxy route — turns this into arbitrary egress.

0.5.0 removes it, but only during an elevated nvx setup — the command this release otherwise tells you that you no longer need. Removing it requires administrator rights, so nvx cannot do it on a normal run. Instead it warns on every affected launch and nvx doctor reports it and exits non-zero, both printing the one-line removal command. Fresh installs are unaffected.

One sandbox can no longer borrow another's egress allowlist

Every nvx sandbox on a machine shares one AppContainer package identity, and Windows scopes its loopback restriction to the package — so two projects running at once sat in the same loopback namespace. A contained process could scan loopback, find another session's relay, and tunnel to a host only that project's policy allowed. The host-side proxy listeners were reachable by any local process for the same reason. The allowlist was per-project; the thing enforcing it was shared and unauthenticated.

Each session now mints a random credential and its proxy requires it, over HTTP and SOCKS both. It travels as ordinary proxy credentials inside HTTP_PROXY, so npm, node and curl send it without knowing anything about nvx, and a sibling that found the port by scanning gets 407. Authentication is checked before the allowlist, so the 403-vs-200 difference cannot be used to probe what another session is permitted to reach.

Each project gets its own sandbox identity

An install in one project can no longer read or write another project, another running session's home, or the credentials a trusted tool has persisted. Previously every sandbox on the machine shared one identity and the permissions nvx granted were never revoked.

If you used nvx before 0.5.0, this is not retroactive. Old permissions are removed the first time nvx runs in an affected project, but nvx keeps no record of where it has run, so projects you do not revisit keep theirs. README and SECURITY.md give the manual command.

The shim directory is no longer a way to shadow a system command

nvx use puts a directory of project-local shims near the front of your PATH, ahead of System32. That directory used to live inside the project, so a contained install could write a file called git into it and wait for you to type git — which then ran uncontained, as you, with every credential you have. The sandbox held; a directory nvx itself put on your PATH went around it.

The shims moved to ~/.nvx/project-bin/<project hash>, which a contained process cannot write, and a name that already resolves elsewhere on your PATH is now never shimmed — node_modules/.bin is itself writable by an install, so relocating alone would only move the plant one directory back.

The cost, stated plainly: if you have a global tool of the same name, the project-local one no longer wins through nvx. npx <tool> still runs the local one, contained.

What this does not fix, said plainly too. A project-local CLI whose name isn't taken by a global one — eslint, tsc, vitest, prettier — still gets a shim, and at the default standard level that shim runs it uncontained, as you. node_modules/.bin is writable by design, so a contained install can rewrite what one of those tools does and wait for you to run it. That is the documented stance that your project's own dependencies are your code rather than a sandbox bug, but the two halves had never been stated next to each other. If you don't want it, isolation.level: strict contains project-local CLIs too.

Requests to weaken the sandbox no longer take a blanket yes

Trusting a project's own .nvx-policy.json when it loosens settings, and adding a host to the egress allowlist, both decide the security model rather than a step inside it. They were covered by -y/NVX_YES, and --agent-mode sets that yes — so an agent cloning a repository nobody had read would auto-approve that repository's request to disable containment.

Those two now need an interactive answer, or NVX_TRUST_YES — deliberately a different variable, because nothing sets that one by habit. Ordinary prompts still honour -y, so non-interactive installs do not stall.

Fixes

  • Installing a package with a lifecycle script hung forever. A contained process cannot create a named pipe, and Windows builds piped child stdio out of them, so npm's default of piping script output blocked before the child even existed. Lifecycle scripts now inherit stdio. Partly, and the limits are now written down: this fixes npm's own piping, not a postinstall that captures a subprocess itself. npm install esbuild still hangs inside the sandbox — install that one with --no-sandbox. nvx prints a hint naming this cause after two minutes instead of sitting silent.
  • Git Bash got no protection at all, while nvx doctor reported that it did. The shims were .cmd/.ps1 only, which bash never selects. Extensionless shims are now written for both shim directories.
  • nvx use silently did nothing in Git Bash, and still printed success. Shell detection always answered PowerShell on Windows, so nvx emitted assignments bash cannot evaluate. Auto-switch on cd never fired there either.
  • nvx doctor wrote files while claiming to diagnose, which also made its own missing-shim check unreachable — it regenerated what it was about to look for. It now reports first and repairs only under --fix.
  • Security prompts hung instead of denying when stdin was not a terminal — CI steps and agent harnesses stopped rather than failing closed.
  • The sandbox refused to start against an nvm-installed node, which is how most Windows developers install it.
  • nvx node --strict app.js ran uncontained — the flag was accepted and discarded.
  • nvx cleanup deleted sandboxes that were still running.
  • An interrupted install blocked that version permanently.
  • A permission grant that could never succeed was retried on every launch, costing several seconds each time.
  • --agent-mode is documented as -y -q and only did the -y half. It now sets quiet too, which gates success and info lines only — warnings and errors still print.
  • nvx setup --undo could not remove the profile-root grant the docs said it removed.
  • Four documented policy keys did nothing. prompts.interactive, prompts.non_interactive, prompts.network_unknown and isolation.filesystem.mode were parsed, merged and scaffolded by nvx policy init, and read nowhere — tightening one was silently ineffective. They are no longer written into new policies and the README marks them unimplemented. Existing policies still parse.

Speed, stated honestly

A contained command costs roughly 1–2 seconds depending on the machine, against ~0.4s uncontained. The first contained run after nvx stages a runtime copies the whole distribution and has been measured between 45 seconds and 3 minutes. The ~38ms figure in the README measures shim dispatch — the uncontained path — not the sandbox. Measure it yourself before depending on it.

Known limitations

The README has a Known limitations section covering what the sandbox does not do — a .env inside the project is readable by a contained install, your own code and project-local CLIs are not contained by default, directory names outside the project are visible on Windows, pre-0.5.0 permissions and a pre-0.5.0 loopback exemption persist until dealt with, npm install -g is refused, and a contained process cannot capture a child's output. Each is pinned by a test. Per-OS detail, and how every guarantee was measured, is in docs/enforcement-matrix.md.

macOS and Linux enforcement is documented but was not re-verified for this release on that hardware; the matrix says which rows that affects.

Verifying downloads

Each asset ships a .sha256 sibling, with a combined SHASUMS256.txt. Binaries are built by GitHub Actions with a build-provenance attestation.