Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

rooz

Rooz

Rooz is a CLI for working in containers, aimed at developers and DevOps engineers. Features: git repositories, SSH key generation, git-safe secrets, shared caches, sidecar containers, declarative configuration. It shares as little as possible with the host, so it can also run workspaces on remote Docker/Podman hosts.

Requirements

One of:

  • Docker >= 29.5.0
  • Podman >= 6
  • Rancher Desktop - untested

The engine must be yours: a rootless daemon on your machine, or a remote one only you use. Rooz protects your host, other workspaces and secrets from the repositories you open, but not from other users of the same engine - anyone with engine API access can read every volume, run containers on your networks and spoof image/volume names. Don’t share an engine with untrusted users.

Install

curl -fsSL https://rooz.dev/install.sh | sh

Installs the latest release to ~/.local/bin/rooz (override with ROOZ_INSTALL_DIR). Supports Linux and macOS, x86_64 and arm64. The script pins one version and embeds its checksums, so the binary is verified against data from rooz.dev, not from the GitHub release. If the GitHub CLI is installed and logged in, it also verifies the signed build provenance.

Manually

Download the binary from the latest release:

PlatformAsset
Linux x86_64rooz-x86_64-unknown-linux-gnu
Linux arm64rooz-aarch64-unknown-linux-gnu
macOS (Apple silicon)rooz-aarch64-apple-darwin
macOS (Intel)rooz-x86_64-apple-darwin
curl -fsSLO https://github.com/queil/rooz/releases/latest/download/rooz-x86_64-unknown-linux-gnu
curl -fsSLO https://github.com/queil/rooz/releases/latest/download/SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS
install -m 755 rooz-x86_64-unknown-linux-gnu ~/.local/bin/rooz

Every release has SHA256SUMS and signed build provenance; verify the latter with gh attestation verify ./rooz-x86_64-unknown-linux-gnu --repo queil/rooz.

Next: Getting started.

Getting started

Initialize

⚠️ Using a remote Docker host? Configure it first.

rooz system init

This creates:

  • An SSH key pair (ed25519) for SSH auth (e.g. github.com). Stored in a volume and mounted read-only under ~/.ssh in every work container. Workspaces can read it but not modify or delete it.

  • An age identity for encrypting secrets in config. It is stored on your machine at ~/.config/rooz/age.key ($XDG_CONFIG_HOME/rooz/age.key if set, or ROOZ_AGE_KEY_FILE), mode 0600. It is never written to the engine (any engine API user can read all volumes) and is used only by the CLI.

    ℹ️ Back up this file. If it is lost, existing encrypted vars cannot be decrypted and must be re-encrypted. To use an existing identity, pass --age-identity. To use a second machine against the same engine, copy the identity file there.

--force regenerates the age identity, deleting the existing one. It does not touch the SSH key pair, which existing workspaces mount. To replace the SSH key (e.g. if leaked):

rooz system init --rotate-ssh-key

This generates a new pair in place and prints the public key. Register it wherever you used the old one and de-register the old one. Running workspaces pick it up on their next start.

⚠️ The SSH key is the engine’s only git identity, and every workspace can read it. The workspace image and user come from the repository’s config, so any repository you open can take the key. Rotation limits damage after the fact. Do not open repositories you would not trust with this key.

Configure

ℹ️ See Configuration.

Rooz works best with your own custom image(s). Image and user can be passed as CLI flags, but it is easier to set them (and the shell) in ~/.bashrc (or equivalent) on the host and override via CLI when needed.

Example configuration

export ROOZ_USER=queil
export ROOZ_IMAGE=ghcr.io/queil/image:latest
export ROOZ_SHELL=bash
export ROOZ_CACHES='~/.local/share/containers/storage/'

System config

rooz system configure sets the .gitconfig rooz uses for cloning (e.g. for aliases).

Usage examples

Create an empty workspace

rooz new myworkspace

Create a workspace from a git repo

rooz new -g git@github.com:your/repo myworkspace2

Enter a previously created workspace

rooz enter myworkspace2

Run one command in a workspace

rooz exec myworkspace2 -- cargo test

Passes stdout and stderr through separately, with no tty or stdin, and exits with the command’s exit code.

  • --root, --work-dir and --container (a sidecar) work as in enter.

Inspect workspaces

rooz list [--json]                       # status, image, age, config origin
rooz status myworkspace2 [--json]        # containers, volumes, config origin
rooz logs myworkspace2 [-c sidecar] [-f] [--tail 100]

Editor support and inspecting merged config

rooz config schema > rooz.schema.json              # JSON Schema of the config file
rooz config show myworkspace --resolved            # merged settings, one row each, with the layer it came from
  • Use the schema with a YAML language server: add a # yaml-language-server: $schema=./rooz.schema.json first line, or set your editor’s yaml.schemas.
  • --resolved merges stored defaults, bases and config as a run does. Templates stay unexpanded; secret values show as ***. Flags given to rooz new are not recorded, so not shown.

Start a config from your project

rooz config template --detect > ws.rooz.yaml   # or --detect path/to/project

Prints a commented starter config. --detect sets the image and the toolchain’s cache paths from:

FileImage
Cargo.tomlrust:1
package.jsonnode:lts
go.modgolang:1
pyproject.toml / requirements.txtpython:3
*.sln / *.csprojmcr.microsoft.com/dotnet/sdk:<highest TargetFramework>, default 10.0

rooz update re-pulls floating tags. .NET is pinned because the SDK image only has its own runtime. With several stacks, the first picks the image; caches of all are listed.

Rename a workspace

