Changelog
Notable changes per release. The release workflow refuses to publish a tag whose version has
no section here, and the section becomes the release notes - so write it before bumping
Cargo.toml, then rename Unreleased to the version you are tagging.
Format: Keep a Changelog. Versions follow Semantic Versioning. Releases before 0.159.0 predate this file - see the git history.
[0.169.3] - 2026-10-20
Fixed
rooz execandrooz enterwork again in images without a writable/etc/passwd(distroless, scratch-based). 0.169.2 made a failed container-user creation fatal; it is a warning that names the cause again.
[0.169.2] - 2026-10-19
Fixed
- A clone directory is named after the repository: only a trailing
.gitis dropped (my.github.iowas cloned asmyhub.io), andgit@host:repo.gitno longer lands in a directory with@and:in its name. url.<base>.insteadOfrewrites use the longest matching alias and everyinsteadOfvalue in a section, as git does.rooz execexits non-zero when the engine reports no exit code or a negative one. Before, both looked like success.- A failed
rooz moverestarts the source workspace it had stopped, and no longer leaves the destination’s volumes behind when the copy container is still being removed. Removing a volume waits briefly for a container that is going away. rooz cache rmgiven the same cache twice removes it once instead of failing midway.rooz enter,start,stopandrestartno longer panic on a container missing from the stored config or with an unexpected name list.- Image references with a registry port (
images:5000/alpine:3) or a digest (img@sha256:...) are pulled as written; they were split on every:. - A command run in a container fails when the engine does not report its exit code, instead of counting as a success. Output split mid-character across chunks no longer fails the command.
- A container whose extra network cannot be connected is removed instead of being adopted
half-networked by the next run.
killno longer spins and panics on podman. - Creating the container user matches the uid exactly: uid 100 no longer deletes the
passwdentry of uid 1000. - A config that does not parse is an error for
new,updateandconfig edit; before, a local one was ignored with a warning (creating the workspace with defaults) and a git one panicked.config editwithout a terminal to retry in stops instead of reopening the editor forever. - Non-adjacent duplicate
cachesare dropped. A secret that is part of another one no longer leaves the rest of it readable in the stored runtime config. rooz tmpremoves its workspace when the shell cannot be entered.rooz enterwaits between reconnect attempts and no longer panics on a container missing from the stored config.rooz remotekeeps the leading/of aunix:///...DOCKER_HOST; the forward pointed at a relative path.- Stack detection skips a project file it cannot read instead of failing.
rooz updateresolves and gates the new configuration before removing anything: a refused consent or an invalid value no longer leaves the workspace (or, with--purge, its volumes) gone. A workspace made from flags alone keeps its image, user, caches, privileged flag and--uidacrossupdate; before, it was rebuilt with the defaults.--uidis stored with the workspace, somove,enter --chownandplansee it.- An undefined template variable (
{{ tga }}) is an error naming it instead of an empty string, andvarsare rendered once, in order, so a forward reference is an error too. extra_reposin a remote--config <url>//<path>source (or a config merging an untrustedbaseslayer) is refused unless the source is inROOZ_TRUSTED_CONFIGS: the clone uses your ssh key.- An interrupted
system initthat left an empty ssh-key volume is completed by running it again. rooz remote: one failed Docker API call no longer closes every forwarded port, and a service that accepts the tunnel probe but never answers cannot stall the poll loop.update --purge --dry-runno longer lists the config volume, which a purge keeps.- The terminal is restored when an
entersession ends with an error, a failed container-user creation is reported instead of ignored, and a failedchownis warned about. rooz move --tohints no longer print a stray\.
[0.169.1] - 2026-10-19
Changed
- A finished container’s install summary lists its steps:
✓ran (with its time),○skipped byskip_if; a sidecar whose image is reused shows↺.
Added
rooz update --rebuildbuilds sidecar images again even when their install steps are unchanged, without touching volumes. Before, a broken image was reused until a step changed or--purge.
Fixed
- When one container’s install failed, the others were cut short, and a sidecar’s half-built install container stayed behind under the sidecar’s name: base image, no env, none of the steps’ work. Every container’s install now runs to the end, all failures are reported, and a sidecar install container is always removed.
[0.169.0] - 2026-10-19
Changed
- Breaking: privileged and vpn consent is pinned to the container’s definition:
ROOZ_ALLOW_PRIVILEGED=dkr@<fingerprint>instead of=dkr. A bare name also covered any other config declaring adkr, e.g. an untrustedbaseslayer with its own image, which then ran as host root. The refusal prints the value to set; it changes with the container’s image, command, env, mounts or install steps.=trueand--privileged truework as before. - Breaking: sidecar containers and workspace networks are named with
_:<workspace>_<sidecar>,<workspace>_egress,<workspace>_net_<sidecar>,<workspace>_peer_<a>_<b>. With-, workspaceawith sidecarxand workspacea-xshared a container name, and peer pairs(a, b-c)and(a-b, c)a network. The firstrooz updateafter upgrading moves a workspace to the new names; until then it keeps working under the old ones.ROOZ_META_CONTAINER_NAMEin sidecars changes accordingly. - Breaking: sidecar names are lowercase letters, digits and
-, starting with a letter or digit;workis reserved. - Breaking: data keys that would share a volume (
a.banda-b), a data key namedinline, and keys not starting with a letter or digit are refused, as are two inline mounts of one container whose paths would share a file. - A sidecar’s inline mount files are kept apart from the work container’s: the same path inline in
both is two files. A sidecar’s
onceinline file is written afresh once after upgrading. - Sidecar images build, and their install steps run, alongside the work container’s install instead
of one after another. On a terminal each container gets a live pane with its current step and last
output lines; elsewhere, or with
ROOZ_PROGRESS=plain, output lines are tagged with their container and step. - Install progress says why a step was skipped (
skipped: skip_if succeeded), androoz updateprints a line when it reuses a sidecar’s built image instead of running its steps.
Fixed
- A container or network found under a workspace’s name is reused only if its labels say it belongs to that workspace (and container); anything else is refused instead of adopted or replaced.
- An invalid sidecar name panicked halfway through
rooz new; container creation errors are now reported as errors. - Caches from a
baseslayer not inROOZ_TRUSTED_CONFIGSwere shared without consent when the top-level config was a local file. They now needROOZ_CACHES, like a repository’s. - Secret values were rendered as templates: a
{{in a secret corrupted it, let the result escape masking, or printed the plaintext in a template error. They are now inserted verbatim. - A key in both
varsandsecretspanicked; it is now an error. - Relative directories in
ROOZ_TRUSTED_CONFIGSresolved against the current directory, which inside a cloned repository could trust its own files. They are now ignored with a warning.
[0.168.3] - 2026-10-19
Fixed
rooz updaterebuilt every sidecar withinstallsteps from scratch, so the steps ran again andskip_ifcould never skip them. It now reuses the built image while the base image,uid,command,argsand steps are unchanged.rooz updatedid not pull sidecar base images when the workspace’s image was set in the config, so a floating tag such asalpine:3never moved. It now pulls them unless--no-pullis given.
[0.168.2] - 2026-10-19
Added
once: trueon adatafile (content:,file:orgenerate:): written only when nothing is at its path, then left to the containers. For history files, generated keys, or a file a sidecar fills in and the work container reads.- A VS Code page in the docs:
rooz code, keeping extensions across rebuilds, per-workspace editor setup and remote hosts. The landing page links to it. - A “Data and mounts” section in the configuration docs.
Changed
rooz updatewrites every non-oncedata file from the config, overwriting changes made in the container, and re-runsgenerate:scripts. Mark files that containers ownonce: true.- Moving a workspace keeps edits made in its sidecars’ data files; they used to be rewritten from the
config on every run. Only
rooz updaterewrites files. - Install progress reads
[install:{container}:{step}], with the container in a stable colour. Sidecars show their short name (claude, notrooz-dev-claude). The step line is printed by rooz, so it is uncoloured when output isn’t a terminal.
Fixed
rooz updateignored edited data files in the work container, and failed on a newly added one, leaving the workspace without a container.rooz newwithoutrooz system initrefuses with a hint. It used to make the engine create an empty, unlabelledrooz_ssh-keyvolume, whichrooz system initthen refused to reuse.tests/interactive.rsbuilds on macOS.
[0.168.1] - 2026-10-17
Added
- The docs site shows the release version in the top bar.
Fixed
rooz enterfollows terminal resizes; the session used to keep the size it started with.
[0.168.0] - 2026-10-17
Changed
- Breaking:
portsmappings are<host>:<container>, as in Docker andkubectl port-forward(was<container>:<host>). Swap the numbers in existing configs:"80:8080"that exposed container port 80 on host port 8080 is now"8080:80". - Breaking: remote
--configsources must use SSH. Non-SSH URLs (https://,http://,git://,file://, remote helpers) are refused with a clear message. - Breaking: the sidecar
userkey is removed; it was accepted but never used. Configs that set it fail to parse - useuid.
Added
- Remote
--configacceptsssh://URLs (alsogit+ssh://,ssh+git://), e.g.ssh://git@host:2222/my/configs//cfg.yamlfor a non-standard port.
Fixed
- The examples in
examples/parse again; a unit test keeps them that way. rooz doctor’s network hint counts networks correctly: one per workspace, plus one per sidecar and one per peer relation.- The secrets refusal message showed a
--config git:...syntax that does not exist.
[0.167.0] - 2026-10-17
Added
- Docs site at rooz.dev, built from
docs/with mdBook, including a generated CLI reference and config schema. curl -fsSL https://rooz.dev/install.sh | shinstalls the latest release. Each release carries aninstall.shthat pins its own version andSHA256SUMS, so the binary is checked against data served from rooz.dev, not the GitHub release it comes from. With a logged-ingh, it also checks the signed build provenance.
[0.166.0] - 2026-10-16
Changed
- Breaking: volume names use one scheme: segments joined by
_, workspace first (rooz_<ws>_work,rooz_<ws>_workspace-config,rooz_<ws>_<data>; shared ones stayrooz_sys-config,rooz_cache_<scope>_<path>). The ssh key volume isrooz_ssh-key, wasrooz-ssh-key-vol. Volumes under the old names are not found or migrated: remove them (rooz system pruneremoves everything rooz made), then runrooz system init- a new ssh key is generated and must be registered again.
[0.165.0] - 2026-10-16
Changed
rooz doctorstarts with the endpoint it connects to (the-Hhost’s socket,DOCKER_HOST, or the default socket), and-vlogs it for every command.
Fixed
- An engine that cannot be reached is reported as
Cannot reach the container engine at <where>: <why>, with what to check, instead of the HTTP client’sclient error (Connect). A missing default socket (noDOCKER_HOST, no/var/run/docker.sock) no longer panics.
[0.164.0] - 2026-10-16
Added
rooz new --dry-runandrooz update --dry-run(also with--purge) print what would be removed and created - image pull, volumes, caches, networks, sidecars, the work container and install steps- and change nothing. The configuration is resolved and gated like a real run, so a plan fails with the error a run would. Env values, scripts and secrets are not printed.
rooz exec <ws> -- cmd...runs one command in a workspace container, passes stdout and stderr through and exits with the command’s exit code.--root,--work-dir,--container.rooz list --json;rooz status <ws> [--json](containers, volumes, config origin);rooz logs <ws> [--container] [-f] [--tail N].rooz cache ls|rm|prune: size, age and mounting workspaces of the shared cache volumes.rmandpruneonly touch your own scope and refuse caches a container still mounts.rooz doctorchecks the engine,rooz system initresults (system config, ssh key, age identity) and, on docker, how much of the default network address pool is used.rooz move <ws> <new-name> [--dry-run]renames a workspace on one engine: volumes are copied under the new name, the workspace is rebuilt from its stored config and the old one is removed. A failure removes the partial new workspace and leaves the old one untouched.rooz move <ws> --to <host> [new-name] [--remove-source] [--dry-run]moves a workspace to another engine fromrooz remote: volumes stream across with owners, modes and symlinks intact, the workspace is rebuilt there from its config file, and the source is left stopped unless--remove-source. The target is checked before anything is copied (name free,system initdone, config gates) and a failure removes the partial workspace there.rooz config schemaprints a JSON Schema of the config file for YAML language servers.rooz config show <ws> --resolvedshows the merged settings (defaults, bases, config) with the layer each came from.rooz config template(it was a stub) prints a starter config; with--detect [DIR]it recognizes Rust, .NET, Node, Go and Python projects and fills in the image and cache paths.- Global
-v/--verboseturns on debug logging (RUST_LOGstill works).newandupdateprint one colored[phase]line per step: image, volumes, clone, container, install. rooz update --allow-secrets, as onnew; before, onlyROOZ_ALLOW_SECRETScould consent.
Changed
- Breaking:
rooz listshows STATUS, IMAGE and AGE instead of RUNNING, and includes workspaces whose containers are stopped. - A failed image pull, volume operation or exec returns an error saying what failed and what to check, where it used to panic. A failed pull no longer leaves a stored config behind.
rooz updatepulls the image before removing the old containers, so a failed pull leaves the workspace as it was (an image that only comes frombasesis still pulled afterwards).rooz remotelog lines color each host name consistently.
[0.163.0] - 2026-10-16
Added
installsteps can be objects:run,skip_ifandroot.skip_ifis a shell check that runs first; exit 0 skips the step, so a tool that is already installed (typically from a cache volume) is not installed again.runandskip_ifare templated.
Changed
- Breaking: install steps run as the workspace user, not root (sidecars: as their
uid). The user and its volumes are set up before the first step. Steps that need root, such asapt-getorapk add, must say so withroot: true. A configuration the operator did not author (unlisted--config git:...orbaseslayer) is refused if it asks forroot: true.
[0.162.0] - 2026-10-15
Added
file: <path>on a data value (data, inlinemounts, sidecarmounts) takes its content from a file next to the config that declares it, for scripts and dotfiles that are clumsy inline. Works for local configs, bases and configs from git (read from the checkout). The path stays below the declaring config’s directory; symlinks, files over 64 KiB and non-UTF-8 files are refused. The content is not templated, and the stored config keeps the reference.
[0.161.0] - 2026-10-15
Added
rooz remoteserves many hosts at once. Hosts are defined in~/.config/rooz/remotes.yaml(rooz remote add|rm|ls);rooz remote [names...]forwards each host’s engine socket to$XDG_RUNTIME_DIR/rooz/<host>.sock(~/.rooz/<host>.sockwithout it) and its published ports to localhost. A host that is down retries with backoff without affecting the others.- Global
-H/--host <name>(envROOZ_HOST) runs any command against a configured host.rooz list --all-hostslists every connected host.rooz codeattaches VsCode through the selected host. rooz system pruneandrooz remove --allprint the engine they target.
Changed
- Breaking:
rooz remoteno longer readsROOZ_REMOTE_SSH_URL/DOCKER_HOST, nor forwards to$DOCKER_HOST; define the host withrooz remote addinstead. - A host may set
socketto skip the$DOCKER_HOSTlookup over ssh; an unset one is now an error instead of a panic. - A local port wanted by two hosts is forwarded for the first one and skipped (with a single warning) for the other.
- Errors are printed readably: the first sentence as a headline, the rest wrapped (to
$COLUMNS, at most 100) below it - instead of one long quotedError: "..."line. This covers theROOZ_ALLOW_PRIVILEGED,ROOZ_ALLOW_SECRETS,ROOZ_ALLOW_VPNandROOZ_TRUSTED_CONFIGSrefusals, among others. The exit code is still 1. - Tunnels are dropped and reopened after an SSH reconnect, and the forwarded socket is removed on exit.
Fixed
git pull/git fetchover ssh inside a workspace no longer printhostfile_replace_entries: mkstemp: Read-only file system. The ssh key volume is mounted read-only in workspaces, and ssh kept trying to rewriteknown_hoststhere. The volume now carries an ssh client config withUpdateHostKeys no, written byrooz system init(also on an already initialized engine, so re-running it fixes existing setups) and on clone.
[0.160.0] - 2026-10-12
Added
vpn: trueon a sidecar grantsNET_ADMIN,/dev/net/tunandnet.ipv4.conf.all.src_valid_mark=1- enough forwg-quick(kernel orwireguard-go) and other in-container VPNs, withoutprivileged. Requires operator consent naming the sidecars,ROOZ_ALLOW_VPN=<names>(or=true), separate fromROOZ_ALLOW_PRIVILEGED; avpnsidecar must run as uid 0.ROOZ_TRUSTED_CONFIGSlists configuration sources that are the operator’s own: repository URLs (matched exactly as passed to--config, never overhttp:///git://) and local directories (every file, bases included, must resolve inside one). A trusted source may expand secrets (still subject toROOZ_ALLOW_SECRETS) and declare caches - including throughbases- which previously only a local--configfile without bases could.
Removed
- Breaking: rooz no longer reads a
.rooz.yamlfrom the root of a repository it clones (rooz new -g <url>). Applying a repository’s configuration is now always explicit:rooz new ws -g <url> --config <url>//.rooz.yaml. Same trust rules as before - it is a--config git:...source, authored by the repository. Existing workspaces created from an in-repo config keep updating:rooz updatere-fetches it as a--config git:...source.
Fixed
- A
rooz newrefused for a missingROOZ_ALLOW_PRIVILEGED(orROOZ_ALLOW_VPN) consent no longer leaves the workspace’s config volume behind, so re-running the same command with consent works instead of failing with “Workspace already exists”. A refusedrooz updatekeeps the previously stored config. rooz updatekeeps the workspace’s repository (-g) when the config came from anywhere other than the repository’s own.rooz.yaml. Previously it forgot it: the work container started in/workinstead of the clone, and--purgeremoved the clone without re-cloning.
[0.159.0] - 2026-09-24
Security
- Sidecar runtime images are reused only by the image id rooz recorded when it committed them
(kept on the sidecar container as
dev.rooz.runtime-image), never by the predictablelocalhost/rooz/<workspace>/<sidecar>name. An image tagged under that name by somebody else is ignored and the install stage runs again. - An age identity found in the engine’s
rooz_sys-configvolume is no longer adopted as this machine’s identity. It is taken out of the volume and kept at~/.config/rooz/age.engine.bakfor the operator to recognise and install withrooz system init --force --age-identity "$(cat ...)", because anyone with access to that engine can write a key there. - Configuration that the operator did not author can no longer abort rooz mid-creation: port
mappings,
envkeys,mountsnaming an undefineddata:entry and non-YAMLbasespaths are refused with a message instead of panicking, before any volume or container is created. envkeys must be identifier-shaped, and theROOZ_META_*namespace rooz injects itself is reserved - a config can no longer forge the metadata a workspace reads back.- Engine-supplied labels are treated as untrusted input:
rooz listskips volumes that carry rooz’s role label without a workspace label (previously a panic for every operator on that engine), and a duplicated label set reports an error instead of aborting. - Every GitHub Action is pinned to a commit SHA, with Dependabot raising upgrades as reviewable pull requests.
Changed
- Breaking: expanding
secretsnow needs explicit consent -ROOZ_ALLOW_SECRETS=<names>(comma-separated, safe to leave in a shell profile and also coversrooz update),ROOZ_ALLOW_SECRETS=true, or--allow-secrets true. A local--configpath is not proof that the operator wrote the file: repositories ship config files and README instructions to pass them. Workspaces withoutsecrets:are unaffected. FileFormat::from_path,RoozCfg::parse_ports,VolumeApi::create_volume_specsandVolumeApi::mounts_with_sourcesreturnResultinstead of panicking.
Documentation
- README states the trust model: the engine is assumed to be the operator’s own, and an engine shared with people who do not trust each other is out of scope.