name: uat-env description: This skill should be used when verifying code changes with an in-browser UAT or E2E test in a project worktree — it starts an isolated, per-worktree Apple container environment (app + database + supporting services) on a deterministic localhost port, and tears it down when verification is complete. Trigger phrases include "run a UAT", "verify this fix in the browser", "spin up the test environment", "start the dev server for this worktree", "uat-env". version: 1.0.0 author: Arlen Greer date: 2026-06-12
uat-env — Per-Worktree UAT Environments (Apple Containers)
Purpose
Manage ephemeral, isolated development environments for in-browser UAT cycles using Apple's container CLI. Each git worktree gets its own container stack (app server, database, cache, mail capture) on a deterministic port block, so two branches of the same codebase can run UATs simultaneously with no shared state — no port conflicts, no shared database, no shared dev stack.
All container lifecycle operations go through one deterministic script: ~/.claude/skills/uat-env/scripts/uat-env. Never improvise raw container run commands for UAT environments — the script handles image builds, startup ordering, health checks, service IP wiring, and naming.
When to Use
- Code edits are ready to verify in a real browser (UAT, E2E, visual check)
- A Playwright or /browse session needs a live app instance for THIS worktree
- The user asks to test a fix while another branch's environment may be running
- A clean-baseline environment with fresh seed data is needed
Do NOT use for: full-stack work needing the project's complete docker-compose profile (LocalStack, WireMock, Selenium, pgAdmin) — those projects keep compose for that; this skill covers the minimal UAT subset only.
Prerequisites
- Apple
containerCLI installed (macOS 26+, Apple silicon) and apiserver running (container system start) - The project has a
.uat-env.jsonat its repo root (per-project configuration) - Run
uat-env doctorto verify all prerequisites in one shot
If the current project has no .uat-env.json, offer to create one — see references/configuration.md for the schema and a worked example (SoftTrak).
Core Workflow: the UAT Iteration Loop
SCRIPT=~/.claude/skills/uat-env/scripts/uat-env
# 1. Start (or reuse) this worktree's environment
"$SCRIPT" up # prints: URL: http://127.0.0.1:<port>
# 2. Run the UAT against the printed URL (browser, Playwright, /browse, /qa)
# 3. Edit code on the host — source is bind-mounted; the running container
# sees changes. Re-run the UAT. Do NOT restart containers per iteration.
# 4. When verification is complete:
"$SCRIPT" down
Key behaviors to rely on:
- Per-worktree network isolation (macOS 26+). Each stack runs on its own named container network (
uat-<project>-<worktree-slug>), so containers from different worktree stacks cannot reach each other. On macOS 15 or hosts withoutcontainer networksupport, the script warns and falls back to the shared default network — stacks still work, but are not network-partitioned. Published-port access on127.0.0.1is unchanged either way. upis idempotent. If the stack is already running, it reports status and the URL. Safe to call before every UAT.- The edit→verify loop needs no container restarts. Source directories are bind-mounted. Only restart (
downthenup) if the app process itself must reload something it cannot hot-reload (Gemfile changes, new migrations needingdb:prepare). - Baseline reset (APFS clone):
uat-env snapshotcaptures the seeded db volume as an APFS copy-on-write clone baseline — instant, zero-space.uat-env resetrestores the db volume to that baseline in place (stops→clone-restores→restarts the db container). Baselines are per-worktree (namespaced by PREFIX), stored on the same APFS volume as the container volumes root. Runsnapshotonce after seeding; useresetbetween UAT iterations to return to pristine data instantly. up --freshsmart path: when a baseline exists,--freshrestores it via clone instead of purging volumes (much faster). When no baseline exists, falls back to the unchanged purge+reseed path.- Seeding:
uat-env seedruns the project's configured seed command inside the app container. - Two branches in parallel: run the same commands from each worktree directory. Ports are derived from the worktree path, so each branch gets a stable, distinct URL.
uat-env urlprints it without starting anything.
Command Reference
| Command | Effect |
|---|---|
up [--fresh] |
Build image if needed, start infra→app→workers in order, wait for health, print URL. --fresh: if a baseline exists, restores it via APFS clone (fast); otherwise purges data volumes for full purge+reseed. |
down [--purge] |
Stop and remove this worktree's containers. --purge also deletes data volumes. |
snapshot [svc] |
Capture the current seeded db volume as an instant APFS clone baseline. Stops the db container for a clean Postgres checkpoint, then cp -c clones volume.img. Optional svc overrides the configured .snapshot.service (default: db). |
reset [svc] |
Restore the db volume to the captured baseline via copy-on-write clone (instant; stops→restores→restarts the db container). Errors if no baseline exists — run snapshot first. Optional svc overrides configured default. |
status |
Show this worktree's containers and URL |
url |
Print the app URL (stable per worktree) |
seed |
Run the configured seed command in the app container |
exec SVC CMD… |
Run a command in a service container (e.g. exec backend bundle exec rails c) |
logs SVC |
Follow logs for a service |
doctor |
Verify CLI, apiserver, config, and port availability |
Session Etiquette
- Start the environment lazily — only when a UAT is actually about to run
- Always
downat the end of a UAT session (wrap-up, task completion) unless the user says to keep it running - If
upfails, rundoctorand report its output rather than retrying blindly - Surface the URL to the user whenever the environment starts — they may want to watch the UAT
- First
upin a project builds the image and installs dependencies; warn the user it can take several minutes. Subsequent starts are fast.
Troubleshooting
- App container starts but edits aren't picked up: only affects evented file watchers (inotify does not propagate over the virtiofs mount). Stat/mtime-based watchers work as-is — verified live on SoftTrak (Rails 8 default
FileUpdateChecker, nolistengem): host edits to views and code reload per-request with no polling config. If a project DOES use an evented watcher: for Rails remove thelistengem or setconfig.file_watcher = ActiveSupport::FileUpdateCheckerindevelopment.rb; for Node bundlers setCHOKIDAR_USEPOLLING=1/ Viteserver.watch.usePolling: truein the project's.uat-env.jsonenv block. - Port reported in use: another process owns the derived port —
doctordetects this. Adjustbase_portin.uat-env.jsonif it collides with a fixed service. could not resolve IP: thecontainer inspectJSON schema may have changed in a newer CLI release — inspect manually and updatectr_ip()in the script.- Memory growth over long sessions: Apple containers don't return freed memory to macOS until restarted;
down/uplong-lived stacks periodically. resetsays "no baseline": runuat-env seedto populate data, thenuat-env snapshotto capture the baseline. After that,resetis instant on every subsequent iteration.snapshot/resetrefuses: "not same APFS volume": theBASELINE_ROOTpath (under~/Library/Application Support/com.apple.container/volumes/.uat-baselines) must sit on the same APFS volume as the container volumes root. This guard preventscp -cfrom silently falling back to a slow full copy. If your home directory is on a different volume than/System/Volumes/Data, contact the project maintainer to configure an alternate baseline location.snapshot/resetstop and restart the db container:volume.imgis a block device attached to exactly one container VM at a time. Both commands stop the db container before cloning/restoring and restart it after, so the consuming container is momentarily unavailable during the operation (typically a few seconds).
Bundled Resources
scripts/uat-env— the lifecycle script (single source of truth for container operations)references/configuration.md—.uat-env.jsonschema, placeholder tokens, and the SoftTrak example with porting notes from docker-compose