Skip to content
vxvx
GitHubBlueskydev.toRSS

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}.
$ 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'

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.

Option Meaning
--check re-evaluate every config and compare with the lock; writes nothing
--format json with --check, the audit as one document

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).

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 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).

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

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.

Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.