Key takeaways
- Treat npm install as code execution, not as a file copy. Lifecycle hooks run during installation, and the shell that runs them is part of your trust boundary.
- Use --ignore-scripts by default for untrusted installs, but expect some packages to fail if they rely on lifecycle hooks or build-time compilation.
- Keep package installation in an isolated runner with no ambient secrets, a narrow filesystem view, explicit egress policy and a throwaway workspace.
- Do not assume a Node permission flag will stop dependency hooks once a shell or external tool is involved; the operating-system account still matters.
- If you must allow scripts, make the allowlist, the package provenance and the rollback path explicit before the install starts.
The question is whether install time can run arbitrary code
The security question is not whether npm can place package files on disk. It is whether the install step can execute commands before the application ever starts. The answer matters for developers, CI systems and coding agents alike because package installation often runs inside the same workspace that holds source, credentials and deployment logic.
npm’s own docs answer that question plainly. Lifecycle scripts are not a passive manifest field. They are executable hooks, and the docs list the order in which they run. That means a package manager is part downloader, part build system and part command runner. If you allow package installation, you are already allowing some code to execute.
What npm documents about lifecycle scripts
The scripts documentation says lifecycle scripts run after package changes are applied and after package.json and package-lock.json have been updated. It also lists the order for npm install as preinstall, install, postinstall, prepublish, preprepare, prepare and postprepare. The same page says scripts are passed to /bin/sh on POSIX systems or cmd.exe on Windows, which is the most important operational detail in the whole story.
npm also notes that scripts run from the root of the package folder. That makes relative paths predictable for package authors and dangerous for anyone who forgot that a hook is not “just build metadata.” A hook can read, write, delete and spawn with the authority of the account that launched npm. The package manager is not merely unpacking bytes; it is handing a shell line to the operating system.
The sandbox I used to verify the behavior
I created a temporary sandbox at /tmp/npm-scripts-audit with two local packages. The dependency package trap-door-dep had preinstall, install and postinstall hooks. Each hook ran a tiny Node command that appended a marker line to ../trace.log so the result would be visible even if stdout buffering or script output formatting changed. The application package depended on the dependency through a local file: reference, so the test did not need network access.
The environment was Node v24.10.0 and npm 11.6.0 on macOS 26.0.1. That matters because the article is not claiming that every historical or future version behaves identically. It is claiming that, in the current toolchain I actually ran, npm install executed the lifecycle hooks described by its own documentation.
npm install did execute the hooks
When I ran npm install in the application directory, npm added the dependency and wrote the trace file. The contents were exactly three lines: dep preinstall, dep install and dep postinstall. That is the clearest possible proof that the install step executed code I never called directly.
The output did not require a malicious package or an internet download to make the point. A benign local package was enough. If a local file dependency can run shell-backed lifecycle scripts during installation, then a remote dependency can do the same unless you explicitly suppress or constrain it. The risk is structural, not theoretical.
What --ignore-scripts changed
I deleted node_modules, removed the trace file and reran npm install --ignore-scripts. npm still installed the dependency, audited the package set and exited cleanly, but no trace file was created. In this sandbox, the flag suppressed all lifecycle hooks for the install path I exercised.
That is the mitigation worth knowing first. --ignore-scripts does not make every package safe, and it can break packages that rely on postinstall compilation or setup. But it does change the default from “execute hooks unless you say otherwise” to “skip hooks unless you intentionally opt in.” For untrusted code, that is the safer default.
Why this matters for coding agents and CI
A coding agent that can run npm install has been given authority over package hooks, even if the agent itself never edits package.json. If the install happens in a workspace with cloud credentials, deployment tokens, SSH keys or write access to other local paths, a hook can touch more than dependencies. That is why package installation belongs in the same review as shell access, not in the same mental bucket as “fetching dependencies.”
CI systems face the same issue. A build job that runs npm install on an untrusted branch may execute author-controlled code before tests, linting or packaging begin. If the job also exposes deployment secrets or artifact credentials, a postinstall hook can become a secret-exposure event. The practical question is not whether the package manager is trustworthy in the abstract. It is whether the install environment is isolated enough to survive a hook you did not intend to run.
What the boundary is not
The boundary is not “the package contents are local” or “the lockfile is committed.” A file dependency still executed hooks in my test. The boundary is also not “Node has a permission model” if the hook is free to use a shell or a non-Node helper. Once the hook is running as an ordinary process, its power comes from the operating system account and the surrounding runner, not from npm’s UI.
This is where the Node docs and threat model are useful context. They help explain why package hooks should be treated as code execution, not as harmless metadata. When the tool that launches them says code inherits the execution user’s privileges, the safest interpretation is simple: installation time is execution time.
The production rule is to shrink the install surface
For untrusted repositories, prefer an install path that starts with --ignore-scripts, a clean workspace and no ambient credentials. If you need hooks for a known dependency set, allowlist them deliberately and isolate the job so the hook cannot reach a broader host filesystem or private network segment. Keep the install step separate from the step that publishes artifacts or deploys code.
If the workflow must compile native modules or run package hooks, make the allowance explicit and reproducible. Record the exact package set, the reason a hook is necessary, the runner identity, the allowed egress destinations and the rollback step if a hook misbehaves. A review that cannot answer those questions has not really decided to trust the install path.
The operational decision
Use npm install with lifecycle scripts only when the package provenance is trusted, the runner is disposable, the secrets boundary is empty or tightly scoped, and the hook surface is understood well enough to explain to a security reviewer. Constrain or avoid it when the install runs on untrusted code, inside a shared workspace or near sensitive credentials. Reject it when you cannot prove what the hooks may execute, what they may read and where they may write.
That is a tighter rule than “it built on my machine,” but it is cheaper than a package-hook incident. The evidence from npm’s own docs and my local probe points the same way: installation is an execution boundary, and you should design the environment as if code will run there—because it will.
Copy-ready npm install decision record
Complete this AccessAllGPT template for one repository and one install workflow. Replace assumptions with dated evidence. Any unresolved gate means constrain the install or reject it.
Entries stay in this browser tab and are not submitted to AccessAllGPT. Blank responses are copied as [Unresolved].
Repository, branch or commit, owner, package manager, Node version, OS, who can modify dependencies and why the install is needed.
Registry, tarball, git dependency, local file dependency or workspace; pinning method; review status; whether the package is trusted, reviewed or untrusted.
Whether preinstall, install, postinstall, prepare or other hooks are allowed; whether --ignore-scripts is the default; and why any exception exists.
Where npm runs, what the hook sees as its package root, whether relative paths are safe and whether INIT_CWD matters.
Ambient tokens, SSH keys, cloud credentials, repository write access, package-registry auth and whether any of them are available during install.
Writable paths, read-only mounts, allowed destinations, private-network blocks, temp directories and the cleanup path after install.
Disposable VM, container, microVM, sandbox or shared host; process privileges; parent-child process policy; resource limits and teardown.
Exact install command, log capture, trace-file or artifact proof, hash checks, approval event, and the failure signal that would stop the workflow.
What gets deleted, what gets reverted, who is notified and how the environment returns to a known-safe state after a bad hook.
Allow hooks, ignore scripts, bounded trial or reject; rationale; owner; expiry; re-test trigger.
Primary sources
Browse the publication-wide evidence index →
- Scripts | npm Docsnpm · Reviewed: Description, Life Cycle Operation Order, Exiting and Working Directory for Scripts · Retrieved · Supports: npm says lifecycle scripts run after package changes are applied, lists preinstall, install and postinstall in order, and documents that scripts are executed through /bin/sh on POSIX systems or cmd.exe on Windows.
- Permissions — Node.js documentationNode.js project · Reviewed: Permissions overview, child-process and filesystem controls, known limitations and security notes · Retrieved · Supports: Node’s permission model narrows selected runtime capabilities but does not claim to sandbox malicious code. That makes shell execution and package hooks a separate authority question, not a Node-only flag choice.
- Security policy and threat modelNode.js project · Reviewed: The Node.js threat model section and its statement about trusted versus untrusted code · Retrieved · Supports: The Node project’s threat model says the runtime trusts the code it is asked to run and that dependencies inherit the execution user’s privileges. That is the right backdrop for treating npm hooks as code execution, not as inert metadata.
- Artificial Intelligence Risk Management Framework: Generative Artificial Intelligence Profile (NIST AI 600-1)National Institute of Standards and Technology · Reviewed: Publication abstract, scope, citation and report metadata · Retrieved · Supports: NIST frames AI trustworthiness as a design, development, use and evaluation problem. That general control framing supports least-privilege package installation, evidence logging and explicit rollback for agentic systems.
Limitations
This investigation is a bounded local reproduction on Node v24.10.0 and npm 11.6.0, not a supply-chain incident report, malware analysis, registry study or cross-platform survey. The probe used only local file dependencies and a trace file, so it proves that npm install executed the hooks in this environment; it does not prove the behavior of every npm version, registry package, package manager or operating system. The --ignore-scripts result shows one useful mitigation, not a complete defense against malicious code later invoked by build tools or the application itself. Node, npm and NIST documentation may evolve, and independent validation on the exact runner remains the right next step for any sensitive deployment.
Disclosures
AccessAllGPT did not receive access, payment, private data or review from npm, Node.js, the OpenJS Foundation, NIST or any package maintainer. The sandbox used synthetic local packages, a local trace file and no external secrets. No vendor supplied data, paid placement or endorsement was involved. AccessAllGPT Research is operated by NeuralArc, is independent, and is not affiliated with OpenAI. Publication-wide relationships are listed on the disclosures page.
Further AccessAllGPT guidance
- Node’s Permission Model Is a Seat Belt, Not an AI Code Sandbox
- Human-in-the-Loop AI: Put Approval at the Consequence Boundary
- AI Agents vs Workflows: Choose the Right Automation Architecture
- MCP Token Passthrough Needs an Audience Boundary
- From Paper Result to Production Decision
- AccessAllGPT Research methodology
Continue the research
Get evidence-led updates for teams making production AI decisions.