Secure Coding with AI - Supply Chain Protection for Node Projects and Guardrails for Agents
Practical security defaults for npm and pnpm so AI-assisted coding does not turn into a supply chain risk.
Short version: Teams that let AI or agents write code and install dependencies need safety rails. This guide outlines practical defaults, sensible workflows, and a least-privilege mindset for running server-side code.
Why this matters right now
In the JavaScript ecosystem, new packages and updates ship every day. That is great for innovation, but it also opens the door to supply chain attacks.
Recent example: compromised npm versions around the Axios ecosystem (malicious versions / remote access trojan). Background and analysis:
Typical attack paths:
- Maintainer account compromise
- Dependency tree / transitive dependencies
- Typosquatting (a package name that looks almost correct)
- A malicious update that initially looks normal
Goal: Even if something goes wrong, the blast radius should stay small.
1) 60 seconds: what npm actually is
- npm is both a package manager and a registry for Node and JavaScript.
- You install packages with
npm install <package>. - Dependencies are declared in
package.json. - Installed packages land in
node_modules. - A lockfile (
package-lock.json,pnpm-lock.yaml) pins exact versions.
Important: You are pulling in code from the outside world. That is the core supply chain risk.
2) The biggest risks during install
2.1 Malicious updates or new packages
- New versions can be compromised and are often only discovered later.
2.2 Lifecycle scripts during install
Packages can automatically run scripts during install:
preinstallinstallpostinstall
There can be valid reasons for this, such as native binaries or build steps, but it is also an attack surface.
3) The 2 most important immediate measures
3.1 Release cooldown: do not trust brand-new releases
Use a cooldown window, for example 7 to 14 days.
Important: This option currently belongs to pnpm, not the npm CLI.
pnpm-workspace.yaml:
minimumReleaseAge: 20160
20160 minutes equals 14 days.
Why this helps: If a compromised package was just published, you do not pick it up immediately.
Tip: For production systems, 7 to 14 days is often a good starting point. For very sensitive projects, consider 30 days.
3.2 Block install scripts
.npmrc:
ignore-scripts=true
What happens then? Dependencies do not run preinstall or postinstall scripts. That lowers risk, but some packages depend on those steps and may break.
4) Why postinstall can still be “necessary”
Some packages need install-time steps for:
- Downloading or setting up native binaries
- Compilation
- Platform-specific checks
If you enable ignore-scripts=true, this can cause errors.
A pragmatic approach:
- Default: scripts off
- Exception: temporarily allow scripts if you truly need that package
- Then switch back to scripts off
Important: never leave scripts globally enabled forever “just so everything works” if your actual goal is security.
5) AI and agents: the most important policy
When AI or agents work in your repo, the risk is not “AI is evil”. The real issue is: AI is fast.
5.1 Golden rule
Agents must not install dependencies without approval.
5.2 Minimal policy
If the AI wants to install a package, it must provide:
- Why is the package needed? (specific use case)
- Alternatives (native solution? already available in the project?)
- Risk check (repo or owner, downloads, maintenance, recent releases)
- Minimal scope (smallest possible addition, no unnecessary tool sprawl)
- Test plan (how do we verify nothing is broken or malicious?)
5.3 Recommended workflow
- AI only proposes commands
- You approve installs or updates
- Then run tests, lint, and build
6) Reproducible installs: lockfile plus CI
This is not flashy, but it is extremely effective.
6.1 Commit the lockfile
package-lock.jsonorpnpm-lock.yamlbelongs in the repo.
6.2 Strict CI installs
- npm:
npm ci - pnpm:
pnpm install --frozen-lockfile
Why: No surprise version jumps in CI or production builds.
7) Audits: useful, but do not trust them blindly
npm auditis a good starting point.- But not every audit finding is a real risk in your context.
Pragmatic approach:
- Block at least high/critical in CI, or warn and require manual review.
8) pnpm and Yarn: short comparison
If you are already considering pnpm:
- pnpm has strong supply chain features and is popular with many teams.
- Important highlight: pnpm can control build and lifecycle scripts via an allowlist, instead of using a strict all-or-nothing switch.
8.1 pnpm: allowlist for build scripts
pnpm v10 leans heavily into the idea that dependencies should not be allowed to run arbitrary install scripts automatically. Instead, you can explicitly allow which packages are permitted to run build scripts.
In practice, this looks like an allowlist or approval flow such as:
onlyBuiltDependencies(only these dependencies may run build scripts)- or a workflow using
pnpm approve-builds(interactively approve what is allowed to build)
In practice, pnpm offers a useful middle ground between “scripts completely off” and “scripts always on”.
9) Runtime security: least privilege for Node, Bun, and server code
Even if a malicious package gets in, you still want to prevent it from reading or writing everything.
9.1 Core principles
- Do not run as root
- Minimize filesystem access (only project paths or uploads, not your whole home directory)
- Isolate secrets (do not spread env vars everywhere)
- Minimize network access (only required hosts and ports)
9.2 Most practical option: containers
- Run the process as non-root
- Optionally use a read-only filesystem
- Only allow write access to defined volumes
- Drop capabilities
Advantage: practical to implement and suitable for many production setups.
9.3 Alternative: systemd sandboxing on Linux
- Run the service as a dedicated user
ProtectSystem=strictReadWritePaths=only for required pathsNoNewPrivileges=true
9.4 Alternative for strict runtime permissions: Deno
Deno includes a built-in permission model for filesystem, network, and environment access. That makes it possible to grant capabilities explicitly instead of allowing everything by default.
One important detail: module loading and general file access are not exactly the same thing. Statically analyzable imports can still work without --allow-read, while normal reads of local files remain blocked.
Example:
- Without
--allow-read, a process cannot arbitrarily read local files.
9.5 Bun
Bun is Node-compatible, but does not provide a comparable permission model like Deno. → In practice, you isolate it through containers, systemd, or the OS.
10) Copy/paste: secure project defaults
10.1 pnpm-workspace.yaml for release cooldown
# Cooldown for brand-new releases
minimumReleaseAge: 20160
10.2 .npmrc for install scripts
# Do not run lifecycle scripts from dependencies
ignore-scripts=true
10.3 Short team rule
- No installs without approval
- Always include: purpose, alternatives, scope, and a test plan
11) Frequently asked questions
”Doesn’t ignore-scripts break a lot of things?”
Yes, some packages depend on it. That is exactly why it is a security default. If you need scripts:
- allow them temporarily
- keep the exception minimal and deliberate
”What about vibe coding?”
Vibe coding is fine, but package installation is a security boundary. That is where you need an extra rule.
12) Next steps
- Standardize secure install defaults across projects
- Define clear approval rules for AI-driven dependency changes
- Protect server runtimes with containers or system-level sandboxing