If you ran npm install this morning and nothing broke, congratulations — you might already be running npm 12 without knowing it. Or maybe you’re still on 11.x and wondering what all the fuss is about.
Either way, npm 12 dropped this week with the most significant security change the package manager has seen in years. Install scripts — those preinstall, install, and postinstall hooks that run automatically every time you add a dependency — are now disabled by default. And I mean fully disabled. Not a warning. Not a prompt. Just silent refusal.
I’ve been testing this on a few side projects the past couple of days, and honestly? The change caught me off guard during the first install. A library I’d been using for months just silently skipped its build step. No error. No terminal fireworks. Just a package that didn’t work the way I expected. That’s when I realized: this isn’t a minor deprecation. This is a fundamental shift in how npm handles trust.

What Actually Changed in npm 12
GitHub officially announced npm 12 on July 8, and the release notes boil down to three default changes that all point in the same direction: stop running code you didn’t explicitly invite.
1. allowScripts Is Off by Default
This is the big one. Before npm 12, installing a package meant trusting every single one of its lifecycle scripts — including scripts from transitive dependencies you’ve never heard of. I wrote about a similar attack surface in Phantom Squatting: When AI Hallucinates Domains That Attackers Use Against You. A compromised package with a postinstall script could run arbitrary code on your machine, your CI runner, or your production build server, and npm would happily execute it.
In npm 12, that stops. preinstall, install, and postinstall scripts from dependencies no longer run unless you explicitly approve them. Even implicit node-gyp rebuild calls — triggered when a package has a binding.gyp but no install script — are blocked.
2. –allow-git Defaults to None
Git dependencies, both direct and transitive, won’t resolve automatically anymore. If a package depends on a GitHub repo, you’ll need to explicitly allow it with --allow-git. This closes an attack vector where a malicious .npmrc inside a Git dependency could redirect the Git binary to something else entirely.
3. –allow-remote Defaults to None
Remote URL dependencies — those https:// tarball links you sometimes see in package.json — are now opt-in as well. Same reasoning: a remote URL points to code outside the registry, outside its auditing, outside its security guarantees.
Why Now? The Supply Chain Wake-Up Call
GitHub has been telegraphing this move for months. The npm approve-scripts feature landed back in February, and the breaking changes preview went up in June. But the urgency — like I detailed in How to Lock Down Your Supply Chain When AI Writes Your Code — is rooted in a hard truth: install-time lifecycle scripts are, in GitHub’s own words, “the single largest code-execution surface in the entire npm ecosystem.”
Think about it. Every time you run npm install, you’re implicitly trusting hundreds of authors you’ve never met. A single postinstall script in a deep transitive dependency — one you didn’t even know existed — could exfiltrate your .env file, install a backdoor, or modify your source code. And because these scripts run during installation, most developers never even see them. They just assume the install succeeded and move on.
The npm 12 change flips the default from “trust everything” to “trust nothing.” It’s a zero-trust model for package installation, and honestly, it’s about time.
How to Check If You’re Affected
Before you upgrade to npm 12, you can test your projects against the new behavior using npm 11.16.0 or newer:
# Upgrade to the latest npm 11.x first
npm install -g npm@latest
# Run install with warnings enabled
npm install
# If your project has packages with scripts, you'll see warnings like:
# "npm WARN lifecycle ... is blocked because allowScripts is disabled"
Don’t just glance at the output and call it done. Actually inspect which packages have scripts and what those scripts do. The npm approve-scripts command gives you full visibility:
# See which packages have blocked scripts
npm approve-scripts --allow-scripts-pending
This command lists every package with lifecycle scripts and marks them as “pending.” You can then approve or deny each one.
Step-by-Step: Approving Scripts for Your Project
Here’s the exact workflow I used to migrate one of my Node.js projects — a small API server with about 80 dependencies — to the npm 12 model:
Step 1: Upgrade to npm 11.16.0+
npm install -g npm@11
npm --version # Should show 11.16.0 or higher
Step 2: Review Pending Scripts
npm approve-scripts --allow-scripts-pending
This produces a list like:
Package Scripts Status
esbuild install pending
node-sass install pending
sharp install, preinstall pending
sqlite3 install pending
Step 3: Approve Trusted Packages
# Approve specific packages you trust
npm approve-scripts esbuild sharp sqlite3
# Or approve all pending (not recommended — review first)
npm approve-scripts --all
Step 4: Deny Everything Else
# Deny packages whose scripts you don't need
npm deny-scripts node-sass
The resulting allowlist gets written to your package.json under a new "allowScripts" field:
{
"allowScripts": {
"esbuild": true,
"sharp": true,
"sqlite3": true
}
}
Commit this file. The allowlist travels with your repo, so every developer and every CI pipeline gets the same trust decisions.
What About Git and Remote Dependencies
If your project uses Git dependencies like this:
{
"dependencies": {
"my-lib": "github:user/my-lib#v1.0"
}
}
You’ll need to pass --allow-git during install:
npm install --allow-git=my-lib
The same applies to remote URL dependencies. Instead of resolving silently, npm 12 requires explicit consent through --allow-remote.
For projects with several Git or remote dependencies, you can set these in .npmrc so you don’t have to remember the flags every time:
# .npmrc
allow-git=my-lib,another-lib
allow-remote=tarball-dep
CI/CD Pipeline Changes
This is where things get practical. If your CI pipeline runs npm install without any special flags, it will break when you upgrade to npm 12 — assuming your project has any packages with lifecycle scripts.
Here’s how to prepare your CI configuration:
# GitHub Actions example
- name: Install dependencies
run: npm ci
env:
NPM_CONFIG_ALLOW_SCRIPTS: "esbuild,sharp,sqlite3"
Or, if you’ve committed the package.json allowlist, npm 12 reads it automatically during npm ci. The allowlist approach is better because it’s version-controlled and doesn’t require environment variable gymnastics.
The GAT Deprecation: What You Need to Know
npm 12 also deprecates Granular Access Tokens (GATs) that bypass 2FA. Two specific changes:
Starting August 2026: GATs configured to bypass 2FA can no longer perform sensitive actions — creating tokens, changing account passwords, modifying package access, managing organization membership.
Starting January 2027: GATs lose the ability to publish directly. Instead, they’ll only be able to stage a publish, which becomes public only after a human 2FA approval step.
GitHub recommends migrating to trusted publishing via OIDC or staged publishing with human approval. If you’re running automated npm publish workflows (and who isn’t these days), this is your cue to audit those tokens.
What This Means for Developers Like Us
I’ll be honest: the first time I ran into a blocked script on npm 12, I was annoyed. My immediate reaction was “just turn it back on.” But after looking at the list of blocked packages in my project and actually reading what those scripts do, I changed my mind.
One package had a postinstall script that downloaded a binary from a CDN over plain HTTP. No hash verification. No version pinning. Just a blind curl | sh equivalent running inside my install pipeline. Would I have caught that without npm 12’s block? Probably not.
That’s the real value of this change. It’s not about making installation harder — it’s about making the hidden visible. Every blocked script is a decision point. And forcing developers to make those decisions consciously is, I think, exactly the right call.
Bottom Line
npm 12 is shipping now. Upgrade to npm 11.16.0 today, run npm approve-scripts --allow-scripts-pending on your projects, review what comes up, and commit your allowlist. Your future self — and your CI pipeline — will thank you.
The default has flipped from “install and trust” to “install and verify.” That’s inconvenient in the short term. But in an ecosystem where a single compromised postinstall script can take down your entire application, as I argued in Better Models, Worse Tools — When Smarter AI Actually Breaks Your Workflow, inconvenience is a small price to pay when the alternative is a compromised build pipeline.