vlt build
Usage:
$ vlt build [<query>] [options]The vlt build command represents a fundamental shift in how
JavaScript package managers handle post-installation tasks. By
separating the build phase from installation, vlt gives you
unprecedented control over which packages run lifecycle scripts in
your project.
Breaking the Traditional Model
Traditional package managers conflate installation with building –
when you run npm install, packages are downloaded, extracted, and
their lifecycle scripts run automatically. This monolithic approach
has several drawbacks:
- Security risks: Malicious packages can execute arbitrary code during installation
- Limited control: You can't selectively enable or disable scripts for specific packages
With vlt, we've reimagined this process as two distinct phases:
vlt install– Downloads and extracts packages intonode_modulesvlt build– Runs lifecycle scripts and performs additional binary linking when needed
This separation leverages the full power of our Dependency Selector Syntax (DSS) query language, allowing you to precisely control which packages are allowed to execute scripts.
Basic Usage
The simplest workflow mirrors traditional package managers but with explicit steps:
# Step 1: Install packages (no scripts run)
$ vlt install
# Step 2: Build the packages allow-scripts selects
$ vlt buildThe build process is idempotent – it only performs work that's
actually needed based on the current state of your dependency graph.
If a package has already been built, vlt build skips it
automatically.
With nothing stored, a bare vlt build in a terminal opens an editor
prefilled with the default query; with no TTY (e.g.
in CI) it builds the default without saving it. See
Allow scripts.
Allow scripts
vlt build [<query>] sets, saves and applies allow-scripts, the
DSS query that picks which packages run lifecycle
scripts. It builds what the query selects, then saves the query when
it changed: to the project vlt.json, or to the user one with
--config=user. A failed build saves nothing. A query in the other
file is never deleted: a project value shadows the user one.
A query is given as the positional argument, with
--allow-scripts=<query> or with VLT_ALLOW_SCRIPTS. The positional
wins.
| Query given | Stored | -y | TTY | Result |
|---|---|---|---|---|
| yes | no | any | any | build it, save it |
| yes | yes | no | yes | Replace it? [y/N]: y opens the editor with the query, N changes nothing |
| yes | yes | yes | any | build it, save it |
| yes | yes | no | no | error: pass -y to replace it |
| no | yes | no | yes | Edit it? [y/N]: y opens the editor with the stored query, N builds it |
| no | yes | yes | any | build the stored query |
| no | yes | no | no | build the stored query |
| no | no | no | yes | the editor, prefilled with the default query |
| no | no | yes | any | build the default, save it |
| no | no | no | no | build the default, not saved |
A query equal to the stored one builds without asking or writing.
$ vlt build "#esbuild"
🔨 Built 1 package successfully.
Saved allow-scripts="#esbuild" to vlt.json
$ vlt install sharp
$ vlt build
allow-scripts is set for this project (vlt.json): "#esbuild". Edit it? [y/N] y
⚠️ The new allow-scripts replaces the current one. Make sure it still
selects every package you trust: packages it no longer selects won't
run their scripts on install.
Current allow-scripts: #esbuild
New allow-scripts: #esbuild, #sharp
🔨 Built 1 package successfully.
Saved allow-scripts="#esbuild, #sharp" to vlt.jsonIn the editor, the query is prefilled: edit it and press Enter. An
empty answer cancels with No changes made., and an invalid query
prints the error and asks again. With nothing stored, it first warns
that the answer is saved and installs run the scripts it selects. -y
skips the prompts.
There is no unbuild: narrowing allow-scripts only changes what runs
from then on. Packages already built stay built.
vlt install, vlt ci and vlt update run the scripts of the
packages allow-scripts selects, so a fresh clone needs no separate
vlt build. Trust packages one at a time:
$ vlt install esbuild
$ vlt build "#esbuild" # saved to vlt.json
$ vlt install sharp
$ vlt build
# Edit it? y
# New allow-scripts: #esbuild, #sharp
# on another machine, install runs both scripts
$ vlt ciDefault query
With no query given and nothing stored, vlt build uses
:scripts:not(:built):not(:malware): packages not built yet that have
lifecycle scripts (or a root binding.gyp) and no malware alerts. It
prefills the editor; -y builds and saves it.
npm registry packages with no security data, because the request timed out or Socket has not scanned them yet, are skipped and reported as not built. See Understanding Security Data.
Examples
Excluding malware and any package not from a specific scope
In this example we're only allowing scripts from packages that do not
have a malware alert associated to it and packages from a specific,
well known scope name.
# Install without running any scripts
$ vlt install
# Look up if there are packages with malware alerts in my current install
$ vlt query ":malware"
# Look up what packages have build scripts and are not from a known scope
$ vlt query ":scripts:not([name^=@myscope])"
# Build (and save) only packages from my known scope that are NOT flagged as malware
$ vlt build "*[name^=@myscope]:not(:malware)"Exact dependency names
Maybe you only trust certain critical build tools to run scripts:
# Install packages
$ vlt install
# Only build the specific tools you trust (saved as allow-scripts)
$ vlt build "#esbuild, #typescript"Options
--target
Builds a DSS query once: no prompt, never saved. It wins over the
query argument, --allow-scripts and the stored allow-scripts.
command.build.target in vlt.json acts the same.
# Build only direct dependencies
$ vlt build --target=":root > *"
# Build only packages with native bindings
$ vlt build --target=":has-binary"--allow-scripts=<query>
Same as the query argument.
-y, --yes
Skip the prompts: replace or keep the stored allow-scripts without
asking.
--config=<user | project>
The vlt.json to save allow-scripts to. Default: project.
--global, -g
Build packages of the
global project, e.g.
vlt build -g. It then re-links their bins and prints where.
Build State Persistence
The build state is tracked in vlt's node_modules metadata. Each
package node stores its build state (needed, built, or failed).
It's possible to retrieve the already built files using the :built
query selector:
# Query all built packages
$ vlt query ":built"
# Return all packages that have scripts that needs to be built
$ vlt query ":scripts:not(:built)"Best Practices
- Start with
vlt install– Always install first, then decide what to build - Use queries for precision – Target specific packages or patterns rather than building everything
- Persist your choices –
vlt build <query>saves it asallow-scripts, so installs run the same scripts everywhere - Review regularly – Periodically audit your
allow-scriptsto ensure it matches your security requirements, continuing to maintain and evolve it as you add and update packages
Migration from Traditional Package Managers
If you're coming from npm, running install followed by build is the functional equivalent of the traditional package manager behavior:
# Traditional approach
$ vlt install
$ vlt buildThe difference being that, as mentioned previously, vlt build by
default protects you from running scripts of dependencies with known
malware. In a terminal, it first asks for the query to save (see
Allow scripts).
If you are really trying to emulate the legacy npm behavior of running
all lifecycle scripts, you can use the --allow-scripts="*" options
to opt-in to all scripts and have the vlt client work as a
traditional package manager would:
# Bad: Legacy npm-style installation (not recommended)
$ vlt install --allow-scripts="*"It is saved to vlt.json, so every later install runs all scripts.