vx lock
vx lock evaluates every vx.config.* and writes the resolved configs
to .vx/projects.lock.json. Runs read the lock, never the configs:
after you edit one, run vx lock and commit the lock.
vx lock # Evaluate all vx.config.* now; write .vx/projects.lock.json.vx lock --check # Audit: full re-evaluation vs the lock. Exit 1 on drift.vx lock --check --format json # The audit as {upToDate, audited, notAudited, problems}.Example
Section titled “Example”$ vx lock✓ vx lock 2 project configs → .vx/projects.lock.json$ vx lock --check✓ vx lock --check up to date (2 projects)After an edit the lock has not seen:
$ vx lock --check✗ vx lock --check packages/lib/vx.config.ts evaluates differently from the lock here — its config, a file it imports, or the environment it reads changed; run 'vx lock'When to run it
Section titled “When to run it”Run vx lock and commit .vx/projects.lock.json after
editing any vx.config.* or .vx/workspace.*, after updating
dependencies or plugins, and after changing a file a config imports.
Runs read their configs from the lock, so until then they keep the old
ones. vx lock --check as CI’s first step fails a stale lock.
The full workflow: the site’s CI guide and examples/ci.
Options
Section titled “Options”| Option | Meaning |
|---|---|
--check |
re-evaluate every config and compare with the lock; writes nothing |
--format json |
with --check, the audit as one document |
What it writes
Section titled “What it writes”vx lock finds every project and freezes each one’s resolved config into
.vx/projects.lock.json at the workspace root. A project is a directory
holding a vx.config.*, in any language: vx lock lists them with git
(git ls-files, tracked and untracked, git’s ignore rules applied; a
nested repository’s own configs too), or walks the tree outside git,
and leaves out any under a nested .vx/workspace.*. Configs are
programs; vx lock evaluates them in the current environment and
stores the post-evaluation objects, by directory (version 4; a lock an
earlier vx wrote is refused, “re-run vx lock”). A project is named by
its config’s name, else by a plugin’s discover stage (vx-lockfile
gives package.json’s name), else by its root-relative path (// for
the root); two projects of one name are refused naming both
directories. The plugin discover stage runs here as in a run, and may
add directories (X-143). Before writing, it
builds the graph of every task the workspace declares, graph stage
included, and checks the graph rules (run.rules), which no run
checks, dependencies (which projects a project may depend on,
VX_E_DEPENDENCY_RULE) among them: a broken one prints ✗ vx lock lock not written
with the rule’s refusal under it and writes nothing.
.vx/projects.lock.json is minified: it is the one file vx reads, and it loads
faster that way (10,000 projects: 15.6 ms against 22.5 ms pretty). For
review, vx lock also writes each project’s resolved config (plugins
applied), pretty, to vx.lock.json beside its vx.config; vx never reads
it, it is no task’s input, and lock.reviewFiles: false
(VX_LOCK_REVIEW_FILES) turns it off. vx lock also writes
.vx/.gitattributes (projects.lock.json linguist-generated=true) when it is
missing, so GitHub collapses the machine lock in a diff; vx init does
too, and an existing one is never edited.
A project with no vx.config.* — its tasks from a plugin’s project
stage (turbo() from @vzn/vx-migrate), or none at all — has nothing
to freeze: the lock records nothing for it, --check does not audit
it, and a run still loads its tasks live. Both verbs name such
projects that have tasks (0 project configs → .vx/projects.lock.json (no vx.config, so not locked: app, lib), five names then and N more),
so an empty lock on a plugin-only workspace never reads like an audit.
One with no task (core’s default build aside) is not named.
The lock is committed, so vx lock refuses to write one that holds a
secret: a config that evaluated to the value of a secret-named variable
(or one a task lists in exec.env.secret), say
`--token ${process.env.API_TOKEN}`. It names each place
(a: tasks.deploy.exec.command holds $API_TOKEN) and writes nothing.
Masking it instead would freeze a *** that every run runs. Let the
shell expand it: $API_TOKEN in the command, the name in
exec.env.passThrough (L-42).
How runs read it
Section titled “How runs read it”Every run consumes it: configs come from the
lock with no evaluation, no staleness check and no schema validation
of its entries, which vx lock validated when it wrote them (frozen-env
semantics: env reads in a config keep their lock-time values; a
locked directory whose config file is gone (X-144) or that this user
may not search (EACCES, named as such), or a missing lock, is a hard error; a config added since the lock is no project until
vx lock, and naming its task, c#build, says so: “c#build: c/vx.config.ts
is not in the lock yet — run vx lock”). An entry edited by
hand into a shape vx lock could not write fails the run with
.vx/projects.lock.json is invalid, run \vx lock``, checked only once
something has crashed, so a normal run pays nothing for it.
--check
Section titled “--check”--check is the audit: it re-evaluates every config in the current
environment and Bun.deepEquals-compares it with the frozen object. A
project that differs gets one message, whether its config, a file it
imports or the environment it reads changed. A lock an earlier vx wrote
fails the shape check and asks for vx lock. It also checks the graph rules over the configs as they
evaluate now, and that each vx.lock.json is what its project resolves to
here, and that none is left where a vx.config was deleted, the
directory still a project or not (delete that one yourself: vx never
deletes a committed file; vx lock names it too, as a warning), unless lock.reviewFiles is off. The CI recipe is vx lock --check && vx run ….
vx lock and --check also warn of a config file another one beside
it hides (vx.config.ts and vx.config.mjs in one directory, or two
.vx/workspace.*): precedence reads the first, and an edit to the
other changes nothing. The warning does not fail --check.
--format json prints the audit as one document
(schemas/lock.json, exit code unchanged; a missing lock is one of
its problems); it answers --check only.
Full design: docs/design/config-lock-2026-06.md. A lock written by a
1.x vx stays valid for every later 1.x (tests/contract-lockfile.test.ts).
Exit codes
Section titled “Exit codes”| Code | When |
|---|---|
0 |
lock written / lock is up to date |
1 |
parse error, workspace-discovery error, missing lock (--check without one), any drift (every mismatched project is listed on stderr), or a secret value the lock would hold |
Runs read the lock
Section titled “Runs read the lock”Every run, vx watch cycle, --affected selection and the picker takes
its projects and their configs only from the committed
.vx/projects.lock.json (owner, 2026-10-14). A run never walks for
configs, never evaluates one, never checks the graph rules
(run.rules) and never writes the lock: only vx lock does. With no
lock, a run fails with no lock: run \vx lock`. A run trusts the lock: it performs NO staleness check, so after adding or editing a config, run vx lock. In a pipeline, vx lock --check` first re-evaluates every
config and checks the rules.