rooz move myworkspace newname [--dry-run]
  • Stops the old workspace, copies its work and data volumes to the new name (volumes cannot be renamed), rebuilds it from its stored config and removes the old one.
  • Secrets need the same consent as rooz update.
  • The new name must be free; it never replaces.
  • On failure, the partial new workspace is removed and the old one is kept.
  • Needs about twice the workspace’s disk space. Install steps rerun; workspace processes end.
  • Afterwards, rooz enter it (as after rooz new).

Move a workspace to another host

rooz move myworkspace --to buildbox [newname] [--remove-source] [--dry-run]
  • Source: the current engine (-H/DOCKER_HOST). Target: a host from rooz remote. Both must be connected.
  • Volumes stream through this machine as tar (owners, modes, symlinks preserved; not buffered in memory).
  • Rebuilt on the target with rooz new from its config file, which must still be on this machine.
  • The source is left stopped. --remove-source removes it; otherwise rooz rm it yourself.
  • On failure, the partial target workspace is removed and the source is restarted.
  • Not moved: caches, images, the SSH key (run rooz system init on the target and register its public key).
  • Warns if the target CPU architecture differs: compiled artifacts will not run.

Check the setup

rooz doctor

Checks that:

  • the engine is reachable and recent enough
  • rooz system init was run (system config and SSH key volumes, age identity on this machine)
  • on Docker: usage of the default network address pool

Exits non-zero if something is missing.

Manage caches

rooz cache ls [--all-scopes] [--json]   # size, age, mounting workspaces
rooz cache rm '~/.cargo'                # by path, or by name as shown in 'ls'
rooz cache prune [--dry-run]            # removes every cache no workspace mounts

rm and prune only affect your own scope (see ROOZ_CACHE_SCOPE) and refuse caches mounted by a container. Size shows - on engines without disk usage reporting.

Interactive shell in an anonymous ephemeral workspace

rooz tmp --image alpine --shell sh

Configuration

ℹ️ Rooz uses YAML for configuration.

Settings come from env vars, command-line parameters, and a config file (--config on rooz new, most options - examples).

  • --config takes a local path or a git file, <repo-url>//<path-in-repo>, e.g. git@github.com:my/configs//path/in/repo/config.rooz.yaml. The repo is cloned with rooz’s SSH key, so only SSH URLs work: scp-like git@host:repo or ssh://git@host[:port]/repo (also git+ssh://, ssh+git://). Other schemes are refused.
  • Rooz does not read config from cloned repositories. Pass it explicitly: rooz new ws -g <url> --config <url>//.rooz.yaml

What a repository’s configuration may set

