How sandbox runs risky installs in a throwaway container
15 Jun 2026@jagreehal/sandbox-node runs your npm install in a throwaway container that can see your project and the registry, and nothing else.
Install scripts still run. node-gyp still builds. Your SSH keys, npm token, cloud credentials, and .env are not in the box, so a malicious dependency has nothing to steal and nowhere to send it.
The one-picture version
Put sandbox in front of the command you already run:
sandbox npm install
sandbox pnpm add zod
sandbox npm run dev
Under the hood the risky code runs in a container that can reach your project files and the npm registry. It cannot reach your home directory, your credentials, or anywhere else on the internet. When the command finishes, the box is deleted and your installed dependencies stay behind.
This is install-time containment, not a general sandbox. It targets the one moment that the previous post is about: when untrusted dependency code executes with your access.
Why this is worth it
You do not have to stop using normal package managers. You do not have to disable install scripts and break native modules. You do not have to read every transitive dependency before you clone a repo.
You keep the workflow you already use. The difference is that install-time code runs somewhere with no secrets to steal, nowhere to phone home, and fewer places to persist. Same workflow, smaller blast radius.
That gives you a handful of practical benefits:
- Safer installs without changing package managers
- Install scripts still work
- Better defaults for agents and CI
- Less reliance on perfect dependency review
- One tool and one config, not a toolchain to wire up
One attack path, end to end
You add a package. One dependency under it ships a postinstall script. During install, that script reads ~/.npmrc, checks ~/.aws/credentials, writes a hook into .git/hooks, and makes an HTTPS request to a host you have never heard of.
On your host, that works because the script runs as you. In sandbox, each step hits a different wall. The credentials are not mounted. The common persistence paths are read-only. The network request has no route unless you allowed that host.
Three layers: check it, contain it, catch it
A supply-chain worm needs three things to work: a version fresh enough to still be live, something worth stealing, and a way out. sandbox works in three layers that line up against those, so no single bypass defeats it: it checks a package before the box runs, contains the code while it runs, and catches anything that tries to get out.
Check it before the box runs
Before any dependency code executes, sandbox vets what you are about to pull. This is the part that reaches out to external sources to confirm there are no known issues:
- Risk hints inspect each direct package before it installs. By default they use the metadata npm already returns, with no extra network calls: install scripts, versions published in the last hours or days, brand-new packages, newly added binaries, deprecations, a name within one or two edits of a popular package (a typosquat, checked against a corpus of around 2,500 names), a release that dropped the npm provenance attestations its earlier versions shipped, and a first-time or long-dormant publisher (a maintainer-takeover signal). The
thoroughlevel, which thestrictpreset turns on, reaches further out to the npm downloads API and DNS to flag missing repository or license metadata, suspiciously low download counts, and a maintainer email domain that no longer resolves (a known route to seizing an npm account). They warn by default;--fail-on-riskmakes them block. - A release-age cooldown refuses any version published more recently than your threshold: three days on the
balancedandagentpresets, seven onstrict. Worms detonate within hours, so the install never even resolves a version that fresh, falling back to the last one that has aged past the threshold. This is the single control the Shai-Hulud incident named most effective. - A known-malware check queries the OSV database for the exact version you would pull and refuses anything flagged as malware (
MAL-...), even a version old enough to clear the cooldown. - Your own blocklist blocks immediately when you already know a package is bad, without waiting for OSV to catch up: a committed
sandbox.advisories.jsonfor your team, plus malware feeds you trust, such as Aikido's public npm malware list. sandbox preflightruns all of the above without installing anything, so you can review a package, or a whole pull request's dependency changes, before it touches your machine.
Contain it while it runs
A check is a signal, not a guarantee, so the vetted install still runs inside the throwaway box. Inside, there is nothing to take and no way out:
- No credentials. The container starts almost empty:
SANDBOX=1,HOME=/root, and little else. No~/.ssh,~/.npmrc,~/.aws, or home directory is mounted, so a script that grepsprocess.envfor an AWS key finds nothing. - Persistence is read-only.
.git,.husky,.claude,.vscode, andpackage.jsonare mounted read-only, so an install cannot plant an auto-running hook. - No route off-box. The container sits on an internal network with no gateway. Its only exit is a proxy that forwards to allowlisted hosts and nothing else, so even malware that ignores
HTTP_PROXYis stuck. UDP and ICMP have no route either, which closes DNS tunnelling and ICMP exfiltration, not just HTTP. - No cloud-metadata pivot. The metadata endpoints (IMDS) are blackholed, so a dependency cannot reach the magic address that would hand it your cloud role's credentials.
- Stripped capabilities.
--cap-drop ALL,--security-opt no-new-privileges, and container-root is not host-root.
Catch it and prove the box held
The last layer is evidence: knowing something tried, and being able to show it did not get out.
- An egress tripwire logs every blocked outbound attempt, the highest-signal event the tool produces, and
--fail-on-egressturns one into a failed build. - Canary honeytokens plant fake-but-realistic AWS, Stripe, and Slack credentials in the install environment, exactly what a thief greps for. If one ever shows up leaving the box, that is unambiguous proof of theft and the run fails hard. On by default in the
strictandagentpresets. - A retroactive sweep (
sandbox scan) re-checks your committed lockfile against OSV later, catching dependencies that turned malicious after you installed them. - A committed-secrets scan (
sandbox secrets) checks the repo itself for credentials you may have committed, reporting where, with the values redacted. - Signed receipts and a tamper-evident audit log turn "the boundary held" into something a later CI stage or a third party can verify without re-running it.
sandbox demoruns four real supply-chain attacks (credential theft, a persistence hook, a cloud-metadata pivot, network exfiltration) against the live sandbox and shows each one contained. It exits non-zero if any attack gets through, so it doubles as a test that the boundary still holds.
It works across npm, pnpm, yarn, and bun, plus runners like npx, bunx, node, tsx, and vite. Anything that pulls new versions goes through the same checks, so update, upgrade, and audit fix are covered too, not just first install.
One tool instead of a toolchain
None of these checks is a new idea on its own. You could assemble most of them from separate tools: one to review packages before install, one to block known malware, one to enforce a minimum release age, and a devcontainer for isolation. Then you wire them together, keep four configurations in sync, and hope every developer set them up the same way.
sandbox puts review, age-gating, malware-blocking, and containment behind one prefix and one config file. You get the safety and the developer experience in one place, instead of orchestrating a toolchain and hoping it holds.
That single file is also what turns this from a personal habit into a team decision. Commit sandbox.config.json and the boundary is reviewed in a pull request, like any other code. sandbox verify enforces it in CI so nothing merges around it, a personal override that loosens it is flagged on every run, and a badge can advertise that installs go through it. Everyone installs against the same policy, whether or not they remember to.
Limits
You should know the edges before you adopt it.
Your source tree stays writable during a normal install, because package managers need a writable root and pnpm writes temp files there. A malicious dependency can still overwrite a file in src/ during install. You will see it in git diff, and --frozen makes the whole tree read-only on every package manager except pnpm. What sandbox blocks is credential theft, persistence, and exfiltration. It does not promise source immutability unless you ask for it.
Anything you grant is in scope on purpose. Forwarding a private-registry token with --env NPM_TOKEN, mounting a path, or turning network on are explicit opt-ins. Grant the minimum.
It needs Docker (or a Docker-compatible engine like OrbStack or Podman) and runs on macOS or Linux, or Windows under WSL2.
How it sits next to other tools
Devcontainers give you a full containerised workspace, but a devcontainer does not make npm install safe: inside it, the install still runs with whatever the container can reach. sandbox fills the install-time gap a devcontainer leaves open, and it can generate the devcontainer for you when you want a persistent environment. That is composition, not competition.
The same is true of Claude Code's built-in OS sandbox: run that for the agent's whole blast radius, and sandbox for the supply-chain surface on top.
Bottom line
sandbox does not try to read the dependency code in time, recognise the package, or out-guess the release timing. It runs the install in a box that has nothing worth stealing and no way to send it. Same workflow, smaller blast radius.
The next post covers setup: getting the sandbox command, the one-button onboarding, making it automatic so you never type the prefix, and wiring it into agents and CI.