Key takeaways
- npm 12’s default allow-git=none stopped the synthetic git dependency before clone or script execution with EALLOWGIT. That is a real new gate.
- After --allow-git=all, npm 12.0.2 printed that prepare was blocked by allowScripts, yet the trace proved prepare had already executed twice inside a temporary git-clone directory.
- The bundled pacote path checks ignoreScripts before spawning the preparation install; the inspected path does not check the project allowScripts decision at that point.
- A commit SHA fixes which Git tree npm fetches, but does not make that tree inert. A prepare hook can execute while npm converts the tree into an installable package.
- For untrusted git dependencies, keep --ignore-scripts plus an isolated, secret-free runner as the deny-by-default boundary; treat allowScripts as useful policy, not a complete sandbox.
The sharp question: which gate runs before git preparation?
npm 12 made two security-relevant defaults explicit. Git dependencies are disabled unless the operator allows them, and dependency lifecycle scripts are blocked unless the root project covers them with allowScripts. Those controls sound composable: permit one pinned Git source, leave its hook unapproved, inspect the result, then decide whether to run the hook.
My probe found a narrower and more consequential boundary. With npm 12.0.2, --allow-git=all allowed the fetch. npm then warned that the synthetic package’s prepare hook was blocked because it was not covered by allowScripts. But the hook had already written an artifact and appended two records from npm’s temporary clone. The warning accurately described one lifecycle decision, but it did not describe every script execution that had occurred while preparing the git dependency.
Chronology: npm 12 changed the default contract in July
The npm CLI project dates v12.0.0 to July 8, 2026. Its breaking changes say allow-git and allow-remote now default to none and must be set to all or root to install Git or user-supplied tarball URL dependencies. The same release says dependency lifecycle scripts are blocked by default unless the root package’s allowScripts policy permits them, with install-scripts tooling and rebuild as the approval path.
The current registry returned npm 12.0.2 as latest on August 28, while 11.19.1 remained the next-11 line. npm 12.0.2 requires Node ^22.22.2, ^24.15.0 or >=26.0.0. I therefore did not treat the host’s Node v24.10.0 as a valid npm 12 test runtime; the npm 12 runs used a downloaded Node v24.15.0 binary. The baseline remained npm 11.6.0 on Node v24.10.0 so version claims stay separated.
Git dependencies are source builds, not registry tarballs
npm’s package.json documentation says a Git URL can include a commit-ish and that an exact commit causes npm to clone that commit. It also documents a separate build flow. If the repository uses workspaces or defines build, prepare, prepack, preinstall, install or postinstall, npm clones the repository into a temporary directory, installs all of its dependencies, runs relevant scripts, packs the resulting directory and installs that package.
That distinction is the mechanism behind this investigation. A registry tarball can arrive with prebuilt output. A Git dependency with prepare is source that npm decides must be made installable. The command has at least two conceptual script surfaces: preparation inside the temporary clone, and lifecycle handling of the resulting dependency in the consumer tree. A policy message about the second surface does not automatically prove that the first surface was inert.
The synthetic repository and exact probe
The dependency was intentionally small. package.json named aag-git-dep-probe version 1.0.0 and defined only "prepare": "node prepare.cjs". prepare.cjs wrote "built by prepare" to built.txt, then appended one line to the path in AAG_TRACE. The second commit recorded process.version, process.execPath, process.cwd() and npm_config_user_agent. The consumer referenced git+file:///tmp/aag-git-dep-probe.BkD8xu/dep#d40165addbb4c8ab3df8a6cf35981657786f9d60.
The command under test was: AAG_TRACE=/tmp/aag-git-dep-probe.BkD8xu/npm12-v2-trace.log npx --yes -p node@24.15.0 -p npm@12.0.2 -- npm install --allow-git=all --foreground-scripts. A local git+file URL kept network fetch, remote authentication and repository mutability out of the mechanism test. The commit SHA made the selected tree deterministic. The hook wrote only in the synthetic package and a dedicated temporary trace path.
First result: npm 12 refused the Git dependency by default
Without --allow-git=all, npm 12.0.2 exited 1 with code EALLOWGIT. Its output said fetching packages of type git had been disabled and named the exact aag-git-dep-probe git+file URL it refused. No trace file existed. That result confirms the release-note claim on this local dependency form and supported runtime.
This gate matters. A repository cannot silently add a Git dependency and expect npm 12’s default install to fetch it. But the gate answers only whether this dependency type may enter the fetch path. Once an organization globally changes allow-git to all for compatibility, it has reopened the source-preparation path for every dependency matching that setting. Fetch permission is therefore not script permission, provenance review or execution containment.
Second result: the warning said blocked, while the trace said executed
With --allow-git=all and no allowScripts entry, npm exited 0 and installed one package. It printed: “1 package had install scripts blocked because they are not covered by allowScripts,” followed by “aag-git-dep-probe@1.0.0 (prepare: node prepare.cjs).” The installed package nevertheless contained built.txt, which existed only because prepare.cjs had run.
The external trace contained two lines. Both used cwd=/Users/calvin/.npm/_cacache/tmp/git-cloneIftV32, process=v24.15.0 and the npx-provided Node executable. In other words, the writes came from the temporary clone preparation path, not from an application call after installation. The hook ran twice in this bounded run. I did not infer that every Git host, cache state or package shape always produces two invocations; the defensible finding is at least one unapproved execution, with two observed here.
npm 11 established the older behavior and the stronger opt-out
On npm 11.6.0, a normal install of the same first commit ran prepare and installed built.txt. The trace again recorded two prepare lines from one temporary git-clone directory. package-lock.json resolved the dependency to the exact 40-character commit. This confirms that pinning and build execution coexist: the lock identifies source, while prepare transforms it.
A fresh npm 11 install with --ignore-scripts installed package.json and prepare.cjs but did not install built.txt and did not create the external trace. That is a concrete difference between “unapproved by allowScripts” in the tested npm 12 Git path and “ignore package scripts” in the tested npm 11 path. It does not prove --ignore-scripts is a complete containment system, but it did suppress the mechanism under test.
The bundled implementation matches the observation
I downloaded npm-12.0.2.tgz from the registry and inspected package/node_modules/pacote/lib/git.js. GitFetcher.#prepareDir reads package.json and returns early when there are no relevant scripts or workspaces. If preparation is needed, it explicitly checks this.opts.ignoreScripts and returns when that option is true. Otherwise it shells out to npm inside the clone so dependencies are in place, after which DirFetcher performs its own preparation and packs the directory.
In the inspected #prepareDir path, I found the ignoreScripts guard but no evaluation of the root allowScripts map before the spawned preparation flow. That source reading explains both halves of the local result: ignore-scripts can stop clone preparation, while an uncovered allowScripts warning can coexist with earlier preparation execution. This is an implementation observation for npm 12.0.2, not a claim about maintainer intent or a promise that later versions retain the path.
A neighboring open issue shows the policy boundary is still settling
npm CLI issue #9783 was opened July 18 and remained open when retrieved. Its reporter describes a user or global .npmrc allow-scripts setting being forwarded into Git-dependency preparation as an environment-layer policy and failing with EALLOWSCRIPTS. That is not the same claim as my no-allowScripts run, so I do not use it as a substitute for the trace.
It is useful independent operational evidence that Git preparation and script-policy propagation have active edge cases in npm 12. Teams should read the issue and current release notes before encoding a permanent migration rule. The practical response is not to assume npm 12 is insecure or useless; it is to test the exact dependency forms and policy layers that the organization intends to authorize.
What the result does and does not establish
Observed fact: on this supported Node/npm pair, default allow-git stopped the dependency; enabling Git fetch allowed prepare to execute in a temporary clone despite an uncovered allowScripts warning; --ignore-scripts suppressed the analogous npm 11 preparation. Source observation: npm 12.0.2’s bundled GitFetcher explicitly guards clone preparation with ignoreScripts and does not visibly consult allowScripts in that method.
Inference: teams should not interpret an allowScripts blocked warning as retrospective proof that no code from a Git dependency executed during package construction. Not established: remote Git transport behavior, Windows or Linux behavior, every npm 12 configuration, exploitability of a real package, secret access, sandbox escape, or the exact reason for two invocations. The synthetic hook’s ability to write a trace reflects the runner authority intentionally given to it; no privilege escalation occurred.
Commit pinning solves identity drift, not execution authority
The lockfile’s resolved field ended in the selected full SHA, which is better than following a moving default branch. A reviewer can inspect that commit, archive it and detect an unexpected resolution change. But the SHA identifies a tree containing executable build instructions. It does not turn prepare into data, constrain the shell, remove dev dependencies from the preparation install or prevent network and filesystem access available to the runner.
Treat a Git dependency as source acquisition plus build. Review the pinned commit, its submodules and its package metadata; mirror it when availability matters; and decide whether to consume a maintainer-produced registry artifact, build an internal artifact in a quarantined pipeline, or allow npm to prepare source during each install. “Pinned” should appear under provenance evidence, not under execution containment.
The safe CI and coding-agent pattern is two-stage
Stage one resolves and inspects in a disposable environment with no deployment, cloud, registry-publish or repository-write credentials. Keep Git and remote dependencies denied unless the manifest inventory expects them. For untrusted Git source, run with --ignore-scripts, capture the lockfile, enumerate lifecycle hooks and workspaces, and compare the selected commit with the reviewed source. Block private-network access and restrict egress to required source endpoints.
Stage two builds only reviewed dependencies in a separate disposable runner. Give it the minimum read-only source and output location, no signing or deployment identity, and a time and network budget. Promote a content-addressed artifact into the normal build rather than repeatedly granting arbitrary source hooks access to a rich CI workspace. A coding agent may propose the dependency and evidence, but deterministic policy—not the model—must decide which source and scripts may execute.
Migration gates for npm 12
Before enabling allow-git, inventory every git, hosted-git and remote tarball reference in package.json and lockfiles. Record owner, commit or tag, why a registry artifact is insufficient, expected lifecycle scripts, submodules, build dependencies and required network access. Test npm ci from an empty cache. A warm cache can hide the preparation path the production runner will encounter later.
Then create fixtures for a registry dependency, local directory, local tarball and Git dependency with visible benign hooks. Verify default, approved, denied, ignore-scripts and rebuild paths on every supported OS and Node/npm pair. Treat a warning as telemetry, not proof: assert on external trace absence and artifact contents. Roll back to npm 11 with --ignore-scripts or remove Git source dependencies if a mandatory no-execution gate cannot be demonstrated.
Decision: deploy npm 12, constrain Git source builds
Adopt npm 12’s default-deny fetch and dependency-script policy; they reduce ambient authority and make exceptions reviewable. Constrain Git dependencies more tightly than registry artifacts. Do not set allow-git=all organization-wide merely to silence migration failures. Prefer explicit project policy, reviewed commit identities and isolated source-build jobs. For untrusted repositories and autonomous agents, keep ignore-scripts at the first install boundary.
Pause or reject a workflow when it requires Git dependency preparation in a credentialed or shared runner, when the team cannot prove which scripts execute before approval, or when a cache miss changes the execution path without detection. Resume only after the exact supported npm release passes a cold-cache trace test and the runner can survive arbitrary code from the approved source. The rollback condition is any script execution outside the recorded approval and build stage.
Copy-ready npm 12 Git-dependency authorization record
Complete this for one project and one supported Node/npm pair. A policy warning is not execution evidence; attach a cold-cache trace and artifact inventory.
Entries stay in this browser tab and are not submitted to AccessAllGPT. Blank responses are copied as [Unresolved].
OS and architecture; Node and npm exact versions; npm tarball integrity; cache state; command and environment.
Package, Git URL, exact commit, host, subdirectory, submodules, lockfile resolution and reviewed source archive.
allow-git and allow-remote values; project versus user/global source; why Git source is required; prohibited alternatives.
build, prepare, prepack, preinstall, install, postinstall and workspace triggers in the root and transitive Git build tree.
Cold-cache external trace; temporary clone path; process and executable; invocation count; generated files; expected and unexpected writes.
Root package allowScripts map; resolved identity; pending or denied entries; warning output; approval and rebuild results.
Disposable runner; secrets absent; filesystem mounts; UID; process limits; network allowlist; private-network denial; teardown.
Built artifact digest; provenance record; storage; consumer install mode; verification that later installs do not rebuild source.
No-execution assertions; mandatory failures; npm/version rollback; dependency removal or vendoring path; incident owner.
Reject, inspect-only, isolated build or approved consumption; owner; expiry; change events that force a fresh cold-cache test.
Primary sources
Browse the publication-wide evidence index →
- npm CLI v12.0.0 release notesnpm CLI project · Reviewed: Release date, breaking changes for allow-git, allow-remote, supported Node versions and dependency lifecycle scripts · Retrieved · Supports: npm 12.0.0 was released on 2026-07-08 and changed git and remote dependency fetching plus dependency-script policy: git fetches require an explicit allowance and dependency scripts are blocked unless covered by the root allowScripts policy.
- package.json: Git URLs as Dependencies (npm CLI 12.0.2)npm documentation · Reviewed: Git URL grammar, commit-ish resolution and the documented temporary clone, dependency install, script, pack and install flow · Retrieved · Supports: npm documents that selected scripts or workspaces make a git dependency a build input: npm clones it into a temporary directory, installs its dependencies, runs relevant scripts, then packs and installs the result.
- Config: allow-scripts and ignore-scripts (npm CLI 12.0.2)npm documentation · Reviewed: allow-scripts matching and scope, project allowScripts policy, override behavior and ignore-scripts semantics · Retrieved · Supports: npm describes allow-scripts as covering dependency install-time hooks including prepare for non-registry dependencies, while ignore-scripts suppresses package.json scripts except explicitly invoked script commands.
- pacote GitFetcher implementation bundled with npm 12.0.2npm CLI project · Reviewed: GitFetcher constructor, #prepareDir, temporary clone and tarballFromResolved implementation paths · Retrieved · Supports: The bundled GitFetcher checks ignoreScripts before spawning npm inside the temporary clone, but the inspected #prepareDir path does not evaluate the root allowScripts policy before that preparation install and pack flow.
- Issue #9783: allow-scripts forwarded to git-dependency preparationnpm CLI issue tracker · Reviewed: Issue title, reproduction context, open state, creation date and latest activity visible through the GitHub API · Retrieved · Supports: An independently filed open issue documents a neighboring npm 12 git-preparation and allow-scripts interaction. It is corroborating community evidence, not proof of the exact local result reported here.
- Backstabber's Knife Collection: A Review of Open Source Software Supply Chain AttacksDIMVA preprint on arXiv · Reviewed: Abstract, dataset scope, submission history and stated supply-chain attack model · Retrieved · Supports: The authors report a manually collected dataset of 174 malicious packages from npm, PyPI and RubyGems dated 2015–2019 and model code execution at different supply-chain stages; this supplies historical independent context, not an npm 12 measurement.
Limitations
This is a synthetic local git+file reproduction on macOS 26.0.1 (Darwin 25.0.0), not a vulnerability disclosure, exploit, malware study, registry-package audit or cross-platform survey. npm 12.0.2 ran with a supported Node v24.15.0 binary through npx; the npm_config_user_agent string inherited from the outer npm 11.6.0 launcher, so process.version and process.execPath—not that inherited string—identify the hook runtime. The probe observed two prepare writes but did not isolate why two invocations occurred or prove that count generalizes. A local Git repository avoids remote transport, submodules and authentication. Source inspection covered the bundled GitFetcher path relevant to this probe, not every npm and Arborist policy path. Issue #9783 concerns a neighboring configuration interaction, not independent reproduction of this exact result. The 2020 paper supplies historical supply-chain context and does not evaluate npm 12. Future npm releases may change these controls.
Disclosures
AccessAllGPT created and executed only synthetic packages in dedicated temporary directories. No third-party package was accused, scanned or modified; no credential, private repository, production runner or external target was used. npm maintainers did not review this article before publication, and the observed behavior is reported as a bounded npm 12.0.2 result rather than maintainer intent. No vendor supplied access, data, payment or endorsement. AccessAllGPT Research is operated by NeuralArc, is independent, and is not affiliated with OpenAI or npm. Publication-wide relationships are listed on the disclosures page.
Further AccessAllGPT guidance
- npm Install Is an Execution Boundary
- Node’s Permission Model Is a Seat Belt, Not an AI Code Sandbox
- Before You Give a Coding Agent Repository Access
- MCP Token Passthrough Needs an Audience Boundary
- Human-in-the-Loop AI: Put Approval at the Consequence Boundary
- AccessAllGPT Research methodology
- Publication disclosures
Continue the research
Get evidence-led updates for teams making production AI decisions.