DEV Community

Alex
Alex

Posted on

Attempt to stop npm postinstall scripts from stealing your secrets

ringfence
Every time you run npm install, you're rolling dice. A postinstall script from some dependency three layers deep gets unrestricted access to your filesystem. .env files, SSH keys, cloud credentials, your whole $HOME directory. It can read it all, and it can ship it anywhere because the network is wide open.

In May 2026, a supply chain worm compromised 84 npm packages that had valid SLSA Build Level 3 provenance. Postinstall scripts siphoned CI/CD secrets, SSH keys, cloud credentials, crypto wallets. It spread quietly because everything looked legitimate. The attack got nicknamed "Shai-Hulud" after the Dune sandworm that swallows everything in its path.

ringfence blocks this class of attack at the OS level. Zero config. It wraps your package manager commands in a sandbox so a compromised dependency can't see anything worth stealing.

One thing to be clear about: ringfence doesn't stop you from installing malicious code. If a package is bad, it still gets installed. What ringfence does is make sure that the postinstall script running on your machine can't read your secrets, can't touch your SSH keys, can't sniff your .env. It sits between the compromised package and your filesystem, not between you and the registry.

How it works

Ringfence is basically a shim. It drops a handful of shell scripts at ~/.ringfence/bin/ and shoves that directory onto the front of your PATH. When you type npm install, the shim catches it and asks: does this command trigger lifecycle scripts? If yes, it goes through the sandbox. If it's npm run build or npm test or npx tsc, it passes straight through to the real binary. You pay about 80ms of Node startup overhead either way.

On Linux it leans on bubblewrap. The sandbox replaces $HOME with an empty tmpfs. After that it selectively brings back only what the package manager needs: .npm, pnpm's store directories, yarn's cache, and registry auth files like .npmrc (those are mounted read only). ~/.ssh, ~/.aws, ~/.gnupg stay invisible because they were never added to the blank tmpfs in the first place.

Next, every secret looking file in your project directory gets masked with /dev/null. .env, .env.*, *.pem, id_rsa*, credentials.json, service account JSON files, and a bunch of other patterns. The files are there, they're just zero bytes.

Environment variables that smell like secrets get unset: anything with TOKEN, SECRET, API_KEY, or PASSWORD in the name, plus prefixes like AWS_, GITHUB_, NPM_, DATABASE_. The network stays open. Registries, git deps, tarball URLs all work fine. The defense isn't blocking exfiltration. It's making sure there's nothing to exfiltrate.

On macOS there's no bubblewrap (no Linux namespaces), so ringfence falls back to Docker. The project directory gets copied to a temp location with all secret files filtered out, mounted into an ephemeral node:lts container with HOME=/work, and after the install finishes only the non-secret files sync back. It's slower than the Linux path, but the security model is the same.

The test suite includes a leak probe: a synthetic malicious script that tries reading .env files, SSH keys, ~/.aws/ credentials, ~/.gnupg/ keys, and secret shaped env vars. Under ringfence, every single probe fails.

Install

npm i -D ringfence && npx ringfence
Enter fullscreen mode Exit fullscreen mode

Two commands. It detects your OS, installs bubblewrap if you're on Linux (using whatever package manager your distro ships), validates Docker if you're on macOS, creates shims for npm/pnpm/yarn/bun, and adds itself to your shell's PATH via .bashrc, .zshrc, and .profile.

After that, every npm install, pnpm add, yarn add, or bun add runs sandboxed. You don't set anything up per project. Commands like npm run dev and npm test and npx go through untouched, those aren't install commands.

To get rid of it: npx ringfence-uninstall. Cleans up the PATH entries and deletes ~/.ringfence/.

Stuff worth knowing

Secret files become zero byte files, not missing files. Making a file absent risks breaking scripts that check for existence. An empty read is safer than ENOENT. The tradeoff: if a dependency genuinely needs a .env file with actual content during install, things break. The README is upfront about this.

Ringfence unsets env vars and masks files. Your GITHUB_TOKEN doesn't leak through the environment just because it wasn't stored in a .env file.

bwrap containers launched by ringfence include --die-with-parent. If ringfence crashes, the sandboxed process dies too. No orphaned containers.

On macOS, packages removed during a container run stick around in node_modules until you clean them manually. The sync back doesn't delete files, it only adds or overwrites. Uninstalled packages linger.

Ringfence only protects install time, when lifecycle scripts fire. Once packages are on disk, npm start or node server.js runs with full host access. That's intentional. It's a supply chain guard, not a runtime sandbox.

No Windows support.

Why bother

The TanStack worm hit packages that had provenance. SLSA Build Level 3, verified. Provenance says where code came from, not what it does. That's the whole problem.

Ringfence is two commands and covers npm, pnpm, yarn, and bun on Linux and macOS. If you're installing dependencies on either platform, there's not much reason to skip it.


If you give ringfence a try, I'd love to hear how it goes. The macOS path in particular could use more real world testing, so feedback from Mac users is especially welcome. Bug reports, edge cases, or just "works fine on my machine" — all useful. Drop an issue on GitHub.


If you give ringfence a try, I'd love to hear how it goes. The macOS path in particular could use more real world testing, so feedback from Mac users is especially welcome. Bug reports, edge cases, or just "works fine on my machine" — all useful. Drop an issue on GitHub.

https://ringfence.pages.dev/

Top comments (0)