- Published on
Introducing Phased Package Installations
- Authors

- Name
- Ruy Adorno
- Bluesky@ruyadorno.com

- Name
- Darcy Clarke
- X (Formerly Twitter)@darcy
Every time you run npm install, you're executing arbitrary code from potentially thousands of packages and package authors. "Install scripts" are run automatically, with full access to your system before you've even had a chance to review what's being installed. Unfortunately, this "download anything and run everything" model has been a security blind spot for years; that ends today.
Today, we're excited to introduce Phased Package Installations, a fundamental reimagining of how package installation works. By creating two distinct phases ("install" & "build"), vlt gives you unprecedented control over which packages can run code in your project.
The Problems with the Legacy Installation Model
Traditional package managers conflate two distinct operations:
- Installation - Resolving, downloading, and extracting packages into
node_modules - Building - Running scripts (
preinstall,postinstalletc. - often referred to as "lifecycle scripts") and linking binaries tonode_modules/.bin
When you run an install command with traditional package managers both steps happen automatically and atomically. You can't download packages without also executing their scripts. This "trust-by-default" model creates a few problems:
- Opaqueness: Scripts are run before you know what they are or how they got there
- Expediency: Compromised dependencies have the opportunity to execute arbitrary code immediately
The Problems with the Current Solutions
- All-or-Nothing: traditional package managers tried to solve this by implementing flags like
--ignore-scriptswhich take an all-or-nothing approach with no nuance - Lack of Context: project's like
pnpm&bunhave gone further by implementing script configurations that allow for finer-grained control with the listing of blessed package names & versions but lack context regarding their associative location within the graph; essentially treating all matching instances of those dependencies the same irregardless of their edges/ancestry or associative metadata
Enter Two-Phased Package Installation
As of v1.0.0-rc.1, the vlt install command now runs vlt build under-the-hood and both now support our Dependency Selector Syntax; install via. an --allow-scripts=<selector> flag and build via. a --target=<selector> flag.
By default vlt install's --allow-scripts selector value is ":not(*)" (ie. allow no dependencies to run their scripts). If this is your preference - as it is for many - then you can likely skip running the vlt build command altogether.
Alternatively, if you want to run scripts for all, safe dependencies, you can explicitly run the new vlt build command after a successful vlt install and it will run the scripts for all dependencies that aren't flagged as malicious (ie. the default --target value is ":scripts:not(:built):not(:malware)").
We believe this change and the corresponding default configurations add a powerful layer of protection and control back to the installation process.
# Phase 1: Download and extract packages with no dependencies running scripts
$ vlt install
# Phase 2: Optionally run lifecycle scripts for all, unflagged dependencies
$ vlt build
This simple change unlocks powerful new capabilities:
- Safe-by-Default: We automatically block all install scripts from running by default
- Observability: Review the resolved dependency graph & installed packages prior to script execution
- Optionality: The Dependency Selector Syntax provides unrivaled granularlity for opting-in/out dependencies
How It Works
Installing Without Scripts
When you run vlt install, packages are downloaded and extracted to node_modules, but no lifecycle scripts execute:
$ vlt install
Your dependencies are now installed, but nothing has run yet. The vlt client will inform you at the end of the install how many packages have lifecycle scripts:
Done in 1474ms
๐ฆ 3 packages have install scripts defined & were not fully built
๐ Run `vlt query :scripts` to list them
๐จ Run `vlt build` to run all required scripts to build installed packages.
As an intermediate step, you can now inspect what was installed, navigate the dependency graph using vlt list or vlt query, check for security issues, and decide if/what you want to build next.
TIP: You can also use our browser-based UI to view more information about your project, easily navigate the dependency graph, security insights, source code & much more. Start the server with:
vlt serve
Querying Your Dependencies
Before building, you might want to see exactly which dependencies have build scripts:
# See all dependencies with lifecycle scripts
$ vlt query ":scripts"
# Check for dependencies with issued malware alerts
$ vlt query ":malware"
# Find dependencies that have lifecycle scripts that are not from a specific org/scope
$ vlt query ":scripts:not([name^=@myorg])"
Building Selectively
Using the Dependency Selector Syntax you can also control exactly which dependencies can run scripts:
# Build only your trusted tools
$ vlt build --target="#esbuild, #typescript, #rollup"
# Build everything from a specific scope
$ vlt build --target="*[name^=@myorg]"
# Build all except known problematic packages
$ vlt build --target=":not(#problematic-package)"
Default Protection
When you run vlt build with no arguments, it uses a sane default value that protects you automatically from packages already known for containing malware:
# Uses default target: :scripts:not(:built):not(:malware)
$ vlt build
The default target:
- Only builds packages that have scripts (
:scripts) - Skips packages already built (
:not(:built)) - Automatically blocks packages with malware alerts (
:not(:malware))
Real-World Examples
Trusting Specific Scopes or Packages
If you only want to run scripts from your organization and well-known build tools:
# Install everything but only run scripts from your organization and well-known build tools
$ vlt install --allow-scripts="*[name^=@mycompany]:not(:malware), #typescript, #webpack"
Trusting Direct Dependencies
If you only want to run scripts for direct dependencies:
# Install everything but only run scripts for direct dependencies
$ vlt install --allow-scripts=":root > *"
Security-First Workflow
Audit your dependencies before building:
# Install packages
$ vlt install
# Check for security issues
$ vlt query ":malware"
# โ Lists any packages with known malware
# Check what needs building using the browser-based UI
$ vlt query ":scripts:not(:built)" --view=gui
# โ Opens default browser showing packages with unbuilt scripts
# Build only safe packages from your scope
$ vlt build --target="*[name^=@myscope]:not(:malware)"
Legacy Compatibility
If you want npm's traditional behavior (not recommended), you can opt-in to allowing all dependencies to run their scripts:
# Run scripts for all dependencies
$ vlt install --allow-scripts="*"
Configuration and Persistence
Target queries can be saved to vlt.json for team-wide consistency:
# Save your target policy
$ vlt config set "command.build.target=*[name^=@myorg]:not(:malware)"
Your vlt.json should looking something like:
{
"workspaces": ["packages/*"],
"command": {
"build": {
"target": "*[name^=@myorg]:not(:malware)"
}
}
}
# Now just run build (you'll notice it uses your saved target configuration)
$ vlt build
The :built and :scripts Pseudo-Selectors
You may have noticed in some of the examples above that we've added two new pseudo-selectors to the Dependency Selector Syntax to make build management easier:
:scripts
Matches packages that have lifecycle scripts (preinstall, install, postinstall &/or prepare):
# Find all packages with scripts
$ vlt query ":scripts"
# Find scripts from external dependencies
$ vlt query ":scripts:not(:workspace)"
:built
Matches packages that have already been built:
# See what's already built
$ vlt query ":built"
# Find unbuild packages with scripts
$ vlt query ":scripts:not(:built)"
Build state is tracked in node_modules metadata, making the process idempotent, vlt build only does work that's actually needed.
If you want to learn more about our DSS query language, read the Query Selectors docs page.
Why This Matters
For Individual Developers
- Peace of mind: Review what's installed before running anything
- Faster debugging: Skip problematic build scripts during development
- Better understanding: See exactly which packages need building
For Teams
- Consistent security policies: Share build targets via
vlt.json - Compliance: Meet security requirements around script execution
For the Ecosystem
- Reduced attack surface: Malicious scripts can't run automatically
- Better security practices: Forces consideration of script execution
- Supply chain hardening: Makes npm supply chain attacks significantly harder
Best Practices
- Always install first: Run
vlt installbeforevlt build - Start restrictive: Begin with narrow targets, expand as needed
- Use scopes: Trust packages from known organizations
- Leverage the
:malwareselector: Always exclude malware in your targets when customizing selectors - Persist your policy: Save target queries to
vlt.json - Audit regularly: Use
vlt queryto review what's installed
Try It Today
Ready to take control of your package scripts? Get started:
# Install vlt via curl
curl -fsSL https://vlt.sh/install | sh
# Install vlt via npm
npm install -g vlt@latest