Repository config (a --config <url>//<path> source, e.g. its .rooz.yaml) may set fields scoped to the workspace: bases, git_ssh_url, image, shell, command, args, env, install, ports, data, mounts, sidecars, extra_repos, vars, user, init.

Refused from repository config:

fieldreasonto allow
secretsplaintext lands in a repo-controlled containerlist the source in ROOZ_TRUSTED_CONFIGS (Trusted config sources), or use a local --config file
cachesshared with your other workspacessame paths in ROOZ_CACHES (or --caches, which covers rooz new only, not rooz update), or source in ROOZ_TRUSTED_CONFIGS
install step root: trueruns as root in the workspacesource in ROOZ_TRUSTED_CONFIGS, or use a local --config file (Install steps)
privilegedfull host accessROOZ_ALLOW_PRIVILEGED=<names> (required for any config file)
sidecar vpnkernel netfilter access as root on a rootful engineROOZ_ALLOW_VPN=<names> (required for any config file)

Clone transports

Clone URLs (root repo, extra_repos) accept ssh://, git+ssh://, ssh+git://, scp-like user@host:path, https://, http://, git:// or file://. Anything else is refused, including ftp:// and remote helpers (name::address, e.g. ext::). Remote --config sources take SSH URLs only (see above).

http:// and git:// are unauthenticated (a network attacker controls the clone); rooz warns. Set ROOZ_REQUIRE_SECURE_TRANSPORT=true to refuse them.

Images

ℹ️ Default image: docker.io/chainguard/git:latest-dev

Set the image via:

  • ROOZ_IMAGE env var

  • --image parameter

  • image in a config file (--config):

    image: ghcr.io/queil/image:dotnet
    shell: [bash]
    

User

Rooz runs as uid 1000 (override with --uid on rooz new); the image must have this user, named rooz_user by default. Override the name with ROOZ_USER or --user: letters, digits, _, -, ., not starting with - or .. Home is /home/<user>: the SSH key volume mounts there and ~/ in mount targets expands to it.

Shell

Default: sh (bash for rooz tmp). Override via:

  • ROOZ_SHELL env var (on rooz new, rooz update, rooz tmp)
  • --shell parameter (on rooz enter, rooz tmp)
  • shell in a config file (a list, e.g. [bash])

Caching

Path-keyed shared caches:

caches: 
- ~/.nuget

Workspaces with the same cache path share one volume at that path. Also set via ROOZ_CACHES (comma-separated, merged with config paths) or --caches.

⚠️ Cache contents (cargo sources, linker config, build scripts) execute in every workspace mounting that path. Cache paths from repository config (a --config <url>//<path> source not in ROOZ_TRUSTED_CONFIGS) are refused unless you also list them in --caches (on rooz new) or ROOZ_CACHES. rooz update re-reads repository config each run, so only ROOZ_CACHES covers it. Local --config files are unaffected.

Cache volumes are scoped per operator (default: rooz process uid), so operators on a shared engine (remote DOCKER_HOST, multi-user socket) get separate volumes - toolchain caches hold executable config (~/.cargo/config.toml, ~/.m2/settings.xml, …). If uids collide across machines on one engine, set a distinct ROOZ_CACHE_SCOPE per operator. The scope is not access control: anyone who can create workspaces on the engine can use any scope.

Dry run

rooz new <name> --dry-run and rooz update <name> --dry-run print the plan and change nothing:

$ rooz new demo --config demo.rooz.yaml --dry-run
pull     image     docker.io/alpine:latest
create   volume    rooz_demo_work
reuse    cache     rooz_cache_uid-1000_---nuget  ~/.nuget
create   network   demo-egress
create   network   demo-net-db
create   sidecar   db  image=docker.io/postgres:17 install: 00-x[root]
create   work      demo  image=docker.io/alpine:latest user=rooz_user uid=1000 env=PGHOST
run      install   00-tools  root,skip_if

Dry run: nothing was changed.
  • Config is resolved and gated as in a real run (bases, templating, secrets, consent, trust); errors match.
  • update also lists containers to remove (and volumes, with --purge).
  • Env values, install scripts and secrets are not printed - only env names and step names/flags.
  • Queries the engine, creates nothing (no volumes, networks, containers, clones). A --config <url>//<path> source is fetched via a temporary helper container.
  • skip_if runs in the container, so a listed step may still be skipped.

Install steps

install runs shell steps in the work container on workspace creation. A step is a string or an object:

vars:
  dotnet: 11.0.100-rc.2.26504.105

install:
  00-tools:
    run: apk add jq
    root: true
  10-dotnet:
    skip_if: dotnet --list-sdks | grep -q '^{{ dotnet }}'
    run: ./install-dotnet.sh {{ dotnet }}
  • Steps run in key order as the workspace user; root: true runs as root. Sidecar steps run as the sidecar’s uid (default root).
  • skip_if runs first as the same user; exit 0 skips the step. Combine with a cache to skip repeat downloads. run and skip_if are templated.
  • skip_if sees the container env: put cached tools on PATH (via env) or use absolute paths.
  • bases layers add steps by key, replace whole steps, or remove with null.
  • root: true is refused from untrusted config (unlisted --config <url>//<path> source or bases layer). Add the source to ROOZ_TRUSTED_CONFIGS or use a local config.

Sidecars

A sidecar with install steps is built once: rooz runs the steps, commits localhost/rooz/<workspace>/<sidecar>:latest, and starts the sidecar from it.

  • The sidecar uses the committed image id, not the name (tags and labels are forgeable). The id is recorded on the sidecar container (dev.rooz.runtime-image).
  • Later runs skip install only if that id still exists and matches; otherwise install reruns and the tag is rebuilt.
  • Removing a workspace removes its built images.

Privileged containers

privileged: true in any config file (local or git, including sidecars) aborts workspace creation and names the containers. To allow:

  • ROOZ_ALLOW_PRIVILEGED=<names> - comma-separated container names, e.g. ROOZ_ALLOW_PRIVILEGED=dkr. Unnamed containers are still refused.
  • ROOZ_ALLOW_PRIVILEGED=true - allows any. Use for CI only.
  • --privileged true - consents and makes the work container privileged regardless of config.

Prefer names over true, e.g. name only the docker-in-docker sidecar, not the workspace using it via DOCKER_HOST. Names are safe to keep in your shell profile; a standing =true also allows privileged containers added by repository config.

⚠️ Only for trusted configs. For repositories you do not control, prefer rootless Podman (confines the grant to your user namespace).

VPN sidecars

vpn: true on a sidecar grants what WireGuard (wg-quick, kernel or wireguard-go), OpenVPN etc. need: NET_ADMIN, /dev/net/tun and net.ipv4.conf.all.src_valid_mark=1, scoped to the sidecar’s network namespace (no host devices or network, unlike privileged).

It exposes kernel netfilter/netlink, a container-escape surface on rootful engines, so it requires ROOZ_ALLOW_VPN=<names> (or =true for any). Independent of ROOZ_ALLOW_PRIVILEGED.

sidecars:
  vpn:
    image: docker.io/linuxserver/wireguard:latest
    vpn: true
    egress: true     # to reach the WireGuard endpoint
    init: false      # the image's s6-overlay must run as pid 1
    mounts:
      /config/wg_confs/wg0.conf:
        content: |
          [Interface]
          ...
  • Must run as uid 0 (default); vpn with another uid is refused (NET_ADMIN is ineffective for non-root).
  • Kernel WireGuard needs the wireguard module loaded on the engine host (containers cannot load it); otherwise wg-quick falls back to wireguard-go.
  • Full tunnels (AllowedIPs = 0.0.0.0/0) work; the work container still reaches the sidecar over its pair network.
  • Omit DNS =: resolvconf conflicts with the engine-managed /etc/resolv.conf.
  • The tunnel is the sidecar’s only. Expose it via a proxy in the sidecar, e.g. HTTP_PROXY=http://vpn:3128 for the work container (or peers:).

Files next to the config

file: loads content from a file (like a Kustomize config map generator). Works wherever content: does: data, inline mounts, sidecar mounts.

data:
  hello.sh:
    file: files/hello.sh
    executable: true
mounts:
  ~/bin/hello.sh: hello.sh
  ~/.config/starship.toml:
    file: files/starship.toml
  • Path is relative to, and below, the declaring config’s directory (a base’s to the base). No absolute paths, no ...
  • Git configs read from the same checkout.
  • Symlinks are refused anywhere on the path.
  • Max 64 KiB, UTF-8 text. For larger or binary data use a volume or generate:.
  • Not templated; use content: for {{ vars }}.
  • The stored config keeps the reference, not the content.

Port mappings

Work container ports - config file only. Format [<host port>:]<container port>, as in Docker, bound to 127.0.0.1:

ports:
- "8080:80"
- "8022:22"

Variables and templating

  • Syntax: handlebars.
  • Declare variables in vars, secrets in secrets. Secrets are decrypted before expansion.
  • Works in most config values, not in YAML keys.
  • Works inside vars, but only for variables defined earlier in the document.
  • Does not work inside secrets.
secrets:
  sqlPassword: '-----BEGIN AGE ENCRYPTED FILE-----|YWdlLWVuY3J ... truncated'

vars:
  sqlUser: admin
  sqlConnectionString: "uid={{ sqlUser }};pwd={{ sqlPassword }}"

env:
  SQL_CONNECTION_A: "database=A;{{ sqlConnectionString }}"
  SQL_CONNECTION_B: "database=B;{{ sqlConnectionString }}"

sidecars:
  tool:
    env:
      SQL_USER: "{{ sqlUser }}"
      SQL_PASSWORD: "{{ sqlPassword }}"

Secrets

Rooz encrypts sensitive values (e.g. API keys) in config files with age.

Example

Run rooz system init first.

Run touch example.yaml and rooz config edit example.yaml. Paste this into the editor, save and quit:

secrets:
  apiKey: 1744420283158995

env:
  API_URL: https://api.example.com/v1
  API_KEY: "{{ apiKey }}"

cat example.yaml now shows the secret encrypted:

secrets:
  apiKey: '-----BEGIN AGE ENCRYPTED FILE-----|YWdlLWVuY3J ... truncated'
env:
  API_URL: https://api.example.com/v1
  API_KEY: "{{ apiKey }}"

Create a workspace, allowing the secret (see Where secrets may be used):

ROOZ_ALLOW_SECRETS=apiKey rooz new secrets-test --config ./example.yaml

Inside the container, API_KEY holds the decrypted value:

rooz enter secrets-test

> echo $API_KEY
1744420283158995

Where secrets may be used

Rooz expands secrets only when both conditions hold:

  1. you allowed the secrets explicitly, and
  2. the whole configuration comes from a trusted source.

Otherwise rooz new aborts and names the secrets involved. vars are not restricted.

Allowing secrets:

  • ROOZ_ALLOW_SECRETS=<names> - comma-separated secret names, e.g. ROOZ_ALLOW_SECRETS=dbPassword. Unlisted secrets are still refused, so it is safe to keep in your shell profile. Also covers rooz update, which re-renders the configuration on every run.
  • ROOZ_ALLOW_SECRETS=true - allows all secrets the configuration declares. Useful for CI.
  • --allow-secrets true - same, for a single rooz new, rooz update or rooz move.

Trusted configuration: one of

  • a local file passed with --config, with nothing else merged into it;
  • a source listed in ROOZ_TRUSTED_CONFIGS (see Trusted config sources).

Rooz refuses to expand secrets if the workspace also merges:

  • a --config <url>//<path> source not listed in ROOZ_TRUSTED_CONFIGS;
  • a bases layer outside a trusted directory.

Any templated field (env, install, command, shell, data content, sidecar env) can reference a secret, so whoever authors a merged layer could exfiltrate it, e.g. env: {LEAK: "{{ MY_SECRET }}"}.

⚠️ Rooz cannot tell your own --config file from one copied out of a repository. Never point --config at a file from the repository you are opening, and read every base layer before including it.

Trusted config sources

Ciphertext is not bound to a configuration: anyone who has the ciphertext can paste it into their own config and rooz will decrypt it. Rooz therefore trusts configurations by source:

export ROOZ_TRUSTED_CONFIGS='git@github.com:me/rooz-configs,~/rooz-configs'
  • Repository URL - --config <url>//<path> from it is trusted, including its bases (read from the same clone). Matched exactly as passed to --config: .../configs and .../configs.git are different entries.
  • Local directory - a --config file is trusted, bases included, when the file and every base it pulls in resolve (symlinks followed) inside it.

A trusted source gets the same as your own local file: secrets (still subject to ROOZ_ALLOW_SECRETS) and shared caches. Anyone who can push to a trusted repository can read your secrets - list only repositories that are yours alone.

Secrets at rest

Decrypted values are never written to the workspace-config volume. The container’s environment and generated files get the real values at creation time, but the persisted runtime.config (rooz config show <ws> --part runtime) has every secret occurrence replaced with ***. Config files in that volume are owner-only.

ℹ️ shell is not masked, because rooz enter executes it. Do not reference secrets from shell.

Sidecars

Sidecars are containers running alongside the work container - a minimal docker-compose. Define them in the config file only:

sidecars:
  sql:
    image: my:sql
    command:
    - --some
    env:
      TEST: "true"

  tools:
    image: my:tools

image may be omitted in a single config layer (e.g. an overlay extending a sidecar from bases:), but must be set once all layers are merged, or workspace creation fails.

--container <name> targets a sidecar instead of the work container in rooz enter, rooz exec and rooz logs.

Networking

Hub-and-spoke:

  • the work container reaches every sidecar over a dedicated pair network, by sidecar name (sql, tools above); container ID and IP also work
  • every sidecar can reach the work container (pair networks are symmetric - keep its listeners minimal)
  • sidecars reach each other only via peers: (or both setting egress: true)
  • sidecars have no internet access unless egress: true
  • workspaces are isolated from each other

peers: lists sidecars this sidecar may reach:

  • each relation gets its own network; sidecars resolve each other by name on it

  • a: peers [b] equals b: peers [a]

  • names must refer to sidecars in the same workspace

  • merged as a deduplicated union across config layers (other lists are replaced): overlays can add edges, not remove them

    sidecars:
      claude:
        peers: [proxy]   # no internet; reaches it only via the proxy
      proxy:
        egress: true     # internet access
      dkr:
        peers: [images]  # docker-in-docker pulling via the mirror
      images:
        egress: true     # registry pull-through cache, needs internet
    

⚠️ Rooz creates one network per workspace, one per sidecar and one per peer relation. Docker’s default address pools allow only ~31 networks host-wide. On an address-pool error, set default-address-pools with a smaller subnet size (e.g. "size": 24) in daemon.json. Podman (netavark) is not affected.

Keys

  • image - container image
  • env - environment variables
  • command - override entrypoint (ENTRYPOINT)
  • args - override entrypoint arguments (CMD)
  • mounts - container path to a data key or inline content:/file:, as in the work container’s mounts. Volumes survive container restarts/deletes:
data:
  sql-data: {}   # empty rw volume
sidecars:
  sql:
    mounts:
      /var/opt/mssql: sql-data
      /etc/tool/config.json:
        content: |-
          {"some": "config"}
  • ports - port bindings, same format as work container ports
  • vpn (bool) - grants NET_ADMIN, /dev/net/tun and src_valid_mark for in-sidecar VPNs, with operator consent (see VPN sidecars)
  • peers - sidecars this one may reach (see Networking)
  • egress (bool, default false) - outbound internet access: joins the workspace’s <workspace>-egress network, shared with the work container and other egress sidecars. Install steps always have internet access.
  • install - install steps, built into an image (see Sidecars)
  • uid - uid to run as (default 0)
  • shell - shell for rooz enter --container (default sh)
  • init (bool, default true) - run an init process as pid 1
  • privileged (bool) - with operator consent (see Privileged containers)
  • work_dir - working directory

Connecting to a remote Docker/Podman host

Rooz uses the local Docker daemon by default. It can work with multiple remote hosts at once: rooz remote forwards each host’s Docker/Podman socket over SSH to a local unix socket, and -H <host> targets one of them.

Preparation

The remote host

Rooz needs the engine socket path. Pass it with --socket when adding the host (below), or export it at the top of ~/.bashrc on the host (read by non-interactive ssh):

  • Docker as root: export DOCKER_HOST=/var/run/docker.sock
  • rootless Docker: export DOCKER_HOST=/run/user/1000/docker.sock (uid 1000)
  • Podman: export DOCKER_HOST=/run/user/1000/podman/podman.sock (uid 1000)

The local host

Define hosts (stored in ~/.config/rooz/remotes.yaml, $XDG_CONFIG_HOME/rooz/remotes.yaml if set, or ROOZ_REMOTES_FILE):

rooz remote add box1 ssh://your-user@box1
rooz remote add box2 ssh://your-user@box2 --socket /run/user/1000/podman/podman.sock
rooz remote ls

Keep this running (e.g. in tmux):

rooz remote          # connects to all configured hosts
rooz remote box1     # or just some of them
  • each socket is forwarded to $XDG_RUNTIME_DIR/rooz/<host>.sock (~/.rooz/<host>.sock if XDG_RUNTIME_DIR is unset)
  • hosts are independent: one down or reconnecting doesn’t affect the others
  • only one rooz remote can serve a given host at a time

Working with a host

rooz -H box1 new myws        # one-off
export ROOZ_HOST=box2        # per shell: every rooz command in it targets box2
rooz list                    # lists box2's workspaces
rooz list --all-hosts        # workspaces of every connected host
rooz -H box1 code myws       # attaches VsCode to the workspace on box1
  • precedence: -H / ROOZ_HOST, then DOCKER_HOST, then the local engine
  • there is no global “current host”: use one terminal (or ROOZ_HOST) per host
  • rooz system prune and rooz rm --all print their target first
  • workspaces, volumes, SSH keys and caches live on each host’s engine: the same workspace name on two hosts means two unrelated workspaces
  • the age identity and ROOZ_TRUSTED_CONFIGS are local and apply to all hosts

Port forwarding

rooz remote forwards ports listed in a remote container’s dev.rooz.forward-ports label (set from ports) to the same port on 127.0.0.1. Local ports are shared by all hosts: a port already bound locally or forwarded for another host is skipped with a warning.

VsCode

rooz code starts code with DOCKER_HOST set to the host’s socket. An already-running VsCode may ignore it and use its docker.host setting instead:

   "docker.host": "unix:///run/user/1000/rooz/box1.sock"

Running with Podman

  1. Enable the podman remote socket. podman info should contain:
remoteSocket:
    exists: true
    path: /run/user/1000/podman/podman.sock

If exists: true is missing, run systemctl --user enable --now podman.socket.

  1. Expose the socket as DOCKER_HOST:
export DOCKER_HOST=unix:///run/user/1000/podman/podman.sock
  1. Use fully-qualified image names, or define unqualified-search registries in /etc/containers/registries.conf.

  2. Complex podman-in-podman scenarios (e.g. networking) may need --privileged (more info).

Notes

Other facts

  • cloned repos are in /work/{repo_name}, repo_name being git’s default clone directory name

  • rooz new/update print one [phase] line per step (image, volumes, clone, container, install) to stderr. -v/--verbose adds debug logging; RUST_LOG=rooz also works

  • rooz system prune removes ALL rooz containers and volumes. rooz rm --all --force removes only workspaces, keeping shared cache volumes and the SSH key volume

    ⚠️ rooz system prune deletes all rooz state. Back up anything important first.

CLI reference

Generated from rooz 0.168.1.

rooz

Usage: rooz [OPTIONS] <COMMAND>

Commands:
  new      Creates a new workspace (container + volumes)
  enter    Opens an interactive shell to a workspace's container
  exec     Runs one command in a workspace container and exits with its exit code
  code     Attaches VsCode to a workspace. (requires VsCode installed and 'code' in $PATH)
  start    Starts a workspace
  stop     Stops a workspace
  restart  Restarts a workspace's work container
  remove   Removes a workspace
  update   Updates a workspace created from a config file
  move     Renames a workspace, or with --to moves it to another host: copies its volumes, rebuilds it there and removes the old one (the source is kept with --to)
  list     Lists workspaces
  cache    Manages the shared cache volumes
  doctor   Checks the engine connection, rooz's setup and the engine's headroom
  status   Shows a workspace's containers, volumes and config origin
  logs     Shows the logs of a workspace container
  config   Config subcommands
  tmp      Enters an ephemeral workspace
  remote   Connects to remote hosts: forwards their engine sockets and published ports. Stays in the foreground
  system   System subcommands
  help     Print this message or the help of the given subcommand(s)

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

  -V, --version
          Print version

rooz new

Creates a new workspace (container + volumes)

Usage: rooz new [OPTIONS] <NAME>

Arguments:
  <NAME>
          

Options:
  -g, --git-ssh-url <GIT_SSH_URL>
          

  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -i, --image <IMAGE>
          

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

      --home-from-image <HOME_FROM_IMAGE>
          

      --pull-image
          

  -u, --user <USER>
          

      --uid <UID>
          

      --caches <CACHES>
          Enables defining global shared caches

  -p, --privileged <PRIVILEGED>
          [possible values: true, false]

      --allow-secrets <ALLOW_SECRETS>
          Consents to expanding the 'secrets' of the --config file into this workspace
          
          [possible values: true, false]

      --start <START>
          Starts the workspace immediately
          
          [default: true]
          [possible values: true, false]

      --config-path <CONFIG_PATH>
          Configures the new workspace from a config file given by the path.

      --dry-run
          Prints what would be created, without creating anything

  -h, --help
          Print help

rooz enter

Opens an interactive shell to a workspace's container

Usage: rooz enter [OPTIONS] <NAME>

Arguments:
  <NAME>
          

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -s, --shell <SHELL>
          

  -r, --root
          

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -w, --work-dir <WORK_DIR>
          

  -c, --container <CONTAINER>
          

      --chown
          Chowns the container's volume mounts to the user the container runs as

  -h, --help
          Print help

rooz exec

Runs one command in a workspace container and exits with its exit code

Usage: rooz exec [OPTIONS] <NAME> -- <CMD>...

Arguments:
  <NAME>
          

  <CMD>...
          

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -r, --root
          

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -w, --work-dir <WORK_DIR>
          

  -c, --container <CONTAINER>
          

  -h, --help
          Print help

rooz code

Attaches VsCode to a workspace. (requires VsCode installed and 'code' in $PATH)

Usage: rooz code [OPTIONS] <NAME>

Arguments:
  <NAME>
          

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz start

Starts a workspace

Usage: rooz start [OPTIONS] <NAME>

Arguments:
  <NAME>
          

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz stop

Stops a workspace

Usage: rooz stop [OPTIONS] [NAME]

Arguments:
  [NAME]
          

Options:
  -a, --all
          

  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz restart

Restarts a workspace's work container

Usage: rooz restart [OPTIONS] <NAME>

Arguments:
  <NAME>
          

Options:
      --all-containers <ALL_CONTAINERS>
          [default: false]
          [possible values: true, false]

  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz remove

Removes a workspace

Usage: rooz remove [OPTIONS] [NAME]

Arguments:
  [NAME]
          

Options:
  -f, --force
          Kill running containers

  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -a, --all
          Remove all workspaces

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz update

Updates a workspace created from a config file

Usage: rooz update [OPTIONS] <NAME>

Arguments:
  <NAME>
          

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -t, --tweak
          If set allows to tweak the existing config and apply the edited version.

      --purge
          If set it removes the workspace volumes. WARNING: potential data loss ahead

      --no-pull
          If set it skips pulling new images

      --dry-run
          Prints what the update would remove and create, without changing anything

      --allow-secrets <ALLOW_SECRETS>
          Consents to expanding the 'secrets' of the workspace config into the updated workspace
          
          [possible values: true, false]

  -h, --help
          Print help

rooz move

Renames a workspace, or with --to moves it to another host: copies its volumes, rebuilds it there and removes the old one (the source is kept with --to)

Usage: rooz move [OPTIONS] <NAME> [NEW_NAME]

Arguments:
  <NAME>
          

  [NEW_NAME]
          The new name; with --to it defaults to the current one

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

      --to <TO>
          Moves the workspace to this host (see 'rooz remote'; both hosts must be connected). The workspace is left stopped on the source

      --remove-source
          Removes the source workspace after a successful move

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

      --dry-run
          Prints what would be copied and rebuilt, without changing anything

      --allow-secrets <ALLOW_SECRETS>
          Consents to expanding the 'secrets' of the workspace config into the moved workspace
          
          [possible values: true, false]

  -h, --help
          Print help

rooz list

Lists workspaces

Usage: rooz list [OPTIONS]

Options:
      --all-hosts
          Lists workspaces on all configured hosts that 'rooz remote' is connected to

  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

      --json
          Prints JSON instead of a table

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz cache

Manages the shared cache volumes

Usage: rooz cache [OPTIONS] <COMMAND>

Commands:
  ls     Lists caches with size and the workspaces mounting them
  rm     Removes caches by name or path (e.g. '~/.cargo'); refuses mounted ones
  prune  Removes all caches that no workspace mounts
  help   Print this message or the help of the given subcommand(s)

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz cache ls

Lists caches with size and the workspaces mounting them

Usage: rooz cache ls [OPTIONS]

Options:
      --all-scopes
          Includes other operators' caches (see ROOZ_CACHE_SCOPE)

  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

      --json
          Prints JSON instead of a table

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz cache rm

Removes caches by name or path (e.g. '~/.cargo'); refuses mounted ones

Usage: rooz cache rm [OPTIONS] <NAMES>...

Arguments:
  <NAMES>...
          

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz cache prune

Removes all caches that no workspace mounts

Usage: rooz cache prune [OPTIONS]

Options:
      --dry-run
          Prints what would be removed, without removing

  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz doctor

Checks the engine connection, rooz's setup and the engine's headroom

Usage: rooz doctor [OPTIONS]

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz status

Shows a workspace's containers, volumes and config origin

Usage: rooz status [OPTIONS] <NAME>

Arguments:
  <NAME>
          

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

      --json
          Prints JSON instead of text

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz logs

Shows the logs of a workspace container

Usage: rooz logs [OPTIONS] <NAME>

Arguments:
  <NAME>
          

Options:
  -c, --container <CONTAINER>
          A sidecar name; the work container by default

  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -f, --follow
          Keeps streaming new output

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

      --tail <TAIL>
          Number of lines from the end, e.g. 100

  -h, --help
          Print help

rooz config

Config subcommands

Usage: rooz config [OPTIONS] <COMMAND>

Commands:
  template  Outputs a workspace configuration template
  edit      Edits a local configuration file
  show      Shows a workspace's configuration
  schema    Outputs the JSON Schema of the config file format, for editor completion and validation
  help      Print this message or the help of the given subcommand(s)

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz config template

Outputs a workspace configuration template

Usage: rooz config template [OPTIONS]

Options:
      --detect [<DIR>]
          Looks for Cargo.toml, *.csproj/*.sln, package.json, go.mod and pyproject.toml/requirements.txt in DIR (default: the current directory) and fills in the image and caches

  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz config edit

Edits a local configuration file

Usage: rooz config edit [OPTIONS] <CONFIG_PATH>

Arguments:
  <CONFIG_PATH>
          

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz config show

Shows a workspace's configuration

Usage: rooz config show [OPTIONS] <NAME>

Arguments:
  <NAME>
          

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -p, --part <PART>
          [default: body]
          [possible values: origin, body, runtime]

      --resolved
          Shows the merged settings (defaults, bases, config) and the layer each came from

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz config schema

Outputs the JSON Schema of the config file format, for editor completion and validation

Usage: rooz config schema [OPTIONS]

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz tmp

Enters an ephemeral workspace

Usage: rooz tmp [OPTIONS]

Options:
  -g, --git-ssh-url <GIT_SSH_URL>
          

  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -i, --image <IMAGE>
          

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

      --home-from-image <HOME_FROM_IMAGE>
          

      --pull-image
          

  -u, --user <USER>
          

      --uid <UID>
          

      --caches <CACHES>
          Enables defining global shared caches

  -p, --privileged <PRIVILEGED>
          [possible values: true, false]

      --allow-secrets <ALLOW_SECRETS>
          Consents to expanding the 'secrets' of the --config file into this workspace
          
          [possible values: true, false]

      --start <START>
          Starts the workspace immediately
          
          [default: true]
          [possible values: true, false]

  -r, --root
          

  -s, --shell <SHELL>
          [env: ROOZ_SHELL]
          [default: bash]

  -h, --help
          Print help

rooz remote

Connects to remote hosts: forwards their engine sockets and published ports. Stays in the foreground

Usage: rooz remote [OPTIONS] [NAMES]...
       rooz remote <COMMAND>

Commands:
  add   Adds or updates a remote host
  rm    Removes a remote host
  ls    Lists configured remote hosts
  help  Print this message or the help of the given subcommand(s)

Arguments:
  [NAMES]...
          Hosts to connect to. Default: all configured

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz remote add

Adds or updates a remote host

Usage: rooz remote add [OPTIONS] <NAME> <SSH_URL>

Arguments:
  <NAME>
          Host name, used with -H/--host

  <SSH_URL>
          Host's SSH url, e.g. ssh://user@host

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

      --socket <SOCKET>
          Path to the container engine socket on the host. Default: $DOCKER_HOST as seen by a non-interactive ssh command

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz remote rm

Removes a remote host

Usage: rooz remote rm [OPTIONS] <NAME>

Arguments:
  <NAME>
          

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz remote ls

Lists configured remote hosts

Usage: rooz remote ls [OPTIONS]

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz system

System subcommands

Usage: rooz system [OPTIONS] <COMMAND>

Commands:
  prune       Prunes all rooz resources
  init        Initializes rooz system
  completion  Outputs shell completion scripts
  configure   Opens the system config in EDITOR
  help        Print this message or the help of the given subcommand(s)

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz system prune

Prunes all rooz resources

Usage: rooz system prune [OPTIONS]

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz system init

Initializes rooz system

Usage: rooz system init [OPTIONS]

Options:
  -f, --force
          

  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

      --age-identity <AGE_IDENTITY>
          Initializes rooz with the provided age identity rather than generating a new one

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

      --rotate-ssh-key
          Generates a new ssh key pair, replacing the current one. Every workspace on this engine gets the new key, and the old one must be de-registered wherever you added it

  -h, --help
          Print help

rooz system completion

Outputs shell completion scripts

Usage: rooz system completion [OPTIONS] <SHELL>

Arguments:
  <SHELL>
          [possible values: bash, elvish, fish, powershell, zsh]

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

rooz system configure

Opens the system config in EDITOR

Usage: rooz system configure [OPTIONS]

Options:
  -H, --host <HOST>
          Runs against a host from 'rooz remote ls' instead of DOCKER_HOST
          
          [env: ROOZ_HOST]

  -v, --verbose
          Prints debug logging (RUST_LOG still applies when this is not set)

  -h, --help
          Print help

Config schema

Each release publishes the config file’s JSON Schema at rooz.dev/schema.json. Use it with YAML language servers via a modeline:

# yaml-language-server: $schema=https://rooz.dev/schema.json

It matches the latest release. For the schema of your installed version:

rooz config schema > rooz.schema.json

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.168.1] - 2026-10-17

Added

  • The docs site shows the release version in the top bar.

Fixed

  • rooz enter follows terminal resizes; the session used to keep the size it started with.

[0.168.0] - 2026-10-17

Changed

  • Breaking: ports mappings are <host>:<container>, as in Docker and kubectl 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 --config sources must use SSH. Non-SSH URLs (https://, http://, git://, file://, remote helpers) are refused with a clear message.
  • Breaking: the sidecar user key is removed; it was accepted but never used. Configs that set it fail to parse - use uid.

Added

  • Remote --config accepts ssh:// URLs (also git+ssh://, ssh+git://), e.g. ssh://git@host:2222/my/configs//cfg.yaml for 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 | sh installs the latest release. Each release carries an install.sh that pins its own version and SHA256SUMS, so the binary is checked against data served from rooz.dev, not the GitHub release it comes from. With a logged-in gh, 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 stay rooz_sys-config, rooz_cache_<scope>_<path>). The ssh key volume is rooz_ssh-key, was rooz-ssh-key-vol. Volumes under the old names are not found or migrated: remove them (rooz system prune removes everything rooz made), then run rooz system init - a new ssh key is generated and must be registered again.

[0.165.0] - 2026-10-16

Changed

  • rooz doctor starts with the endpoint it connects to (the -H host’s socket, DOCKER_HOST, or the default socket), and -v logs 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’s client error (Connect). A missing default socket (no DOCKER_HOST, no /var/run/docker.sock) no longer panics.

[0.164.0] - 2026-10-16

Added

  • rooz new --dry-run and rooz 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. rm and prune only touch your own scope and refuse caches a container still mounts.
  • rooz doctor checks the engine, rooz system init results (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 from rooz 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 init done, config gates) and a failure removes the partial workspace there.
  • rooz config schema prints a JSON Schema of the config file for YAML language servers.
  • rooz config show <ws> --resolved shows 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/--verbose turns on debug logging (RUST_LOG still works). new and update print one colored [phase] line per step: image, volumes, clone, container, install.
  • rooz update --allow-secrets, as on new; before, only ROOZ_ALLOW_SECRETS could consent.

Changed

  • Breaking: rooz list shows 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 update pulls the image before removing the old containers, so a failed pull leaves the workspace as it was (an image that only comes from bases is still pulled afterwards).
  • rooz remote log lines color each host name consistently.

[0.163.0] - 2026-10-16

Added

  • install steps can be objects: run, skip_if and root. skip_if is 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. run and skip_if are 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 as apt-get or apk add, must say so with root: true. A configuration the operator did not author (unlisted --config git:... or bases layer) is refused if it asks for root: true.

[0.162.0] - 2026-10-15

Added

  • file: <path> on a data value (data, inline mounts, sidecar mounts) 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 remote serves 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>.sock without it) and its published ports to localhost. A host that is down retries with backoff without affecting the others.
  • Global -H/--host <name> (env ROOZ_HOST) runs any command against a configured host. rooz list --all-hosts lists every connected host. rooz code attaches VsCode through the selected host.
  • rooz system prune and rooz remove --all print the engine they target.

Changed

  • Breaking: rooz remote no longer reads ROOZ_REMOTE_SSH_URL / DOCKER_HOST, nor forwards to $DOCKER_HOST; define the host with rooz remote add instead.
  • A host may set socket to skip the $DOCKER_HOST lookup 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 quoted Error: "..." line. This covers the ROOZ_ALLOW_PRIVILEGED, ROOZ_ALLOW_SECRETS, ROOZ_ALLOW_VPN and ROOZ_TRUSTED_CONFIGS refusals, 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 fetch over ssh inside a workspace no longer print hostfile_replace_entries: mkstemp: Read-only file system. The ssh key volume is mounted read-only in workspaces, and ssh kept trying to rewrite known_hosts there. The volume now carries an ssh client config with UpdateHostKeys no, written by rooz 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: true on a sidecar grants NET_ADMIN, /dev/net/tun and net.ipv4.conf.all.src_valid_mark=1 - enough for wg-quick (kernel or wireguard-go) and other in-container VPNs, without privileged. Requires operator consent naming the sidecars, ROOZ_ALLOW_VPN=<names> (or =true), separate from ROOZ_ALLOW_PRIVILEGED; a vpn sidecar must run as uid 0.
  • ROOZ_TRUSTED_CONFIGS lists configuration sources that are the operator’s own: repository URLs (matched exactly as passed to --config, never over http:///git://) and local directories (every file, bases included, must resolve inside one). A trusted source may expand secrets (still subject to ROOZ_ALLOW_SECRETS) and declare caches - including through bases - which previously only a local --config file without bases could.

Removed

  • Breaking: rooz no longer reads a .rooz.yaml from 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 update re-fetches it as a --config git:... source.

Fixed

  • A rooz new refused for a missing ROOZ_ALLOW_PRIVILEGED (or ROOZ_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 refused rooz update keeps the previously stored config.
  • rooz update keeps 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 /work instead of the clone, and --purge removed 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 predictable localhost/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-config volume is no longer adopted as this machine’s identity. It is taken out of the volume and kept at ~/.config/rooz/age.engine.bak for the operator to recognise and install with rooz 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, env keys, mounts naming an undefined data: entry and non-YAML bases paths are refused with a message instead of panicking, before any volume or container is created.
  • env keys must be identifier-shaped, and the ROOZ_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 list skips 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 secrets now needs explicit consent - ROOZ_ALLOW_SECRETS=<names> (comma-separated, safe to leave in a shell profile and also covers rooz update), ROOZ_ALLOW_SECRETS=true, or --allow-secrets true. A local --config path is not proof that the operator wrote the file: repositories ship config files and README instructions to pass them. Workspaces without secrets: are unaffected.
  • FileFormat::from_path, RoozCfg::parse_ports, VolumeApi::create_volume_specs and VolumeApi::mounts_with_sources return Result instead 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.