website/blog/2026-08-05_proto-v0.60.mdx
Our lockfile implementation gets a major overhaul in this release, alongside concurrency-safe installs, simpler plugin configuration, and a friendlier path API for plugin authors.
proto has been steadily marching towards an official v1 release, and we're almost there! As part of this journey, we're making some breaking changes now — like the configuration and WASM API changes in this release — so that plugins, and their authors, have ample time to migrate before v1 lands.
<!--truncate-->If you're using a community WASM plugin, they will most likely need to update to the latest APIs and release a v0.60 compatible version. If you're a plugin author, please read the breaking changes below for the full migration list.
When we introduced lockfiles in v0.51, enabling the
settings.unstable-lockfile setting would lock that
directory and all of its subdirectories, similar to how package manager workspaces function.
Versions defined in nested .prototools configs were absorbed into the top-most .protolock file,
and defining another lockfile deeper in the tree was a hard error.
This model sounded nice on paper, but in practice it caused more friction than it solved —
especially in monorepos, where nested projects define their own .prototools, but a lockfile at
the root would claim them all. In this release, we've reworked the model entirely: a lockfile is
now scoped to the config in which it was enabled, and only that config.
[settings]
unstable-lockfile = true
In practice, this means the following:
.protolock file only tracks tools with versions defined in its sibling .prototools config.
Tools installed ad-hoc (without a configured version) are tracked by the closest config.Lockfiles are still unstable, so if you run into any issues, please report them!
The scoping rework is only half of the lockfile story. Until now, lockfiles were primarily the
concern of proto install — created and updated during installs,
but invisible everywhere else. With this release, lockfiles are now deeply integrated across the
command line.
proto outdated and proto status
now display a "Locked" column (when applicable), showing the version locked for each configured
spec. Both commands also include a new locked_version field in their --json output.proto outdated --update now updates matching lockfile records when writing new versions to
their respective configs. Since these versions haven't been installed yet, checksums are removed
from migrated records, and will be re-populated on the next install.proto pin (and proto install --pin) now migrates matching
lockfile records to the newly pinned spec, while proto unpin
removes them entirely.proto versions now labels versions that have a lockfile record
with "locked", and includes a locked field in its --json output.proto debug config now renders .protolock files
alongside their sibling configs, and includes them in a locks field in its --json output.proto clean no longer removes records when cleaning stale
versions, as the version may still be required by a config, or in use on other machines.And to round it out, proto diagnose now audits the health of
your lockfiles, reporting warnings for records that are missing a resolved version, duplicate
records for the same spec, and stale records that no longer match any configured version.
proto is increasingly being executed concurrently — task runners fan out processes that hit shims simultaneously, CI pipelines install tools in parallel jobs, and AI agents run many commands at once. If multiple of these processes tried to install the same tool and version at the same time, they would all download and unpack into the same directory, clobbering each other and sometimes leaving a corrupt install behind.
In this release, installs of the same tool and version are now serialized through a file lock. The first process acquires the lock and performs the actual install, while the remaining processes wait for it to finish, detect that the version was installed concurrently, and reuse the result. This works across any number of processes, no matter how they were spawned.
Third-party plugins have always been configured through the [plugins.tools] and
[plugins.backends] tables, which keeps plugin locators separate from the settings of the tool
itself. This separation works, but it always felt a bit disconnected.
In this release, we've added new plugin fields to the [tools.*]
and [backends.*] tables, allowing the locator and its settings to
be defined side-by-side. Instead of this:
[plugins.tools]
example = "github://your-org/example"
[tools.example]
custom-setting = true
You can now write this:
[tools.example]
plugin = "github://your-org/example"
custom-setting = true
Both styles are supported, so use whichever layout you prefer.
Installing Ruby with proto has always meant building it from source — a slow process that requires
a compiler toolchain and a handful of system dependencies. Starting with this release, the Ruby
plugin will install pre-built binaries from the jdx/ruby project
when one is available for your platform, and only fall back to building from source when one
isn't. Installs that previously took minutes now finish in seconds.
WASM plugins run in a sandboxed environment, where directories on the host file system (real
paths) are mounted into the guest under fixed virtual paths. Until now, our VirtualPath type was
modeled as an enum that tried to capture both worlds at once — it worked, but it was clunky to use,
and didn't compose with the wider Rust ecosystem.
In this release, we've rebuilt this layer from the ground up:
VirtualPath is now a simple newtype wrapper around PathBuf, giving it access to all PathBuf
methods, something that wasn't possible before.RealPath type — also a newtype wrapper around PathBuf — represents real paths on the
host file system, making it explicit in function signatures which side of the sandbox a path
belongs to.convert_to_virtual_path and convert_to_real_path functions (and the VirtualPathExt
and RealPathExt extension traits) perform the conversion directly in the guest, using the
host-to-guest path mappings.If you maintain a WASM plugin, refer to the breaking changes below for the full migration list.
// Before
use extism_pdk::*;
use warpgate_pdk::{VirtualPath, into_virtual_path, virtual_path};
#[host_fn]
extern "ExtismHost" {
fn from_virtual_path(path: String) -> String;
fn to_virtual_path(path: String) -> Json<VirtualPath>;
}
let path = into_virtual_path(value); // with func
let path = virtual_path!(buf, value); // or with macro
// After
use warpgate_pdk::{VirtualPath, VirtualPathExt};
let path = VirtualPath::create(value)?;
The settings.builtin-plugins setting has been split into two new settings, builtin-tools and
builtin-backends, allowing built-in tools and built-in backends to be toggled independently. Both
accept the same values as before: true to enable all (the default), false to disable all, or a
list of names to enable a select few.
[settings]
builtin-tools = ["node", "bun"]
builtin-backends = false
The builtin-plugins setting is now deprecated, but will remain as an alias for builtin-tools
for backwards compatibility.
As part of the path rework above, the following breaking changes were made to the WASM API:
from_virtual_path and to_virtual_path host functions, the into_real_path and
into_virtual_path wrapper functions, and the real_path! and virtual_path! macros. Use the
new conversion utilities instead.PluginUnresolvedContext.tool_dir and version fields.LocateExecutablesOutput.exes_dir field, use exes_dirs instead.CommandInstruction.cwd to be a VirtualPath.ExecCommandInput.paths to be PathBufs.View the official release for a full list of changes.