Agent sandbox manager — run many parallel agent sessions, each isolated in its own gVisor sandbox on a k8s cluster. Supports Claude Code, Codex CLI, OpenCode, and Pi.
brew trust bsklaroff/yaac
brew trust libkrun/krun
brew tap libkrun/krun
brew install bsklaroff/yaac/yaac
yaac cluster install # cluster + CNI + registry + every image yaac shipsThe formula pulls in the whole toolchain: node, kubectl,
podman (>= 6.0), a pinned kind build (yaac-kind — see the
version-skew note),
and a patched krunkit+libkrun pair (yaac-krunkit/yaac-libkrun —
stock krunkit's virtiofs reports every file as owned by whichever process
asks, which breaks hostPath writes from gVisor worktree pods; see the
machine notes and
#27).
It also installs what the containerless driver
needs on the host — tmux, socat, fd, ripgrep — since that mode runs
worktrees as host processes with no session image to supply anything.
A dev install replaces the brew one — both want to own the same
bin/yaac symlink, so never keep both installed (brew uninstall yaac
first if you have the package; switch back later with
npm uninstall -g @bsklaroff/yaac && brew install bsklaroff/yaac/yaac).
Install the toolchain the formula would otherwise pull in:
brew trust bsklaroff/yaac
brew trust libkrun/krun
brew tap libkrun/krun
brew install node pnpm kubernetes-cli podman bsklaroff/yaac/yaac-kind bsklaroff/yaac/yaac-krunkit
# For the containerless driver (worktrees as host processes, no image):
brew install tmux socat fd ripgrep# Debian/Ubuntu 26.04+. Note: NOT nodejs/npm from apt — Debian's Node is
# built without the TypeScript type-stripper the frontend build needs
# (see below). Install Node via nvm instead.
# libgomp1 is the OpenMP runtime the pinned llama.cpp build (session titles)
# links against; a minimal 26.04 does not ship it. yaac fetches it into its
# own cache if it is missing, so this line only saves it the trip.
sudo apt install podman acl libgomp1
# For the containerless driver (worktrees as host processes, no image):
sudo apt install tmux socat fd-find ripgrep
# Node via nvm: its official builds ship the type-stripper (Node >= 22.18).
# 22.22.2 matches the repo's .nvmrc; pnpm installs into nvm's user-owned
# prefix, so no sudo.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.6/install.sh | bash
export NVM_DIR="$HOME/.nvm" && \. "$NVM_DIR/nvm.sh" # or just open a new shell
nvm install 22.22.2 && nvm alias default 22.22.2
npm install -g pnpm
curl -fsSLo kind "https://kind.sigs.k8s.io/dl/v0.32.0/kind-linux-$(dpkg --print-architecture)"
sudo install -m 755 kind /usr/local/bin/kind && rm kind
curl -fsSLo kubectl "https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/$(dpkg --print-architecture)/kubectl"
sudo install -m 755 kubectl /usr/local/bin/kubectl && rm kubectl
# yaac uses rootful podman on Linux (the calico-node agent needs it); enable the
# socket and grant your user access:
sudo systemctl enable --now podman.socket
sudo setfacl -m u:$USER:x /run/podman
sudo setfacl -m u:$USER:rw /run/podman/podman.sockDon't install Node from apt on Linux: Debian/Ubuntu build their nodejs
package without Node's built-in TypeScript type-stripper (Amaro), which the
frontend's Vite build relies on to load its .ts config. With the apt build,
pnpm build dies at vite build with ERR_NO_TYPESCRIPT /
Unknown file extension ".ts". nvm installs the official Node binaries, which
enable type-stripping by default (Node >= 22.18), so the build works.
The apt-shipped podman 5.x works fine on Linux and pairs with stock kind v0.32.0; only podman 6.x needs the pinned kind build (see the version-skew note). yaac drives the rootful podman engine on Linux — kind's node needs the host netfilter and routing access that rootless podman doesn't delegate, or the calico-node DaemonSet hangs (see Linux: rootful podman).
Give the host swap before yaac cluster install if swapon --show is
empty. Worktree pods run under gVisor, which holds the sandboxed workload's
memory in a memfd — shmem, which the kernel cannot reclaim at all without
swap, so a worktree under pressure is OOM-killed where it would otherwise
have paged out. Create it first: the cluster's kubelet picks up
swapBehavior: LimitedSwap (k8s/kind-config.yaml) only when kind creates
the node, and no later install adds it.
# ext4. On btrfs: sudo btrfs filesystem mkswapfile --size 32G /swapfile
# On ZFS use a zvol, not a swapfile.
sudo fallocate -l 32G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstabA pod's share is memoryRequest / nodeRAM × totalSwap, so sizing swap at
roughly node RAM gives a worktree about its memory request again. If you run
swap deep, check that systemd-oomd (SwapUsedLimit=90% by default) won't
kill the kind node container first.
Then build, link, and wire the cluster:
git clone https://github.com/bsklaroff/yaac.git
cd yaac
pnpm install
pnpm build
npm install -g . # symlinks the checkout — every pnpm build is live
yaac cluster installyaac ships a local web app — a GUI over the same server the CLI drives. Launch it with:
yaac openThis starts the server if needed and opens your browser straight into the
authenticated app: a live worktree sidebar, the project list, and an embedded
terminal (xterm.js) attached to each session's tmux. yaac open --no-browser
prints the URL instead of launching a browser.
It's local-first — the server binds 127.0.0.1 only, and the browser
authenticates with an HttpOnly cookie obtained from a one-time token
that yaac open mints and exchanges for you (no manual pasting). The CLI
and web app drive the same on-disk state, so you can mix them freely.
To reach the server from another device, run it on an always-on machine and
join both to a private Tailscale tailnet. The server
keeps binding 127.0.0.1; a tailscale serve proxy terminates TLS on the
tailnet and forwards to it, so remote access is opt-in and off by default —
never expose it with tailscale funnel. On the server:
tailscale up
tailscale serve --bg 8787 # tailnet-only TLS proxy
export YAAC_ALLOWED_HOSTS=<host>.<tailnet>.ts.net # admit the tailnet host
export YAAC_TRUST_PROXY=1 # trust the proxy's TLS
yaac server restart
yaac auth token create laptop # per-device token (once)Put the two env vars in the server's permanent environment (a systemd unit
or shell profile) and restart it — a detached restart won't inherit an
interactive export. Then browse to https://<host>.<tailnet>.ts.net (the
hostname over HTTPS, not ip:8787), or point a client CLI at it with
yaac remote set https://<host>.<tailnet>.ts.net --token <token>. See
docs/remote-hosting.md for the full flow — client
and phone setup, forwarded-port reachability, and the security model.
A worktree's forwarded ports (a dev server, or an inner yaac's own web UI)
are offered by the server but bound by a client — yaac forward, or the
desktop app, which does it resident in its tray
(docs/port-forward-tunnel.md). To reach them
from other tailnet devices, run yaac forward --bind <the server's tailnet IP> on the server (from tailscale ip -4) and set YAAC_FORWARD_BIND to
the same address so the webapp's port chips link to
http://<host>.<tailnet>.ts.net:<port>. Those listeners are plain http and
reachable by any tailnet device (not yaac-token-gated), so keep this to a
personal tailnet.
The same web app is also available as a macOS Electron shell (@yaac/desktop).
It has no bundled frontend of its own: the main process resolves the target
server (remote if enabled, else the local daemon — starting one if none is
up), mints the same one-time token yaac open does, and loads the server
origin into a native window. It lives in the tray (close hides, Quit stops
only the shell) and badges the dock for waiting worktrees. It is not part of
pnpm build and never ships in the npm artifact.
No extra prerequisites beyond the repo's pnpm install (the electron dev
dependency downloads its binary on install); dev runs also need the yaac
CLI on PATH for local-server and auth-daemon auto-start. Run it from the repo
root:
pnpm desktop:dev # tsup-bundle the main process, then launch electron
pnpm desktop:hot # same, but the window loads Vite for frontend hot-reload
pnpm desktop:build # just build the main-process bundle (dist/main.js)To build a standalone (unsigned) macOS .app:
pnpm desktop:package # build + stage the bundled server → electron-builder (dist-app/)
pnpm desktop:install # the above, then install into /ApplicationsSee packages/desktop/README.md for the boot
and auth flow, packaging internals, and the by-hand verification matrix.
yaac [command]
Commands:
open Open the web app in your browser (starts the server if needed)
cluster Manage the kubernetes cluster yaac runs worktrees on
project Manage projects
worktree Manage worktrees (a git worktree + its container and agents)
config Edit project configuration files (via the server)
auth Manage credentials (GitHub tokens and tool API keys)
remote Point this CLI at a remote yaac server
yaac cluster <command>
check Verify cluster prerequisites (kubectl, registry, hostPath wiring)
install Converge this machine and its cluster to the installed yaac
version: the kind cluster and CNI if there is none, the node
fixups, every built-in image, and the in-cluster layers.
Safe to re-run; never destructive
--nodes <n> Nodes to create (default 1; ignored if a cluster exists)
--adopt-cni Install into a cluster whose CNI yaac did not install
delete [-y] Delete the kind cluster (registry included), keeping
on-disk worktrees and their checkouts (-y skips confirmation)
yaac project <command>
list List all projects
add <remote-url> Add a project (HTTPS URL or SSH URL like git@host:path)
yaac worktree <command>
create [options] <project> Create a new worktree for a project
-t, --tool <tool> Agent tool to use (claude, codex, opencode, or pi)
-b, --branch <branch> Reference branch for the worktree (defaults to
the project's referenceBranch config, else the
remote default branch)
list [options] [project] List running worktrees
-s, --stopped List stopped worktrees (checkouts are kept)
stop <worktree-id> Stop a worktree: tear down its container,
keep its checkout and diff
restart <worktree-id> Restart a worktree, resuming the agent sessions
that were running when it stopped
agents <worktree-id> List the worktree's agent sessions (open first)
attach <container-id> Attach to the agent tmux session
shell <container-id> Open a raw shell in the worktree container
monitor [options] [project] Poll and display running worktrees in real-time
-n, --interval <seconds> Refresh interval in seconds (default: 5)
yaac tool <command>
get Show the current default agent tool
set <tool> Set the default agent tool (claude, codex, opencode, or pi)
yaac config <command>
edit <project> Open the project's yaac-config.json in $EDITOR
edit-dockerfile <project> Open the project's Dockerfile.yaac in $EDITOR
edit-user-dockerfile Open the global ~/.yaac/Dockerfile.user in $EDITOR
yaac auth <command>
list List configured credentials (masked)
update Add or update credentials (GitHub, Claude Code, Codex, OpenCode, or Pi)
clear Remove stored credentials (interactive)
token <command> Durable access tokens for remote clients
create <name> Mint a token (printed once) for a remote client
list List tokens (masked)
revoke <name> Revoke a token by name
server <command> The login broker that runs Claude/Codex sign-ins on this machine
run|start|stop|status
yaac remote <command>
set <url> --token <t> Configure and enable a remote server (verifies the token)
unset Forget the remote (commands target the local server)
on | off Toggle the configured remote without re-entering the token
status Show the configured remote (masked token)
Detach from a tmux session with Ctrl-B D. Kill the tmux session (and the
container) with Ctrl-B K (custom binding, not standard tmux). Open a new
shell in the tmux session with Ctrl-B C, and switch between shells with Ctrl-B N (next) and
Ctrl-B P (previous).
yaac centralizes credentials on the host and injects them into worktree traffic through the shared proxy (a yaac-proxy Deployment in the cluster). Real tokens are never written into the container filesystem. Credentials live under ~/.yaac/.credentials/ (directory permissions 0700, files 0600), split by service:
~/.yaac/.credentials/github.json— GitHub tokens~/.yaac/.credentials/claude.json— Claude Code credentials (OAuth bundle or API key)~/.yaac/.credentials/codex.json— Codex credentials~/.yaac/.credentials/opencode.json— OpenCode credentials (OpenRouter API key)~/.yaac/.credentials/pi.json— Pi credentials (OpenRouter, Anthropic, or OpenAI API key)
A worktree is tool-agnostic — it holds whatever agent sessions you open in it, in any mix — so injection is not scoped to one tool: any agent in any worktree can spend any credential the host has signed in. The proxy only rewrites requests carrying the placeholder sentinel it put in the container's env, so traffic you authenticate yourself passes through untouched.
The proxy pod mounts this directory RW (hostPath) and reads credentials at request time, so updates via yaac auth update propagate to every running worktree immediately without needing to restart pods. The proxy is reachable only inside the cluster (ClusterIP Service); the server talks to it over a loopback exec tunnel (kubectl exec + socat, which works regardless of the pod's runtime tier).
yaac requires one or more GitHub Personal Access Tokens (PATs) for git operations and GitHub API access inside worktree containers. Multiple tokens can be scoped to different owners so you can use separate tokens for different orgs or personal repos.
Tokens are stored as an ordered list. When yaac needs a token for a given repo, it walks the list and uses the first matching entry:
{
"tokens": [
{ "kind": "https", "pattern": "github.com/acme-corp/*", "token": "ghp_org_scoped_token" },
{ "kind": "https", "pattern": "github.com/my-user/private-repo", "token": "ghp_repo_scoped_token" },
{ "kind": "https", "pattern": "gitlab.com/group/sub/*", "token": "glpat_subgroup_token" },
{ "kind": "https", "pattern": "github.com/*", "token": "ghp_fallback_token" }
]
}Each pattern is host-prefixed and takes one of these forms:
<host>/*— matches every repo on<host><host>/<path>— matches a specific repo at<path>(any depth:acme/foo,group/sub/repo, or a single segment likemyrepofor Gerrit-style hosts)<host>/<prefix>/*— matches every repo whose path starts with<prefix>(the prefix itself can span multiple segments, e.g.gitlab.com/group/sub/*)
First match wins, so put more specific patterns before broader ones. On first run, yaac prompts for a token if none are configured.
Tokens are used for:
- Host-side git operations — clone and fetch use HTTPS with the matching token embedded in the request.
- Worktree-side GitHub requests — the MITM proxy injects the token as an
Authorizationheader into all HTTPS requests togithub.comandapi.github.com. The token is never written into the container filesystem. Each worktree uses the single token that matches its project's remote URL.
Token injection only happens over HTTPS. Plain HTTP requests through the proxy never receive credentials.
yaac also manages the API credentials for the agent tool itself, so Claude Code, Codex, OpenCode, and Pi don't need to authenticate inside each container. On first run (or via yaac auth update), yaac runs the tool's native login flow on the host and stores the resulting credentials. OpenCode is API-key only (OpenRouter): the key stays on the host and the proxy swaps the in-container placeholder on requests to openrouter.ai. Pi is likewise API-key only, against a chosen provider (OpenRouter, Anthropic, or OpenAI): the key stays on the host and the proxy swaps the in-container placeholder on requests to that provider's host (as Authorization: Bearer, or x-api-key for Anthropic).
For Claude Code OAuth, each project's .claude/.credentials.json inside the container holds placeholder tokens (yaac-ph-access / yaac-ph-refresh) together with the real expiresAt and scopes. The proxy transparently rewrites outbound API calls, swaps the placeholder refresh token on refresh requests, and writes refreshed bundles back to the host file — so real tokens never enter the container filesystem. For API-key mode the proxy injects the key as an outbound header.
Each worktree runs as a single-pod Kubernetes Job with the following hostPath mounts:
| Host | Container | Description |
|---|---|---|
~/.yaac/projects/<project>/worktrees/<worktree-id> |
/workspace |
Project code (working directory) |
~/.yaac/projects/<project>/repo/.git |
/repo/.git |
Repository metadata |
~/.yaac/projects/<project>/claude/ |
/home/yaac/.claude |
Claude Code configuration |
~/.yaac/projects/<project>/claude.json |
/home/yaac/.claude.json |
Claude Code project settings |
~/.yaac/projects/<project>/codex/ |
/home/yaac/.codex |
Codex configuration and transcripts |
~/.yaac/projects/<project>/opencode-config/ |
/home/yaac/.config/opencode |
OpenCode configuration (shared per project) |
~/.yaac/projects/<project>/opencode-data/<worktree-id> |
/home/yaac/.local/share/opencode |
OpenCode session data (per worktree) |
~/.yaac/projects/<project>/pi-sessions/<worktree-id> |
/home/yaac/.pi/agent/sessions |
Pi session logs (per worktree) |
~/.yaac/projects/<project>/.cached-packages |
/home/yaac/.cached-packages |
Per-project package-manager caches |
The worktree container runs as user yaac with home directory /home/yaac. All project data is stored under ~/.yaac/projects/<repo-name>/ on the host — which is why the cluster node must have your home directory extraMounted (see Cluster setup). The repo plus the Claude and Codex state directories are shared across all worktrees within a project (but isolated between projects), so those worktrees can inspect each other's history; OpenCode and Pi session data are per-worktree (OpenCode to avoid concurrent-write issues in its database, Pi so pi --continue resumes only that worktree's log).
The .cached-packages directory is shared by every worktree within the project, so package-manager caches survive worktree teardown and are reused across worktrees. pnpm's default store-dir is pre-configured to /home/yaac/.cached-packages/pnpm-store, so pnpm install populates the per-project store automatically with no extra configuration.
Per-machine, per-project configuration lives under each project's data dir:
~/.yaac/projects/<repo-name>/config/yaac-config.json
~/.yaac/projects/<repo-name>/config/Dockerfile.yaac
~/.yaac/Dockerfile.user
The easiest way to populate these is in $EDITOR:
yaac config edit <project> # yaac-config.json
yaac config edit-dockerfile <project> # Dockerfile.yaac
yaac config edit-user-dockerfile # ~/.yaac/Dockerfile.user (global)
Example yaac-config.json with all options:
{
"envPassthrough": ["TERM", "LANG"],
"env": {
"NODE_ENV": "development",
"MY_FLAG": "1"
},
"envSecretProxy": {
"MY_API_KEY": {
"hosts": ["api.example.com"],
"header": "x-api-key"
},
"OAUTH_CLIENT_ID": {
"hosts": ["auth.example.com"],
"path": "/oauth/*",
"bodyParam": "client_id"
},
"OAUTH_CLIENT_SECRET": {
"hosts": ["auth.example.com"],
"path": "/oauth/*",
"bodyParam": "client_secret"
}
},
"bindMounts": [
{ "hostPath": "$HOME/datasets", "containerPath": "/mnt/datasets", "mode": "ro" },
{ "hostPath": "$HOME/models", "containerPath": "/mnt/models", "mode": "rw" }
],
"cacheVolumes": {
"pip-cache": "/home/yaac/.cache/pip"
},
"initCommands": ["pnpm install"],
"addAllowedUrls": ["internal.corp.example.com", "*.mycdn.example.com"],
"hideInitPane": false,
"referenceBranch": "develop"
}-
envPassthrough — environment variables passed directly from your host to the container.
-
env — environment variables hardcoded with literal values, baked into the container at worktree creation. Applied after
envPassthrough, so a name listed in both takes the literal value here. Values are not expanded —"$HOME"is passed through as the literal string$HOME. -
envSecretProxy — environment variables injected via a MITM proxy into HTTPS requests. The actual secret value never enters the container. Each entry specifies how the secret is injected:
hosts— hostnames to intercept (required).header— inject as this HTTP header (default:"authorization"). When using the default header, the value is automatically prefixed with"Bearer ". Useprefixto override.bodyParam— instead of a header, replace this form/JSON body parameter. Useful for OAuth client credentials that are sent in POST bodies.path— only inject on matching URL paths (default"/*"). Supports*wildcards.
Each entry must have either
headerorbodyParam(not both).Note: GitHub authentication (
github.comandapi.github.com) is handled automatically using your stored PAT — you do not need to addGITHUB_TOKENtoenvSecretProxy. -
bindMounts — host directories mounted into the container. Each entry specifies:
hostPath— absolute path on the host (required). Environment variables like$HOMEor${HOME}are expanded.containerPath— absolute path inside the container (required).mode—"ro"for read-only or"rw"for read-write (required).
-
cacheVolumes — per-project persistent cache directories mounted into the container. Keys are cache names (backed by
~/.yaac/projects/<project>/cache-volumes/<name>on the host), values are absolute container paths. Caches persist across worktrees. Note: a per-project~/.yaac/projects/<project>/.cached-packagesdirectory is already bind-mounted at/home/yaac/.cached-packageson every container for pnpm (and other package-manager caches you want to share across worktrees), so you don't need acacheVolumesentry for pnpm's store. -
initCommands — commands run inside the container after it starts (e.g.
pnpm installagainst the warm shared cache). These run on every worktree, not just the first. Accepts two shapes (cannot be mixed):- String list — all commands are chained with
&&and run in a single tmux window namedinit, parallel to the agent:"initCommands": ["pnpm install", "pnpm build"]
- Object list — one tmux window per entry, so multiple long-running processes (e.g. a backend and a frontend dev server) run in parallel and can be inspected independently. Each entry has a
name(the tmux window name; must not collide with the agent window —claude/codex/opencode/pi/init/yaacare reserved), acommandsarray (chained with&&inside that window), and an optionalhidePanethat overrides the top-levelhideInitPanefor this window. Windows are spawned independently, so any shared setup (e.g.pnpm install) should be listed in each window that needs it:"initCommands": [ { "name": "backend", "commands": ["pnpm install", "pnpm dev:backend"] }, { "name": "frontend", "commands": ["pnpm install", "pnpm dev:frontend"] } ]
- String list — all commands are chained with
-
hideInitPane — when
true, the init commands tmux pane is automatically closed after the commands finish or error (default:false). Whenfalse, the pane is preserved withremain-on-exitso you can inspect the output. -
addAllowedUrls — additional host patterns to allow on top of the default allowlist. By default, the proxy blocks outbound requests to hosts not on the default list. (How a worktree's traffic reaches the proxy in the first place, and why it fails closed: Worktree egress.) Use this to add extra hosts without replacing the defaults. Supports exact hostnames (
api.example.com) and wildcards (*.example.com). -
setAllowedUrls — completely replaces the default allowlist with the given list of host patterns. Cannot be used together with
addAllowedUrls. Set to["*"]to allow all outbound URLs (disables filtering), or[]to block all external network access. If the resolved list does not includeapi.anthropic.comorgithub.com, a warning is printed since worktrees require these to function. -
nestedContainers — run an in-pod rootless podman so
docker build/docker run/docker compose up --buildwork inside the worktree exactly as a project README instructs (thedockerCLI talks to podman's Docker-API socket). See Nested containers. -
referenceBranch — the branch on
origin(noorigin/prefix) that new worktree worktrees are created from and set upstream to. Unset → the remote's default branch. A per-create pick overrides it:yaac worktree create --branch <branch>, or the branch typeahead in the webapp's new-worktree popover (which can also pin a new default). Changing it affects new worktrees only — existing worktrees keep their base, and prewarmed spares are re-pointed at claim time rather than invalidated.
Every yaac variable is read in one place — packages/shared/src/env.ts — which owns its default and validation. The rest of the codebase imports the typed env / testEnv accessors instead of touching process.env.
| Variable | Default | Description |
|---|---|---|
YAAC_DATA_DIR |
~/.yaac |
Data directory holding projects, worktrees, and the server lock. |
YAAC_SERVER_PORT |
8787 |
Port the server binds on 127.0.0.1 (auto-increments if busy). 0 requests an OS-assigned ephemeral port. |
YAAC_USE_TOR |
false |
Route the server's host-side git/ssh through a Tor SOCKS proxy, and (under the k8s driver) every worktree's egress with it. Under containerless it covers only the server's own git — a host-run worktree has no proxy to route through, and the server says so at startup. Off when unset/empty/0/false; any other value is on. |
YAAC_HOST_TOR_SOCKS_URL |
socks5h://127.0.0.1:9050 |
SOCKS endpoint used when YAAC_USE_TOR is on. |
YAAC_KIND_CLUSTER |
yaac |
Name of the kind cluster yaac cluster install creates and converges. |
YAAC_PREWARM_POOL_SIZE |
1 |
Prewarmed worktrees kept ready per active project (0 disables prewarming). |
YAAC_WORKTREE_ID |
(unset) | Set automatically in every worktree, under both drivers — not something you set yourself. A yaac started inside one reads it as "reachable only through the outer server's port-forward" and skips the client credential (see docs/remote-hosting.md). |
YAAC_ALLOWED_HOSTS |
(unset) | Comma-separated extra hostnames the server's Host-header check admits (e.g. its tailnet name behind tailscale serve). Loopback is always allowed. |
YAAC_TRUST_PROXY |
(unset) | 1 when the server runs behind a trusted TLS-terminating proxy: trusts X-Forwarded-Proto to mark the session cookie Secure. |
YAAC_FORWARD_BIND |
127.0.0.1 |
Address the webapp claims a worktree's forwarded ports are reachable at; a remote-hosting server sets its tailnet IP. The server binds nothing itself — match this with yaac forward --bind <same address> on that machine. |
YAAC_BUNDLED |
(unset) | Set to true by the build (tsup) in the shipped bundle so it loads assets from dist/. Build-time define, not a runtime knob. |
EDITOR / VISUAL |
vi |
Editor opened by the yaac config edit* commands (git's convention: $EDITOR, then $VISUAL, then vi). |
YAAC_UID is a Docker build arg (not a runtime variable) — see Custom images.
These are set by the build or the test harness; production reads several of them only via their defaults.
| Variable | Default | Description |
|---|---|---|
YAAC_K8S_NAMESPACE |
yaac |
Namespace holding every yaac k8s object. E2e runs isolate per-file namespaces here. |
YAAC_IMAGE_PREFIX |
(unset) | Prefix applied to built/pushed image names (test isolation). |
YAAC_PROXY_IMAGE |
yaac-proxy |
Proxy image tag override. |
YAAC_REQUIRE_PREBUILT_IMAGES |
(unset) | 1 fails fast if a required image isn't already in the registry (CI/e2e). |
YAAC_STARTING_GRACE_MS |
60000 |
Grace window (ms) protecting freshly-created worktree pods from the stale-worktree reaper. |
YAAC_BUILD_ID |
(unset) | Override the build id for tests running from source (no dist/.build-id). |
YAAC_SERVER_URL / YAAC_SERVER_SECRET |
(unset) | Point the CLI at an in-process server without the lock file (tests). |
YAAC_E2E_NO_ATTACH |
(unset) | 1 skips the post-provision terminal attach (no-TTY e2e). |
YAAC_E2E_SKIP_FETCH |
(unset) | 1 skips the host-side git fetch during create (e2e fixtures pre-populate the repo). |
YAAC_E2E_CLAUDE_LOGIN / YAAC_E2E_CODEX_LOGIN / YAAC_E2E_OPENCODE_LOGIN / YAAC_E2E_PI_LOGIN |
(unset) | Short-circuit the native tool login with a serialized OAuth bundle (claude/codex) or raw api key (opencode/pi). |
YAAC_E2E_OPENCODE_PROVIDER |
(unset) | Picks the opencode provider during e2e login (defaults to openrouter). |
YAAC_E2E_PI_PROVIDER |
(unset) | Picks the pi provider during e2e login (defaults to openrouter). |
The proxy and relay sidecar containers read their own internal variables (API_PORT, PROXY_AUTH_SECRET, TRANSPARENT_HTTPS_PORT, TRANSPARENT_HTTP_PORT, TRANSPARENT_TUNNEL_PORT, DNS_STUB_PORT, USE_TOR, and the KUBERNETES_SERVICE_* pair). The server and cluster inject these when building each pod spec — they are not user-configurable.
The default image (Ubuntu 24.04 + Node.js + pnpm + Claude Code + gh + tmux) can be customized:
-
Dockerfile.yaac— customizes the base image. Behavior depends on theFROMline:- Layered (recommended) — layers on top of the default image. The default Dockerfile is built first, then Dockerfile.yaac is applied on top. Use this to add packages or config while keeping the standard Ubuntu + Node.js + Claude Code environment. Must use
ARG BASE_IMAGEandFROM ${BASE_IMAGE}so the parent image is injected via--build-arg:ARG BASE_IMAGE FROM ${BASE_IMAGE} # Rest of Dockerfile...
- Any other
FROM— replaces the default image entirely (e.g. use a different base distro or toolchain). Must install Claude Code yourself, since the default Dockerfile is skipped.
Place at
~/.yaac/projects/<repo-name>/config/Dockerfile.yaac, or open it in$EDITORwithyaac config edit-dockerfile <project>. - Layered (recommended) — layers on top of the default image. The default Dockerfile is built first, then Dockerfile.yaac is applied on top. Use this to add packages or config while keeping the standard Ubuntu + Node.js + Claude Code environment. Must use
-
~/.yaac/Dockerfile.user— applied on top of whichever base is used (e.g. nvim config, shell customization). Must useARG BASE_IMAGEandFROM ${BASE_IMAGE}so the parent image is injected via--build-arg:ARG BASE_IMAGE FROM ${BASE_IMAGE} # Rest of Dockerfile...
Layer order: default → Dockerfile.tools (agent CLIs) → Dockerfile.nestable (only when nestedContainers is on) → Dockerfile.yaac (if layered) → Dockerfile.user. A standalone Dockerfile.yaac replaces the default + tools (+ nestable) layers entirely.
nestedContainers: true runs a rootless podman inside the worktree pod and points the docker CLI (and compose) at its Docker-API socket:
docker build/docker run/docker compose up --buildwork as-is. Image pulls ride the worktree's transparent egress to the MITM proxy: the upstream registries (docker.io, ghcr.io, quay.io and their CDNs) are auto-added to the worktree allowlist, and anything else is denied fail-closed. BuildRUNsteps and nested containers automatically trust the proxy CA.- Nested containers share the pod's network namespace: a container's listener is reachable on
localhost:<port>directly (docker run -pis a no-op — the app binds the port itself), and container-private networks are unsupported — usenetwork_mode: hostin compose files. - Built layers are promoted into a per-project shared store at worktree teardown, so an identical
docker buildin the next worktree is a pure cache hit.
Each project gets a plain-HTTP push registry (registry:2) reachable from its worktrees as yaac-reg-<project>.<namespace>.svc:5000, which is the bus those promoted layers ride: a worktree's salvaged images are pushed there at teardown and pulled by the next one. Only the project's own worktrees can reach its registry. Stale content-hash tags accumulate until project removal or cluster recreate (registry:2 has no safe online GC).
Running yaac inside a worktree needs no container feature at all: start the inner server with yaac server start — a host server is the containerless driver — and its own worktrees are tmux servers in the worktree's checkout.