
One cache for every checkout
The cache moves to ~/.vx and is shared by every checkout of a repo, --affected follows task edges, and npx @vzn/vx-migrate does the whole adoption.
vx prune cuts a workspace and its lockfile for a Docker build, projects take tags you can filter on, and remote cache writes can be scoped per pull request.

vx 0.0.361 to 0.0.495 all shipped on 2026-10-03. The theme is closing gaps people hit when they move a Turbo or Nx repo over: a Docker prune, tags, interactive tasks, Turbo’s range spelling, and a remote cache a PR cannot poison. Dev servers, the sandbox and remote execution also tell you more when something goes wrong.
In this release
Declaring pnpm(), bun(), npm() or yarn() from @vzn/vx-lockfile
now adds vx prune, vx’s answer to turbo prune. It copies the named
projects and their workspace dependencies to out/, and cuts each lockfile
to what that subset installs, so it still installs frozen. --docker
splits the copy into json/ (manifests and lockfile, the cacheable install
layer) and full/ (everything).
$ vx prune web --dockervx prune: 2 projects → out (json/ + full/), bun.lock pruned ui (packages/ui) web (packages/web)flowchart LR W["workspace"] -->|"vx prune web --docker"| O["out/"] O --> J["json/ package.json files + lockfile"] O --> F["full/ ui and web sources"]
PR #2750
A project config can carry tags, and --filter tag:<pattern> selects the
projects holding a match, as Nx’s tag: does. vx show and vx mcp list
them, and the Nx adoption carries Nx tags over. Tags are in no cache key.
import { defineProject } from '@vzn/vx/config'
export default defineProject({ tags: ['frontend'], tasks: { build: { exec: { command: 'vite build' } } },})$ vx run build --filter tag:frontend ⏺︎ 7ms success no-cache ui#build ⏺︎ 8ms success no-cache web#buildPR #2749
exec.interactive: true marks a task that reads the terminal: a prompt, a
REPL, a watch mode’s keys. On a TTY it gets vx’s stdin, stdout and stderr,
and nothing else starts while it runs. It always runs on this machine and
is never cached or sandboxed. Off a TTY, on CI, it runs like any task. The
Turbo adoption maps Turbo’s interactive: true to it.
import { defineProject } from '@vzn/vx/config'
export default defineProject({ tasks: { repl: { exec: { command: 'bun repl', interactive: true } } },})PR #2748
cacheScope decides where a run’s remote cache writes land. 'trusted'
reads and writes the shared keys and is the default on CI. 'read-only'
writes nothing and is the default off CI. Any other name, such as
'pr-123', reads its own keys then the trusted ones, and writes only its
own, so a pull request never writes what the default branch reads. Set it
in vx.workspace.ts, with VX_CACHE_SCOPE, or let the github() plugin
(now @vzn/vx-ci) set it on Actions. It is a client-side convention: real
isolation needs a server that scopes writes by token.
VX_CACHE_SCOPE=pr-123 vx run build --allPR #2746. See Cache poisoning.
A task’s stdout is a pipe, so tools printed plain under vx. Tasks now get
FORCE_COLOR=1 unless they already see FORCE_COLOR or NO_COLOR, as
under Nx and Turbo. When vx’s own output is plain, it strips the escapes
from task output, live and replayed. vx help on a terminal now has bold
headings and cyan verbs, flags and examples; piped help stays plain.
NO_COLOR=1 vx run build --allA dependency server that crashed mid-run was named only at the end, while
its dependants failed with no word of why. vx now says so when it happens,
and dependants not yet started skip, named for the server. A server whose
readyWhen never matches no longer waits in silence: after 10 seconds vx
says what it waits for and whether anything bounds the wait.
$ vx run web#e2e┌─ web#e2e > $ sleep 1; echo e2e donevx: web#dev exited with code 3 while the run went one2e done└─ web#e2e ── (1.01s) successPRs #2152, #2338, #2463. Deep dive: Dev servers in the graph.
Turbo’s CI spelling --filter=[origin/main...HEAD] was refused as a range.
vx diffs a base against the working tree, so a range that ends at HEAD
means the same set plus any uncommitted edit. <base>...HEAD and
<base>..HEAD now work in --filter and --affected; a range that ends
anywhere else is still refused.
vx run test --affected=origin/main...HEADA sandboxed task that failed on a read the sandbox hid saw only its tool’s
“File not found”: tsc extending a root tsconfig.base.json was the
common case. A failed task now names the hidden paths that exist on the
host and the grant to add. Separately, the host’s credential stores
(~/.ssh, ~/.aws, ~/.npmrc and the like) are now denied to sandboxed
tasks unless allow.read names one, so a dependency cannot copy a key into
a cached output.
vx: the sandbox hid paths outside the project that exist on this machine,which are not reported as violations: /repo/tsconfig.base.json. If thetask reads one, grant it, e.g. `allow: { read: ['../../tsconfig.base.json'] }`.PRs #2387, #2344, #2451. See the sandboxing guide.
With @vzn/vx-reapi, an action no worker picked up waited without bound
unless the task set exec.timeout, and then it failed as timed out. The
new queueTimeoutMs bounds the wait for a worker. Past it, vx cancels the
remote operation, runs the task on this machine, and says why once. A task
placed exec.remote: 'only' fails with that reason instead.
import { defineWorkspace } from '@vzn/vx/config'import { reapi } from '@vzn/vx-reapi'
export default defineWorkspace({ plugins: [ reapi({ endpoint: 'grpcs://grpc.example.com:443', execute: true, queueTimeoutMs: 60_000, }), ],})PRs #2716, #2723. Deep dive: Remote execution.
nx() runs every Nx executor through nx-exec, a Node boot per task. A
workspace’s own executor that only wraps a shell command can now skip that:
nx({ executors }) takes a function per executor name that returns
{ command, timeout?, env? }, or undefined to leave the target to
nx-exec. Each function’s source text keys the cached mapping, so an edit
re-maps. Also, an Nx output that starts with a wildcard (**/*.d.ts) now
stays cached instead of turning the target uncached.
import { defineWorkspace } from '@vzn/vx/config'import { nx } from '@vzn/vx-migrate'
export default defineWorkspace({ plugins: [ nx({ executors: { '@acme/tools:shell': (t) => ({ command: String(t.options.command) }), }, }), ],})PRs #2733, #2736. Deep dive: From Nx.
SCHEMA_VERSION v29 to v30: an older cache index is dropped on
first open (#2246).nx() migrations write nx-exec lines where they wrote executor
commands (#2632).vx init keeps existing configs and exits 0 instead of refusing with 1
(#2546).reapi() refuses onWarn, tlsCaPem, tlsClientCertPem and
tlsClientKeyPem; use the file options
(#2535).@vzn/vx-reapi’s plugin-api record drops the WireOnly type
(#2541).refuseUnknownOptions takes a PluginOptionKinds record, and a plugin
option of the wrong kind is refused
(#2475).@vzn/vx-reapi stops exporting its wire and Merkle internals
(#2521).@vzn/vx-migrate exports only its four plugins
(#2520).@vzn/vx-lockfile stops exporting its parser namespaces
(#2512).@vzn/vx-otel stops exporting its OTLP builders
(#2508).@vzn/vx-github stops exporting its Checks API helpers
(#2503).See Upgrading.
npm install -D @vzn/vx@latestA standalone binary updates itself with vx upgrade.