$ whoami
sally
$ vz new
sally@vz-0-app:~/repos/app$ whoami
sally
Describe a repository's environment in a few lines of YAML. vz starts it in a container and drops
you in: as yourself, in your repository, with only what you chose to share. On a laptop, a server or
a CI runner, for you and for the agents working beside you.
Status: young and moving fast. It works day to day, but the configuration may still change before 1.0. Feedback and issues are welcome.
Why vz
- Your repo, mounted.
cdinto any git repository and typevz new. The repository is there, read-write, at the same path as on your host. - You, inside. Same user, uid, groups, home and paths. Files you create stay yours; git and ssh just work; paths in errors match your editor's.
- Any image. Official images work unchanged, or
vzbuilds the repository's own Dockerfile. Switch branches, switch environments; nothing rebuilds needlessly. - Configurations. Yours in a library, the project's in the repository, chained by
extends:vz new -c trustedfor sudo and docker,cifor the pipeline. - Secure by default, made for agents. No Linux capabilities, no sudo, no docker, no host network, no secrets, unless you grant them. Let an agent loose: of your machine, it reaches the repository and nothing else.
- State that stays. Caches, shell history and agent sessions survive the container, per repository, and never clutter your home.
- Secrets out of sight. Env files are read on the host and pass by name: never on a command line, never in a log.
- Sessions. Name a container, keep it, attach from any terminal:
vz new api,vz at api. - One static binary. No daemon, no runtime, no editor plugin. Just Docker.
Quick start
From the latest release: the static binary and its alias vz, into ~/.local/bin:
url=https://github.com/jan-blomquist/viz-shell/releases/latest/download/viz-shell-x86_64-unknown-linux-musl.tar.gz
&& &&
&&
Or from crates.io, as the static musl build vz needs, since it mounts itself into every container:
Or from a checkout (needs Rust and just):
&&
&&
This repository is the worked example: three files.
# vz.yml: the toolchain to build viz-shell, on the library's Debian
name: default
extends: vz-debian-trixie # the library's base: sudo, the docker CLI, locales
image: # ARG BASE: built on vz-debian-trixie's image
state:
- /usr/local/cargo/registry # survives the container, kept in .vz_state/
env:
defaults:
files: # read on the host, never mounted
# dev.vz.yml: the developer shell, on the toolchain
name: dev
extends: default
image: # fish, Node, opencode, Codex, Claude Code
shell: fish
state:
env:
passthrough:
The first run writes your library, ~/.config/viz-shell/: one configuration, vz-debian-trixie, and
the Dockerfile it builds. Your own additions are one more file, not checked in:
# sally.vz.yml
name: sally
extends: dev
mounts:
- ~/repos/notes:rw
env:
passthrough:
How vz differs
- Not an editor plugin. Dev Containers center on the editor; vz is shell-first. Use it with any editor, or none, over ssh, on a server, in CI.
- Not a package manager. Nix and Devbox assemble packages; vz runs whatever image you give it, and builds your Dockerfile when you give it one.
- Not host integration. Distrobox and Toolbox deliberately share your home and your host; vz isolates by default and shares only what the configuration declares.
Guide
- Use
- Configurations
- Sessions
- Images
- Your user in the image
- State
- Mounts
- Share
- Privileges
- Environment
- Hooks
- Examples
- Logging
- Development
- License
Use
vz alone lists the commands. vz (the alias of viz-shell) reads the configurations
of the repository and your library, and runs the chain of the one asked for, default without -c. It pulls or
builds the image if missing, and runs a container, removed on exit unless persistent, with its
hooks: create once, attach before every shell or command. vz exits with the shell's,
or the command's, exit code. Inside:
- the repository is mounted read-write at its host path; the working directory is yours;
- you are you: same user, uid, group and home, so
whoami,~and ssh work as on the host; TERM,COLORTERM,LANGandVZ_LOGare copied in when set. ATERMthe image has no description for, as slim images lack those of newer terminals like ghostty, kitty or wezterm, becomesxterm-256color, so tmux, less and htop still work;VZ_CONTAINERnames the container,VZ_CONTAINER_CONFIGits configuration, when notdefault, andVZ_REPOthe repository root: for prompts, scripts and agents that want to know where they run.
banner: prints a banner above an interactive shell (never above vz -- command), in the
manner of fastfetch: what the shell is about to be. true shows the built-in art, false no banner,
and a string, art of your own above the facts; an empty one, the facts alone. Write it as a |2
block when its first line starts with spaces, so YAML knows the indentation. Colored on a terminal,
unless NO_COLOR is set.
The environment shows as a count, never names or values.
_ _ _ _
__ _(_)____ ___| |__ ___| | |
\ \ / / |_ /____/ __| '_ \ / _ \ | |
\ V /| |/ /_____\__ \ | | | __/ | |
\_/ |_/___| |___/_| |_|\___|_|_|
sally@vz-0-app
--------------
Version: 0.1.0
Session: new, ephemeral
Repo: ~/repos/app
Branch: main
Chain: vz-debian-trixie (library) → default → dev → sally
Config: sally
Image: vz-app:3f9c2a1b7d4e8f60 (on vz-viz-shell:9a1c0d2e5b7f3a41)
Shell: fish
Sudo: yes
Docker: /run/user/1000/docker.sock
Network: host
Mounts: 4 (2 vz-debian-trixie.vz.yml, 1 vz.yml, 1 sally.vz.yml)
State: 2 paths in ~/repos/app/.vz_state
Env: 3 variables
Hooks: 2 create, 1 attach
The library's vz-debian-trixie writes the built-in art out in full, so it is there to edit; a later
configuration replaces it, or turns it off with banner: false.
shell picks the interactive shell: a name on the image's PATH, or an absolute path. It is also
$SHELL and your login shell inside. An image without it gives a warning, then bash, else sh, so a
library shell: fish doesn't break images without fish. Keep fish's configuration and history per
repository, apart from the host's, as state:
shell: fish
state:
- ~/.config/fish
- ~/.local/share/fish
Unknown keys in a configuration are refused, naming the line.
Configurations
A configuration is one YAML document: an image, state, mounts, env, hooks, and what the shell may
share and do. vz new runs default; vz new -c NAME runs NAME, after the configurations it
extends. Two places hold them: the repository, at the git root, and the library,
~/.config/viz-shell/ (or under $XDG_CONFIG_HOME), for configurations a repository extends by name.
- Files.
vz.yml,*.vz.ymland*.vz.yaml, in the repository root, the library, and the folders the library's files list underscan:; nothing else is read, and a filename means nothing more.-f FILEreads another file, any YAML, as one of the repository's.scan:belongs in a file of the library folder, and is refused anywhere else. - Documents. A file holds one or more documents, separated by
---; each is a configuration. - Names.
name:is required, but for one configuration in each scope (the repository, the library with itsscan:folders): the one without a name isdefault. Two without, or two of one name, in one scope are refused, naming both files. A configuration nameddefaultis an ordinary one. - Extends. One name, explicit; nothing is implied. It resolves to the repository's configuration,
else the library's, never to itself: a repository's
trustedwithextends: trustedextends the library's. An unknown name is refused, listing the folders scanned and the names found; a cycle, naming it. - Chain. The parent's chain, then the configuration; later wins: settings (
image,state_dir,banner,shell,persistent,attach) are replaced,shareandprivilegesper key,env.defaultsper variable name; in every list, an entry with an earlier one's key (a path; a name for passthrough; the command for hooks) updates it in its place, a new one comes last, andenabled: falseremoves one. A key twice in one list is refused. Paths are made absolute as each file is read, relative to its own folder, so an entry is removed however either file writes it. - Images.
image:follows the chain: a Dockerfile declaringARG BASEbuilds on the image before it; one without it, or an image reference, replaces (Images).ARG BASEwithout a default requires an image before it, and is refused without one;docker build .then needs--build-arg BASE=…;# check=skip=InvalidDefaultArgInFromunder# syntax=silences BuildKit's lint about the missing default. An image holds only what its chain extends. - Default. Without
-c: the repository'sdefault. The library holds none: without one,vz newsaysno default configuration: add vz.yml with `extends: vz-debian-trixie`, or run with -c NAME.
A repository writes the configurations it needs, trusted too:
# vz.yml
name: default
extends: vz-debian-trixie
image:
mounts:
- ~/repos # read-only
---
name: trusted
extends: default # default's lineage, then this
privileges:
share:
A configuration of your own, beside the repository's: whether it is checked in is yours.
# sally.vz.yml
name: sally
extends: default
image: # stacks on default's image
mounts:
- ~/repos/shared-lib:rw # default mounts ~/repos read-only; this one, writable
- ~/.config/gh
# sally.Dockerfile
ARG BASE # the repository's image, passed by vz; this file does not build alone
FROM ${BASE}
RUN apt-get update && apt-get install -y --no-install-recommends ripgrep \
&& rm -rf /var/lib/apt/lists/*
Run it with vz new -c sally, or VZ_CONFIG=sally. --show-effective-config prints the chain, one
line per configuration with its file, each image with its configuration, file and verdict, then the
result as YAML:
# vz-debian-trixie (~/.config/viz-shell/vz-debian-trixie.vz.yml)
# default (vz.yml)
# sally (sally.vz.yml)
# image: ~/.config/viz-shell/vz-debian-trixie.Dockerfile (vz-debian-trixie, ~/.config/viz-shell/vz-debian-trixie.vz.yml)
# image: Dockerfile (default, vz.yml, ARG BASE: required, stacks)
# image: sally.Dockerfile (sally, sally.vz.yml, ARG BASE: required, stacks)
vz configs lists every configuration, the repository's first: its name, file, what it extends, what
it changes.
- The chain is recorded on the container, in the labels
vz.chain(<config>@<file>, fold order) andvz.image(tags, bottom first), and shown on entry asChain:, attached or not, a library configuration marked(library);docker inspecthas it for debugging.
The library. The first vz writes one template when the library has no configuration named
vz-debian-trixie, from templates/, embedded in the binary, and never overwrites a
file: vz-debian-trixie.vz.yml, Debian trixie with sudo, the docker CLI and locales, what vz's
features need, every option written at its default with what flipping it does; and
vz-debian-trixie.Dockerfile, which it builds. A repository extends it by name. Add configurations of
your own beside it, for repositories to extend.
The portability test for a repository's configurations: no host path outside the repository, except
state, which lives inside it. Someone cloning the repository gets its environment; what it extends
from their library is theirs.
Trust: vz runs the configuration it is given; it cannot tell a hostile one, which can name any host
file or share the docker daemon. Review a repository's configuration as you would its code. What
protects the host is what reaches the container: only the repository, and what the configuration
shares, mounts or grants; and, unless privileges.sudo is granted, the secure floor inside it.
Sessions
Every container is named vz-<index>-<repository>, its hostname too, so your prompt says which one
you are in. The index is the lowest free one of the repository's containers; vz new api adds a
name: vz-1-app-api. Labels (vz.repo, vz.index, vz.name, vz.config, …) identify them:
vz ls, vz attach and vz kill look containers up by label, by index or name.
persistent: true # the container outlives the shell that created it; `vz kill` removes it
attach: true # `vz new` without a name joins this repository's container of the same configuration
persistent |
attach |
vz new |
the creating shell exits | the next vz new |
|---|---|---|---|---|
| false | false | a new container | it is removed | another new one |
| true | false | a new container | it is kept | another new one |
| true | true | a new container, or joins the kept one | it is kept | joins it |
| false | true | a new container, or joins the running one | it is removed, with attached shells | joins it |
vz attach [INDEX|NAME] [-- COMMAND](orvz at) runs a shell, or the command, in a container of this repository, as you; without a target, in the only running one. A stopped persistent container is started.attach: truejoins unnamed containers only; a named one is attached by name.- A container is entered only with its own configuration:
vz attach 0to a container started with-c trustedis refused, namingvz -c trusted attach 0.vz newnever lands in a trusted container. - A container keeps the configuration it was created with; attaching after a change warns, and
vz killthenvz newapplies it.vz -- COMMANDjoins asvz newdoes. - Attaching is
docker execof vz's own binary: it waits for the entrypoint to finish setting you up, then becomes you, as the entrypoint does.
Images
image: hello-world # pull
image: # build; paths relative to the configuration file's folder,
# or ~/… under your home, or absolute; ${repo}, ${home} substituted
dockerfile: Dockerfile
context: . # optional, default: the configuration file's folder
args: # optional
- A built image is tagged
vz-<Dockerfile's folder>:<hash of Dockerfile + args>and builds only when missing. One Dockerfile used by many repositories, say from the library'svz-debian-trixie, is one image. Editing the Dockerfile or args rebuilds; editing a copied file does not — remove the image to force it. - Builds run
docker build, via docker-wrapper, so.dockerignoreapplies. - This repository's
Dockerfile: the Rust toolchain, fish as its shell, andvzbuilt from the checkout, stacked on the base (below); alone, on pinned Debian, with what the base adds.
Stacking. A Dockerfile that declares ARG BASE before its first FROM, then FROM ${BASE}, is
built on the image the configurations before it in the chain resolved to: vz pulls or builds that one
first, then passes --build-arg BASE=<its tag>. Images follow the chain, so a later
configuration's Dockerfile lands on top. The base's tag joins the hash: a new base rebuilds what stacks on it. Three
forms: ARG BASE=<default> stacks, and with no earlier image builds alone on its default;
ARG BASE stacks, and with no earlier image is refused, naming the file; no ARG BASE (or BASE
set in args, or an image reference) replaces. --show-effective-config marks each image
ARG BASE: stacks, ARG BASE: required, stacks, ARG BASE: its default or replaces; the banner
shows Image: vz-app:3f9c2a1b (on vz-tools:9a1c0d2e, debian:stable-slim).
What an image needs. Official images like debian, alpine, rust, node or python already
meet the contract. For your own images:
- a shell:
bash, elsesh, or the oneshellnames; - tools under
/usr/localor/opt, not in a home, so the image serves every user andstatemounts in your home cannot shadow them; - no reliance on an
ENTRYPOINT(vz runs its own) or on a baked user (vz adds you); sudo, if a configuration grantsprivileges.sudo.
Base image. The first run writes the library's template, vz-debian-trixie
(templates/vz-debian-trixie.vz.yml and
templates/vz-debian-trixie.Dockerfile, embedded in the
binary); its image: builds the Dockerfile, locally, on first use, as vz-viz-shell:<hash>. It
holds what vz's features need: sudo, the docker CLI, locales, ca-certificates; nothing else. A
repository extends it by name; edit it, or add a configuration of your own beside it with another
image.
How the image is built. apt when Debian's version will do. Otherwise the vendor's release,
downloaded from its URL and verified by sha256, or, for a static binary whose vendor publishes an
image as the way to get it, COPY --from that image. Every FROM is pinned by digest and every
download checksummed.
A repository stacks on it and adds what it needs:
# The image this one builds on, passed by vz; `docker build .` needs --build-arg BASE.
ARG BASE
FROM ${BASE}
RUN apt-get update \
&& apt-get install -y --no-install-recommends git fish \
&& rm -rf /var/lib/apt/lists/*
An opinionated everyday image, with agents, shells and tools, is a repository of your own, built the same way: a configuration of your own in the library for repositories to extend, or beside a repository's own, stacked on its image (Configurations).
Pin a version, never a moving tag: a new base is then an edit to the FROM, which changes the
image's hash, so the repository rebuilds on that branch, and only there.
Your user in the image
Default — added at start. The image needs no user. vz starts the container as root with
itself as entrypoint, which adds your /etc/passwd and /etc/group lines, creates your home,
then becomes you. One image serves everyone; your home starts empty.
Opt-in — baked at build. For tools installed into your home or files owned by you, declare
any of these build args; vz passes your values:
| Arg | Value | Arg | Value | |
|---|---|---|---|---|
VZ_USER |
user name | VZ_GROUP |
group name | |
VZ_UID |
uid | VZ_HOME |
home path | |
VZ_GID |
primary gid |
ARG VZ_USER
ARG VZ_UID
ARG VZ_GID
ARG VZ_GROUP
ARG VZ_HOME
RUN groupadd -g "$VZ_GID" "$VZ_GROUP" \
&& useradd -u "$VZ_UID" -g "$VZ_GID" -d "$VZ_HOME" -m -s /bin/bash "$VZ_USER"
# Later build steps run as you.
USER $VZ_USER
Alpine: addgroup -g "$VZ_GID" "$VZ_GROUP" && adduser -D -u "$VZ_UID" -G "$VZ_GROUP" -h "$VZ_HOME" "$VZ_USER".
- Declared args join the image hash, so such an image is built per user.
- At start, a baked user must match you: name, uid and home; group name and gid. Anything else holding your name, uid or gid is refused, naming it.
- Always pass
-d "$VZ_HOME". On Ubuntu 23.04+,userdel -r ubuntufirst: it holds uid 1000. USERaffects only the build; the container always starts as root for the entrypoint.- Don't set these in a configuration's
args: they would override yours and fail the match.
State
Container paths whose contents survive the container. Each is kept in the state folder, .vz_state/
at the git root by default, at its own container path and mounted back; the host's own files are untouched.
state_dir: .vz_state # optional: the state folder, see below
state:
- ~/.local/share/opencode # a folder
- /var/cache/apt # any absolute path
- # a file, "{}" the first time
| Entry | Inside | Kept at |
|---|---|---|
~/.local/share/opencode |
/home/sally/.local/share/opencode |
.vz_state/home/sally/.local/share/opencode |
/var/cache/apt |
/var/cache/apt |
.vz_state/var/cache/apt |
- Expanded:
{ path, type: dir | file, init, enabled }.initis a file's content whenvzcreates it; never rewritten. state_dir: relative to the configuration file's folder,~/…or absolute. Without it every configuration,-cones included, shares.vz_state/at the git root.vzcreates missing entries as you. Delete the state folder to start over; ignoring it in git is up to you, but keep it out of Docker build contexts:**/.vz_state/in.dockerignore.- A state folder hides what the image had at that path, and belongs to you.
- A
filesuits tools that update in place. One that saves by rename (git config) fails with "Device or resource busy": keep a folder and point the tool into it. - Paths start with
~/or/, without.,..or//, and may not hold or sit inside the repository.
Mounts
Host paths shown inside, reusing the host's own files: at the same path, unless a target names
another. Written as docker's -v: path[:target][:ro|rw]. Mounts are read-only unless :rw.
The default is read-only, vz's secure-by-default posture; a mount says :rw to be writable.
mounts:
- ~/.config/gh:rw # read-write
- ~/repos # read-only
- ~/repos/skills:~/.agents/skills # elsewhere inside
- ~/repos/skills:~/.config/opencode/skills # one source, several targets
The map form says the same, key by key, and adds enabled: false, which removes an entry an
earlier layer added:
mounts:
-
-
- Each path starts with
~/or/; anything else, a mode other thanroorrw, or an empty field is refused, quoting the entry. - The repository
vzruns for is always read-write, even inside a read-only mount like~/repos: deeper mounts land on top. A mount that lands on the repository itself is skipped, so one repository can be read-write for every session, its own included:- ~/repos/notes:rw.VZ_LOG=viz_shell=debugshows the skip. - A mount must exist on the host;
vznever creates one. - A single file mounts too, with two catches: a read-write one breaks tools that save by renaming
over it ("Device or resource busy"), and a running container keeps seeing the old version when
the host replaces the file by renaming, as many editors and
git configdo. Folders have neither. - ~/.sshgives ssh inside your keys,configandknown_hosts, as on the host. The keys are then readable by everything in the container: mount it only where you trust what runs there.- Mounts are keyed by their target, where they land, else their path: a later configuration updates or removes one by it, in either form, and one source may land in several places.
- A mount may land inside a state folder, such as a library inside a tool's persisted config:
vzcreates its mount point in the state folder, as you. It may not hold a state path, sit on one, or lie inside a state file. - Paths follow the state rules.
Share
What of the host the shell shares; nothing unless a layer turns it on.
share:
docker: true # the host's docker daemon
host_network: true # the host's network stack
docker: the socket behind the current docker endpoint (it followsDOCKER_HOSTanddocker context use) is mounted at its own path,DOCKER_HOSTpoints at it, and you join its group: docker works inside as you, without sudo. The image needs the docker CLI.- Sharing the daemon gives the shell root-equivalent control of the host: only for trusted repositories.
host_network:--network host, the host's network stack, itslocalhostand its ports. Without it the shell still reaches the internet, through docker's own network, and the host answers tohost.docker.internal, as in Docker Desktop, but not on itslocalhost. It gives no root, but the shell reaches every service the host does, and its ports can clash.falsein a later configuration turns either off:share: { docker: false }.vzinsidevztalks to the host's daemon, which mounts host paths: run thevzbuilt in the repository (target/…/release/viz-shell); another is refused.
Privileges
What the shell may do inside; nothing beyond the secure floor unless a layer grants it.
privileges:
sudo: true # root through sudo; the image needs sudo
- The secure floor, by default: every Linux capability dropped,
no-new-privilegesset. The container's root processes keepCHOWN,SETUID,SETGIDandKILL: the entrypoint to set you up, the init to pass signals on to your processes. Once the entrypoint becomes you, the shell holds no capabilities, and setuid programs such assudoorsugain nothing. At most 512 processes run, so a runaway or a fork bomb stops there, not at the host's limit. sudo: true: docker's default capabilities, nono-new-privileges, no process limit of its own, and a password-less sudoers line for you. An image without sudo gets a warning, and the shell starts without it.- The library's
trustedgrants it;falsein a later configuration takes it back.
Environment
Variables inside the container, from four sources; later wins:
env:
defaults: # 1. written here: the lowest level
RUST_LOG: info
REPO_ROOT: ${repo} # ${repo} and ${home} are substituted
files: # 2. read on the host, in this order; never mounted
- .env # skipped when missing
- # must exist
passthrough: # 3. copied from the host's environment
- GH_TOKEN
- "FMP_*" # `*` and `?` globs
- Paths are relative to the configuration file's folder,
~/…or absolute. Files use.envsyntax:KEY=value,#comments,export, and quotes around values with spaces. defaultsis a map, keyed by variable name: a later configuration overrides per name,nullremoves one.filesandpassthroughare lists: expanded forms{ path, required, enabled }and{ name, enabled }.- An env file tracked by git is loaded with a warning: its values are in the repository's history.
- Values reach the container by name (
docker create --env NAME, anddocker execwhen attaching), never on a command line or in a log;--show-effective-configand--show-envnever print a value from a file or the host.docker inspectof the container still shows them, as for any container environment. vzsetsHOME,VZ_*,TERM,COLORTERM,LANGand, when docker is shared,DOCKER_HOSTitself; the environment cannot change those.
Hooks
Commands run inside the container before you enter it:
hooks:
create: # once per container, on its first entry
- npm ci
-
attach: # before every entry: each shell, and `vz -- command`
- git fetch --quiet
- Run as you, in the repository root (
$VZ_REPO), throughsh -c, with the session's environment; each list in order. Output goes to the terminal of the entry that runs them. createruns once per container, on its first entry; a stop and start doesn't run it again. An ephemeral container runs it on everyvz: keep hooks idempotent and fast.attachruns before every entry: the first shell, eachvz attach, andvz -- command.- A failing hook fails the entry, naming it and its exit status. A failed
createruns again on the next entry. - Entries arriving while
createruns wait for it. - Keyed by the command: a later configuration adds one, replaces one by the same command, or removes one with
enabled: false. An empty command is refused. - Another shell's syntax goes through it:
fish -c '...'.
To seed a config file once, a state file with init: needs no hook.
Examples
Recipes in examples/: each folder holds its configuration files, <name>/*.vz.yml,
any Dockerfiles they build, and <name>/test.bats, a bats file that
runs the recipe and checks the result, one @test per check; examples/helpers.bash, loaded by each,
sets up a throwaway home, library and repository and removes the containers a file started. Copy a
folder's configuration files and Dockerfiles to your repository root, or try one in place with -f.
just examples runs them all, just examples mounts one. They need docker: run them from inside vz,
in this repository's default configuration, which shares the host's daemon (vz -- just examples).
| Recipe | Shows |
|---|---|
pull-image |
the smallest configuration: one document, no name:, the default |
build-dockerfile |
building from a Dockerfile, with args |
image-stack |
ARG BASE stacking along the chain, on a built image and on a reference; replacing Dockerfiles; deep extending another document's tools; a configuration that extends nothing; images named by their folder |
baked-user |
your user baked into the image, installing into your home |
state |
folders, a file with init, absolute paths |
mounts |
the path[:target][:ro|rw] string form: read-only ~/repos, a read-write config folder, a single file, one source at several targets, a mount inside state, a mount on the repository skipped |
configs |
documents of one file, each extending default: overriding and removing entries; debian12 in a file of its own; trusted with extends: trusted reaching the library's; VZ_CONFIG |
docker |
the host's docker daemon inside, as you; off in another configuration |
env |
every environment source and their order, a later configuration's overrides, values kept out of sight, the TERM fallback |
library |
no default in the library; a library configuration run by name; a repository's nameless default extends: vz-debian-trixie; its trusted on the library's; vz configs; a library filename meaning nothing |
local |
a configuration of your own: sally.vz.yml extending the default, a value, a mount's mode, an added mount, sally.Dockerfile stacked on its image; joe.vz.yml with the banner, its Chain: and Config: |
resolution |
what reading and resolving refuse: an unknown name, a cycle, two names in extends, profiles:, two without name:, one name twice, a nameless one beside a default, ARG BASE with nothing before it, scan: outside the library, one library name in two folders, no default |
privileges |
the secure floor by default, its process limit; sudo in another configuration; an image without sudo |
host-network |
the host's network in another configuration, docker's own by default, host.docker.internal |
shell |
fish as the shell, its configuration as state; a missing shell's fallback |
sessions |
named containers, VZ_CONTAINER, persistent ones, attach by index, name or attach: true, kill |
hooks |
create once per container, across a stop and start, and attach per entry, in order, in the repository root; one removed in another configuration; a failing hook; attach: true, the next vz running attach again; configurations in files of their own extending them |
Logging
Stderr, filtered by VZ_LOG (default warn,viz_shell=info,docker_wrapper=error):
VZ_LOG=viz_shell=debug VZ_LOG=debug
Development
Rust 1.98.1 and the musl target are pinned in rust-toolchain.toml. The binary is static,
because vz mounts itself into every container it starts: build it anywhere, run it on any Linux host.
Inside vz: vz new -c dev, the toolchain with fish and the coding agents, then just build there.
Build first (just build); the example tests run target/.../release/viz-shell, or $VZ.
Each file runs with a throwaway HOME (target/vz-examples/<example>/home), so ~ never touches
yours, and starts with an empty .vz_state/. vz -- just examples runs them inside, in default,
which shares the host's docker daemon; from dev, no: the agents' configuration does not reach it.
The gate, .github/workflows/gate.yml, runs on every pull request and on master: it builds vz on the runner,
then runs the example tests through it, from this repository's default image. Every run proves the glory
path on a clean machine: scaffolding the library, building the base, stacking, the socket share.
Releases are release-plz's: release-plz.toml, .github/workflows/release.yml. Every push to master
updates one release PR, bumped from the conventional commit titles since the last release: feat: a minor,
anything else a patch, ! or BREAKING CHANGE: a major from 1.0 (0.x to 1.0 is a hand edit of
Cargo.toml). Merging it tags v<version>, publishes to crates.io, creates the GitHub release with the
changelog and attaches the static tarball. Pull requests are squash-merged, so their titles must be
conventional commits: Feat/ci gate lands under Other. The maintainer creates two secrets:
RELEASE_PLZ_TOKEN, a PAT, so the release PR runs the gate, and CARGO_REGISTRY_TOKEN.
Issues and pull requests are welcome. Run just test and just examples before sending one.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.