cd /news/ai-agents/show-hn-augur-sandboxed-macos-vms-wi… · home › topics › ai-agents › article
[ARTICLE · art-140463] src=github.com ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Show HN: Augur – Sandboxed macOS VMs with Xcode for AI Coding Agents

Developer h1d3mun3 released Augur, an open-source tool that runs Claude Code inside sandboxed macOS virtual machines with full Xcode, xcodebuild and the iOS Simulator preinstalled, plus a lightweight Linux container mode. Augur boots a real macOS guest on Apple's Virtualization.framework, exposes only the current directory, and enforces a host-side egress allowlist by default without sudo; the base VM build takes about 75 minutes from Apple-signed IPSW and Xcode XIP assets, and each project gets its own thin clone with isolated Claude history. The project documents accepted risks such as the .git/hooks host execution risk and the exfiltration ceiling in docs/decisions and docs/security-reviews, and gates releases on a full macOS-VM end-to-end run.

read33 min views1 publishedSep 27, 2026
Show HN: Augur – Sandboxed macOS VMs with Xcode for AI Coding Agents
Image: Michielbdejong (auto-discovered)

Sandboxed macOS VMs with full Xcode for AI coding agents — plus a lightweight Linux container mode.

augur runs Claude Code inside an isolated guest where it can build, run, and test Apple-platform code, while your host stays out of reach. Only the current directory is exposed; network egress is limited to an allowlist enforced on the host.

augur up --macos → augur claude --macos → the agent runs xcodebuild test inside the VM (5× speed)

  • Xcode inside the sandbox — not just Linux. macOS VM mode boots a real macOS guest on Apple's Virtualization.framework, with Xcode,xcodebuild , and the iOS Simulator preinstalled. Most agent sandboxes stop at Linux containers; augur gives the agent the full Apple toolchain without giving it your Mac.
  • xcodebuild test works headless, over SSH. Tests that needtestmanagerd normally require a GUI login session. The base VM is built with auto-login and a virtual display, so the agent can run every test type unattended. SeeRunning xcodebuild test .
  • One command, no GUI on macOS 27. On a macOS 27+ host and guest,augur build --macos provisions the VM's account through macOS 27'sVZMacGuestProvisioningOptions . There is no Setup Assistant to click through, and the admin password is randomized per build. Older pairs fall back to a short manual step. SeeADR-0018 .
  • Built only from Apple-signed assets. The base VM comes from an Apple IPSW plus an Xcode XIP, with no third-party VM images or automation scripts. The VM backend (augur-vm ) is a small Swift CLI that ships in this repo.
  • Egress allowlist, on by default, no sudo. A host-side proxy lets the guest reach only the domains you allow, DNS included. Project-supplied domains need your approval on first sight. SeeEgress allowlist .
  • Per-project isolation. Each project gets its own thin clone of the base VM or its own container. It sees its workspace and only its own Claude history, not the rest of your home directory. Claude auth is injected through the environment, and the host's credential store is never mounted.
  • Honest about its limits. Design decisions and accepted risks, such as the.git/hooks host execution risk and the exfiltration ceiling, are written down indocs/decisions/ and docs/security-reviews/ . Releases are gated on a full macOS-VM E2E run.
git clone -b release https://github.com/h1d3mun3/augur.git && cd augur
brew install go                          # needed for macOS VM egress filtering (augur-gvproxy)
bash install && source ~/.zshrc          # installs augur + builds augur-vm / augur-proxy

augur build --macos --ipsw ~/Downloads/macOS.ipsw --xcode-xip ~/Downloads/Xcode.xip

cd ~/projects/my-ios-app
augur claude --macos                     # Claude Code in an isolated, egress-filtered macOS VM

Don't need Xcode? augur build && augur claude gives you the lightweight Container mode instead.

Container mode macOS VM mode
Best for Web, backend, scripting, anything Linux iOS / macOS / Apple-platform apps
Isolation Linux container (Apple Container) Full macOS VM (Apple Virtualization.framework)
Xcode / xcodebuild / xcodebuild test ✗ ✓
iOS Simulator ✗ ✓ (other platforms via --platforms )
Egress allowlist ✓ ✓
One-time setup ~5 min (image build) ~75 min (base VM build; unattended on macOS 27+)
Per-project start fast (container kept on down ) thin clone of the base VM, kept on down
Disk usage ~2 GB ~70 GB+
Requires Apple Container 1.0.0+ (macOS 26+) augur-vm (bundled), IPSW, Xcode XIP

Workspace rule. augur works in any directory except one that contains augur itself. augur up/ claude/ shell refuse to run in $HOME, in ~/.augur, or in any parent of either, because the workspace is shared read-write and those directories hold the binaries your host runs (see ADR-0014). Use a subdirectory instead.

Both modes share one install. The release branch is the gated stable channel:

git clone -b release https://github.com/h1d3mun3/augur.git
cd augur
brew install go     # macOS VM mode only: needed for egress filtering (augur-gvproxy)
bash install
source ~/.zshrc     # or source ~/.bashrc

The install script copies augur and Dockerfile to ~/.augur/, refreshes the managed egress baseline (~/.augur/augur.conf.default), builds the host egress proxy augur-proxy (needs Swift) and, on macOS, the VM backend augur-vm (needs Swift) and augur-gvproxy (needs Go), and configures PATH. Safe to re-run.

Stable vs. bleeding-edge. Cloning -b release installs the latest release that passed the full macOS-VM E2E gate — augur version then reports a bare X.Y.Z. To follow development instead, clone main (the default branch); augur version reports X.Y.Z-dev+<sha> so you can always tell the two apart. Pin an exact version with git clone --branch vX.Y.Z. See Cutting a release.

augur never mirrors your host ~/.claude — that tree is executable config (hooks, skills, commands), it is full of host-absolute paths that break in a Linux guest, and its permission set was written for a world with no sandbox. The full reasoning is in ADR-0013.

What you get instead is a directory you populate on purpose:

~/.augur/claude-profile/
├── settings.json      → copied     (Claude rewrites it at user scope)
├── CLAUDE.md          → copied     (/memory can edit it)
├── keybindings.json   → copied     (the TUI owns it)
├── commands/          → symlinked  (host edits live in Container mode — see the macOS caveat)
├── skills/            → symlinked
├── rules/             → symlinked
├── output-styles/     → symlinked
├── workflows/         → symlinked
└── themes/            → symlinked

Opt-in and inert by default. No directory, or an empty one, and augur wires nothing. Only names that actually exist in the profile are ever written into the guest — ship nosettings.json and augur never touches yours. #

Host-global, read-only. One profile for every project on the machine: personal tooling is not project-scoped. Read-onlybecause it is shared — a guest able to write there would plant a hook or command for every project. #

In Container mode the directories are live , in two senses. Editing a fileinside an already-wired directory (adding a command tocommands/ that's already there) needs no augur action at all — you're editing the live mount, and the nextaugur claude sees it. Adding/removing/replacing a whole entry (populatingcommands/ for the first time, or deleting it) needs augur to re-run the wiring, which happens oneveryaugur claude /shell /up , not just when the container happens to restart. (Claude Code itself onlyscans a top-levelskills/ /commands/ directory that existed when its session started, so the very first time you populate one, exit and relaunchclaude once — that part isn't augur's to fix.) #

Your own files are never deleted. If the guest already had a real~/.claude/commands or~/.claude/skills when you first populate the profile, augur moves it aside to<name>.pre-profile rather than replacing it (an empty one is simply dropped). #

macOS VM mode reaches the profile over a virtiofs share. The profile works there (it is shared read-only into the VM and wired the same way), but through avirtiofs share rather than a bind mount, on every macOS version. #

macOS VM mode caveat on macOS 26.x — host-side edits need a VM restart. When the host or the guest runs macOS 26.x, a file the guest hasalready read keeps returning the content from that read after the host edits it: the guest's virtiofs client holds on to the cached copy and never re-fetches it. The stale copy comes from the guest's own earlier read, not from anything cached up front at boot — but profile files are read as soon as the guest is wired and Claude Code starts, so in practice a profile edit made while the VM is running is not seen. Runaugur down --macos && augur up --macos to pick it up. Container mode is unaffected.On macOS 27.0+ (host and guest) the defect no longer reproduces , and host-side edits show up in a running guest within about a second. That was confirmed on one machine (M1 Max, build 26A428), not across hardware or 27.x point releases. SeeADR-0019 for what was tested and what it does not establish.On macOS 26.x, three things are worth knowing, because each is the opposite of what seems reasonable:

  • It is not specific to read-only shares. The read-write workspace share behaves identically, so this is not only about the profile — edit a source file on the host and a running guest may keep reading the old bytes.
  • There is no timeout to wait out. One measured case stayed stale for904.9 s with zero natural refreshes; every share then went fresh together 10.3 s after a guest vnode reclaim was forced. Waiting is the one remedy that does not work.
  • down && up works because the guest reboots with an empty vnode cache , not because a freshvm run rebuilds the share device — Virtualization.framework cannot rebuild a share device on a live VM in any case. On macOS 26.x this is a platform defect, and augur accepts it rather than working around it — see ADR-0017 for the measurements, why the mitigation was removed, and what would bring it back. Issues#124 and#135 (closed by ADR-0019).

Your repository's own .claude/settings.json, CLAUDE.md, .claude/commands/, .claude/skills/ and .mcp.json already work with no setup — they arrive inside the workspace mount. The profile is for what a repo cannot supply because it is yours, not the project's.

These apply to both modes.

The read/write workspace mount includes .git — treat it as attacker-controlled. A prompt-injected agent running inside the container or VM can write .git/hooks/pre-commit (or post-checkout, post-merge, …) into the mounted repo. Git runs repo-local hooks with no trust prompt, so the next git command you run on the host in that repo executes guest-authored code at your full host-user privilege — a complete escape of both the container/VM boundary and the egress allowlist. This isn't a bug augur can close in software: it's inherent to mounting a repo read-write so an agent can edit it. Review diffs before trusting them, and treat .git/hooks (and anything else the host later runs unreviewed, e.g. a Makefile or .envrc) in an augur-touched repo as attacker-controlled until inspected. See docs/security-reviews/2026-07-10-egress.md §6 item 12.

Claude Code's --worktree isn't specially supported (augur claude --worktree ... won't forward the flag). If you want it anyway: run augur shell (or augur shell --macos), then type claude --worktree <name> yourself at the prompt — the worktree's files/git state persist fine, and its conversation history survives augur down && up (or augur down --macos && augur up --macos) too. Both modes mount the guest's whole ~/.claude/projects parent to a per-project host dir — ~/.augur/claude-projects/<project>-<path hash> in Container mode, ~/.augur/claude-projects/<vm> in macOS VM mode — so every cwd-keyed leaf under this project (the main checkout and any worktree) is stored host-side and persists (only augur destroy/ augur destroy --macos plus deleting the host history dir removes it). See docs/decisions/0004-no-special-worktree-support.md for the full trade-offs.

Full macOS VM built from an Apple-signed IPSW. Supports Xcode, xcodebuild, and iOS Simulator. The VM is isolated per project — each directory gets its own thin clone of the base VM.

augur ships its own VM backend (augur-vm), a small Swift CLI built directly on Apple's Virtualization.framework — no third-party tools required. Install builds it and installs it into ~/.augur (on the macOS host; needs the Xcode / Swift toolchain, plus Go for augur-gvproxy, the macOS VM egress datapath).

augur build --macos --ipsw ~/Downloads/macOS.ipsw --xcode-xip ~/Downloads/Xcode.xip

This will:

  1. Create a macOS VM from your IPSW (augur-vm create --from-ipsw )
  2. Provision the account — automatically , with no GUI window, when both this host and the IPSW's guest OS are macOS 27+ (a randomly generated password is kept at~/.augur/macos-admin-password , mode 600); otherwise open the VM window for manual Setup Assistant completion (credentials:admin /admin , Remote Login enabled). Pass--manual-setup to take the manual flow even on a macOS 27+ pair. SeeADR-0018 .
  3. Install Xcode, Homebrew, GitHub CLI, and Claude Code
  4. Download the iOS Simulator runtime (Xcode installed from a XIP does not bundle it)
  5. Save the result as a reusable base VM (augur-macos-base )

By default only the iOS Simulator runtime is baked in. Use --platforms to bake in others — baking them into the base VM means every project clone gets them without re-down:

augur build --macos --ipsw ... --xcode-xip ... --platforms iOS,watchOS

augur build --macos --ipsw ... --xcode-xip ... --platforms all

Supply chain note: The base VM is built entirely from Apple-signed assets (IPSW + Xcode XIP). No third-party automation scripts are used.

cd ~/projects/my-app

augur up --macos        # clone base VM and start (first run clones automatically)
augur up --macos --gui  # same, but also open a VM window (display + keyboard + pointer)
augur claude --macos    # launch Claude Code  (starts VM if not running)
augur shell --macos     # open a bash shell   (starts VM if not running)
augur setup-token --macos  # get a Claude subscription token (runs in the VM, saves on the host)
augur down --macos      # stop the VM (keeps the clone — next up is fast)
augur destroy --macos   # stop and remove the project VM clone
augur status --macos    # show VM status, toolchain, and auth info
augur list --macos      # list all VMs and their state
augur update --macos    # update CLI tools in the base VM
augur version --macos   # show augur version (macOS mode)

At most two macOS VMs can run at once. Apple's macOS license permits up to two virtualized macOS instances per Mac, and Virtualization.framework enforces that limit: starting a third running macOS guest fails. That covers every macOS guest on the host, not just augur's. Each project clone counts while it is running, and so does the build VM during augur build --macos / augur update --macos. Stopped clones don't count, so you can keep as many as your disk allows. Run augur down --macos in a project you're not using (or check augur list --macos) to free a slot. Container mode runs Linux guests and isn't affected.

On macOS, Claude Code stores its OAuth login in the Keychain, which is unreadable over SSH and absent from a freshly cloned VM. So macOS mode injects a credential through the environment on every up (the same way it does for the GitHub token):

  • CLAUDE_CODE_OAUTH_TOKEN — subscription token; either set the env var / save it to~/.claude_code_oauth_token , or run**augur setup-token --macos** : it runs claude setup-token inside the guest (so you don't install Claude Code on the host), then you paste the token back once and augur saves it to~/.claude_code_oauth_token .
  • ANTHROPIC_API_KEY — Console API key (env or~/.anthropic_api_key ). Takes priority if both are set.

A cloned macOS guest boots with its wall clock a fixed amount behind the host's (measured at ~95 minutes on one host — a constant inherited from the base VM's saved state, not drift), and it cannot fix itself: NTP is UDP/123 and macOS VM egress drops UDP by design. So augur sets the guest's clock from the host's over SSH — on up --macos (both a fresh boot and a reconcile of an already-running VM) and on claude/ shell --macos, which attach without going through up. It runs before the token is injected, because a token minted on the host seconds ago looks not yet valid to a guest sitting in the past. Best-effort: if it cannot be set you get a warning, not a failed up. See docs/decisions/0015-guest-clock-from-host.md.

Path Description
Current directory exposed at ~/workspace-<project> in the VM (read/write, virtiofs auto-mount)
~/.gitconfig copied on VM start (unlike container mode, which mounts it read-only). augur then rewritescredential.https://github.com.helper inthe guest's copy so HTTPSgit push works offGH_TOKEN ; any helper the host set forgithub.com is replaced, including the pairgh auth setup-git writes, because those need a host path or the host Keychain the guest does not have. Your host file is never modified.
~/.config/gh/ not shared , as in Container mode.gh works off the injectedGH_TOKEN alone; seeAPI keys and authentication for why the host's gh config stays on the host. (A share would also be inert here: it would land under/Volumes/My Shared Files/ and nothing wires it to~/.config/gh inside the VM.)
Claude history only this project's history is shared, in a per-VM isolated dir (~/.augur/claude-projects/<vm> ), so other projects' transcripts stay invisible. History is not shared across modes, so a container session can't be resumed in the macOS VM or vice versa.
Claude auth not shared — injected via env (the macOS Keychain is unreadable over SSH; see above)
Everything else not visible to the VM

The macOS guest auto-mounts the shared directory under /Volumes/My Shared Files/workspace-<project>; augur symlinks it to ~/workspace-<project>. The sealed system volume can't host a symlink at /workspace, so the per-project ~/workspace-<project> path is used in the VM (container mode uses /workspace-<project>).

xcodebuild test needs an Aqua (GUI) login session to reach testmanagerd; a headless SSH login has none. Two things make that session exist at boot: the base VM is built with auto-login enabled, and the VM is always run with a virtual display device (macOS only starts an Aqua session when a framebuffer exists — --no-graphics suppresses only the host-side window, not the display device). With both in place, xcodebuild test works over SSH for all test types. Recommended invocation (SwiftData's @Model macro needs -skipMacroValidation in a headless VM):

NSUnbufferedIO=YES xcodebuild test \
  -scheme <Scheme> \
  -destination 'platform=macOS,arch=arm64' \
  -parallel-testing-enabled NO \
  -skipMacroValidation \
  -derivedDataPath ~/DerivedData \
  CODE_SIGNING_ALLOWED=NO

If builds are flaky from the shared mount (virtiofs is not tuned for heavy I/O — occasional "project is damaged" errors), copy the project to local disk first: rsync -a ~/workspace-<project>/ ~/Developer/<app>/.

augur destroy --macos removes the current project's VM clone — but augur names a clone from its project directory, so if that directory is later renamed or moved, the old clone becomes unreachable by destroy (it's still on disk, just under a name destroy doesn't compute). augur list --macos still shows it; remove it directly:

augur list --macos       # every VM the store knows about, by name — not just this project's
augur-vm stop <name>     # if it's running
augur-vm delete <name>   # remove that VM/clone

Same story for ~/.augur/claude-projects/<vm>/ — a per-clone Claude-history directory that destroy --macos doesn't touch either; it's plain files, safe to rm -rf once you know the clone is gone for good. Both are accepted, documented trade-offs, not oversights — see docs/decisions/0004-no-special-worktree-support.md (§9, "What actually shipped") and docs/decisions/0005-no-prune-command.md.

  • macOS (Apple Silicon)
  • At most two macOS VMs running at once per Mac (Apple's limit, see Concurrent VM limit )
  • Xcode / Swift toolchain (to build the bundled augur-vm backend viabash install )
  • Go (brew install go ), sobash install can buildaugur-gvproxy . Egress filtering is on by default, andaugur up --macos fails closed without it unless you pass--no-egress .
  • macOS IPSW (Apple-signed)
  • Xcode XIP (Apple-signed, from developer.apple.com)

Lightweight Linux container. Suitable for most projects that don't need Xcode.

The container is hosted by Apple Container (container, github.com/apple/container) on macOS 26+ — native to macOS, no Docker Desktop, no licensing; each container runs in its own lightweight Linux VM. augur status shows the active engine.

After Install, build the image:

augur build

Apple Container only: augur build/ augur update implicitly starts a BuildKit "builder" VM (~2 CPU/2GiB) that keeps running after the build finishes, to speed up the next one. It's a single instance shared by every container build on the machine — not scoped to a project — so augur deliberately never stops it for you (doing so from one project's down/ build could kill another project's in-flight build). If you want to free the RAM/CPU, run container builder stop yourself once you're sure nothing else is building.

cd ~/projects/my-app

augur up [--swift VERSION]      # start the container
augur claude                    # launch Claude Code
augur shell                     # open a bash shell (for debugging)
augur setup-token               # get a Claude subscription token (runs in the guest, saves on the host)
augur down                      # stop the container (kept for a fast, cache-preserving restart)
augur destroy                   # stop and remove the container entirely (+ its egress network)
augur status                    # show status, toolchain, and auth info
augur list                      # list all augur containers across projects, with state + address
augur build [--swift VERSION]   # build the container image
augur update [--swift VERSION]  # rebuild image with latest tool versions
augur init-conf                 # scaffold ./.augur/{allowlist,resources}.conf
augur version                   # show augur version
Path Description
Current directory mounted at /workspace-<project> (read/write), named after the directory
~/.claude/projects/-workspace-<project> only this project's Claude history is shared (read/write) — not the rest of~/.claude , so other projects' transcripts and host auth/settings stay invisible
~/.claude/agents/ this project's user-level custom subagent definitions (/agents ) are persisted (read/write), keyed per-project under~/.augur/claude-agents/<project> — so they surviveaugur down /upand destroy /recreate. Isolated per project (not the host's global~/.claude/agents ), so a guest can't plant a subagent read by another project. Project-level.claude/agents/ in the repo work too, via the workspace mount.
~/.augur/claude-profile/ opt-in operator profile, mountedread-only — your personalcommands/ ,skills/ ,rules/ ,output-styles/ ,workflows/ ,themes/ ,CLAUDE.md ,settings.json andkeybindings.json are wired into the guest's~/.claude/ . Absent or empty (the default) wires nothing. SeeOperator profile .
~/.config/gh/ not mounted .gh authenticates with the host's token, injected asGH_TOKENper session like the Claude auth below
~/.gitconfig mounted read-only
Claude auth injected via env ( CLAUDE_CODE_OAUTH_TOKEN /ANTHROPIC_API_KEY )per session : only into theaugur claude /augur shell session, never into the container at creation. The values go through an--env-file pipe, so they are on no command line, not on disk, and not in the container's saved config (container inspect shows none). The host's credential store is never mounted
Everything else not visible to the container — including the rest of~/.claude and the host's~/.claude.json , which augur never reads, copies, or mounts (ADR-0013 )

augur recreates the container for its own reasons — an egress toggle, a memory change, augur build/ update/ install-cert. That discards the writable layer, and your up-arrow prompt history (~/.claude/history.jsonl) lives there rather than on a mount. So augur keeps a small, capped snapshot of just that file under ~/.augur/claude-carryover/<project> (mode 0600) — taken when you exit augur claude/ shell, on augur down, and before build/ update throw the layer away — and restores it into the fresh container.

It carries prompt text only: no credentials, no tool permissions, no trust state. And augur destroy deletes it, so the clean-guest button stays a clean-guest button. Container mode only — the macOS clone already survives down.

augur down stops the container and keeps it (like augur down --macos keeps its VM clone) so the next augur up restarts it fast, preserving the writable layer's caches and tool state that live outside the mounted workspace. augur destroy removes this project's container and its egress network when you're done with it (or to force a clean, from-scratch container). augur update/ augur install-cert rebuild the image, self-prune the previous generation (container image prune), and remove this project's container so the new image takes effect on the next up — other projects on the same image tag need their own augur destroy && augur up. For anything beyond that — stopped containers from projects you're finished with, or reclaiming the shared builder's own resources — use augur destroy per project, or Apple Container's own commands directly. augur list shows every augur container across projects (filtered to the augur- prefix, unlike raw container list), so you can spot finished ones by their project slug first:

container prune              # remove stopped containers
container image prune --all  # remove dangling AND unused tagged base images
container builder stop       # stop the shared BuildKit builder (see note above), or:
container builder delete     # delete it outright — also clears its own build cache

augur deliberately doesn't wrap these in a command of its own — they're already one-liners in Apple's CLI, and disk cleanup beyond the automatic self-prune is rare enough not to carry as a maintained wrapper. container builder delete is machine-wide, not scoped to this project: it aborts any build in flight anywhere else on the Mac. See docs/decisions/0005-no-prune-command.md.

  • Apple Container (container )1.0.0 or newer , on macOS 26+. augur refusesup /claude /shell /setup-token on an older CLI (down ,destroy andlist still work, so you can clean up first).
  • bash

Restrict the container/VM to a set of domains — everything else is blocked. Enforcement runs in a small proxy on the host, as your user — it never needs sudo. Useful for sandboxing an agent so it can only reach the services it should.

On by default. Filtering is always active using a managed baseline (~/.augur/augur.conf.default, shipped with sensible defaults and refreshed on every install). Add your own always-on domains to ~/.augur/augur.conf, or per-project ones to a ./.augur/allowlist.conf in the project root. To disable for one run, pass --no-egress.

cd ~/projects/my-app

augur init-conf   # scaffolds ./.augur/allowlist.conf
cat >> .augur/allowlist.conf <<'EOF'
registry.example.com
api.myservice.com
EOF

augur up            # container, egress on (baseline + augur.conf + allowlist.conf if present)
augur up --macos    # macOS VM, same
augur up --no-egress  # disable egress filtering for this run
augur up --egress     # force egress filtering on (re-enables if AUGUR_EGRESS=0)
augur status        # shows: Egress on/off + the active allowlist

Filtering is on by default; set AUGUR_EGRESS=0 to disable it persistently, or AUGUR_EGRESS=1 to force it on. The --no-egress / --egress flags override that environment variable for a single run.

One pattern per line, # for comments:

Pattern Matches
example.com that exact host (the apex) only
*.example.com subdomains only ( api.example.com , notexample.com )
.example.com the apex and all subdomains

The effective list is three layers merged (union — a layer can only widen, never narrow):

  1. Managed baseline (~/.augur/augur.conf.default ) — shipped defaults for Claude Code / GitHub / Homebrew. augur owns this file andrefreshes it on every install , so shipped domain updates reach you automatically. Don't edit it; your changes are overwritten.
  2. Your global additions (~/.augur/augur.conf ) — always-on domains you add.Never overwritten by install.
  3. Project (./.augur/allowlist.conf ) — per-project domains.

The merge happens on the host, so the guest can't widen its own policy by editing the mounted file. Edits take effect on the next augur up.

Project domains require approval (trust-on-first-use). Because ./.augur/allowlist.conf ships inside a repository you may not fully trust, augur shows the domains it adds and asks you to approve them on first sight and again whenever the file changes — the same model as SSH host keys. The approval fingerprint is stored host-side under ~/.augur/project-hashes/ (never in the project tree or a mounted share), so a compromised guest that rewrites ./.augur/allowlist.conf cannot get the change honored without a fresh host-side approval. augur status shows the project's domains and whether they're approved. For non-interactive / disposable runs (CI), set AUGUR_ACCEPT_PROJECT_CONF=1 to accept automatically; without a TTY and without that variable, an unapproved/changed conf fails closed.

Mode Enforcement
Container The agent runs on a host-only--internal network (internet severed; the host reachable) withNET_ADMIN dropped and--no-dns (external DNS fails closed). Thehost-sideaugur-proxy is its only egress, reached via the host-only gateway. A boot self-test fails closed if direct egress is ever reachable.Trade-off: host-only also lets the agent reach other host services bound to0.0.0.0 ; closing it would need hostpf rules (sudo), which augur avoids.
macOS VM The guest's only NIC is a host-owned socket ( VZFileHandleNetworkDeviceAttachment ); a bundledgvproxy runs the guest's network on the host and funnels every connection to the proxy. Needs no special entitlement.

In every mode the proxy decides by domain (the CONNECT host, or the TLS SNI / HTTP Host) and connects out by name.

install builds the proxy (augur-proxy, Swift) automatically — both container and macOS VM modes run the native host augur-proxy. macOS VM egress also needs Go (for augur-gvproxy) — brew install go, then re-run bash install. Without it, augur up --macos fails closed while egress filtering is on (the default); pass --no-egress to run the VM unfiltered. Container mode does not need Go.

The host ports the proxy uses are derived per-project so two egress-enabled projects can run at once; override with AUGUR_PROXY_HTTP_PORT / AUGUR_PROXY_SOCKS_PORT / AUGUR_SSH_FWD_PORT if needed.

Scope. This guarantees "the guest can only reach allowlisted domains." DNS is gated on the same allowlist (a name resolves only if it's connectable), so the guest can't tunnel data out via DNS queries either. It is still not exfiltration-proof: an allowlisted, writable host (e.g. github.com with your GH_TOKEN) and the shared workspace are intentional channels. This is a deliberate, accepted boundary, not a gap augur intends to close — see docs/decisions/0008-exfiltration-ceiling-accepted.md for why. See also augur-proxy/README.md and gvproxy/README.md.

Both modes size their guest from the same file — MEMORY= for container mode, MACOS_CPU=/ MACOS_MEMORY_MB= for macOS VM mode — so one .augur/resources.conf, committed to the project, covers whichever mode(s) you use.

Apple Container's per-container default memory (~1 GB) is too tight for running an agent, so augur passes --memory 4g by default.

To change the default for a project — and have it apply consistently everywhere you clone or open that project, unlike an environment variable that only exists on the machine where you set it — commit a .augur/resources.conf (augur init-conf scaffolds one alongside the allowlist):

augur init-conf                   # or by hand: mkdir -p .augur
echo "MEMORY=8g" > .augur/resources.conf

Precedence: an AUGUR_CONTAINER_MEMORY environment variable (if set) overrides .augur/resources.conf, which overrides the built-in 4g default. augur status shows the effective value.

The macOS VM defaults to 4 vCPU / 8192 MB regardless of host power. Set MACOS_CPU=/ MACOS_MEMORY_MB= in the same .augur/resources.conf:

augur init-conf                   # or by hand: mkdir -p .augur
cat >> .augur/resources.conf <<'EOF'
MACOS_CPU=8
MACOS_MEMORY_MB=16384
EOF

Precedence: AUGUR_MACOS_VM_CPU/ AUGUR_MACOS_VM_MEMORY_MB environment variables (if set) override .augur/resources.conf, which overrides the built-in 4 vCPU / 8192 MB default. Applied on every augur up --macos — no destroy/re-clone needed. augur status --macos shows the effective values.

Like .augur/allowlist.conf, this file is guest-writable (it lives inside the mounted workspace) — but unlike the allowlist, it carries no approval gate: a guest requesting more or less memory/CPU for itself isn't a containment breach the way widening egress is, so it just takes effect on the next augur up.

Set the key you need. Add to ~/.zshrc:

export ANTHROPIC_API_KEY="sk-ant-..."   # for Claude Code

Alternatively, place the key in a file (~/.anthropic_api_key).

Account-based auth (subscription, no API key needed): augur never mounts the host's Claude credential store into the guest — auth is injected via the environment in both modes. The easiest way to get a long-lived subscription token is augur setup-token, which runs claude setup-token inside the guest (no Claude install on the host) and saves the token for you. Or generate one yourself if you already have Claude Code on the host:

claude setup-token            # prints a token for CLAUDE_CODE_OAUTH_TOKEN
export CLAUDE_CODE_OAUTH_TOKEN="..."   # or save it to ~/.claude_code_oauth_token

augur reads CLAUDE_CODE_OAUTH_TOKEN (env or ~/.claude_code_oauth_token) and injects it into the container/VM. ANTHROPIC_API_KEY takes priority if both are set.

In Container mode, credentials are injected per session: each augur claude / augur shell resolves them afresh, so a rotated key or token takes effect on the next session with no container recreate. A session already running keeps the value it started with. augur setup-token runs with no credentials injected (it exists to mint one), and neither does a container exec you run by hand outside augur. In macOS VM mode they are written to ~/.augur-env when the VM starts.

GitHub CLI:

brew install gh
gh auth login

gh credentials are shared automatically in both modes: the host's gh auth token is injected as GH_TOKEN (per session in Container mode, like the Claude credentials above), and a guest-only git credential helper makes HTTPS git push work off it. Neither mode shares the host's ~/.config/gh: on a macOS host gh keeps the token in the Keychain, so a shared hosts.yml would only make gh auth status show a second, failed login next to the working GH_TOKEN one, and after gh auth login --insecure-storage it would hand the guest a plaintext token. The trade-off is that your gh config (aliases, git_protocol, editor, GHE host entries) does not apply inside the guest.

augur's tests are split by what each layer can prove on a free runner, and by which bugs each layer actually catches.

make unit           # Swift build/test + shellcheck + version smoke              (CI: macos-26)
make offline-tests  # seam + command-construction shell tiers (shimmed engine)   (CI: ubuntu)
make container-e2e  # LOCAL egress FAIL-CLOSED proof on Apple Container (macOS 26+)
make e2e            # LOCAL pre-release gate: macOS VM boot + xcodebuild test (never in CI)

The shell test tiers live in tests/ and run via tests/run.sh (see tests/README.md); each live tier self-skips when its prerequisites are absent, so the same command is safe in CI, the Linux dev container, and on a Mac.

CI runs on free GitHub-hosted runners only, and no CI job boots a VZ guest:

Job Runner What it proves
build-unit macos-26 swift build /swift test the CLIs (augur-vm builds,augur-proxy builds + tests),shellcheck , and a side-effect-freeaugur version smoke. No engine, no VM.
offline-tests ubuntu-latest The seam + command-construction tiers: drive the real cmd_up /cmd_claude against acontainer shim and assert the built argv is byte-identical to the seam's declaration. No engine/VM needed.

Both jobs are secrets-zero — the coding agent is never authenticated in CI. So pull_request runs from forks are safe: there is nothing to exfiltrate.

Why the live E2Es are not in CI. GitHub's arm64 macOS runners are themselves Virtualization.framework guests with no nested virtualization (confirmed by GitHub; the request to enable it was closed as not planned). So anything that boots a VM/microVM — the macOS VM mode or Apple Container mode — cannot run on any GitHub-hosted runner (standard or larger). A bigger runner gives more cores/RAM, not nesting. Those heavy paths — the Apple Container egress fail-closed proof (make container-e2e) and the macOS VM E2E ( make e2e) — are gated locally instead.

Before tagging a release, run the macOS-VM E2E on a real Mac:

make -C /path/to/augur e2e
AUGUR_E2E_PROJECT=/path/to/app AUGUR_E2E_SCHEME=App make -C /path/to/augur e2e

This boots the macOS VM, checks the virtiofs workspace mount and testmanagerd reachability, optionally runs xcodebuild test inside the VM, and re-proves the egress fail-closed guarantee for the VM datapath (the macOS-VM variant of the container-e2e assertions). It's local-only for the nested-virtualization reason above, and because it needs Apple-signed IPSW/XIP that can't live in CI.

The make e2e gate above can't run in CI, so instead of trusting a human to remember it, the release is structurally blocked until it passes. The rationale for these choices (branch model, VERSION-not-tags, linear history off, accepted admin-bypass) lives in ADR-0009; the operator flow is below. The pieces:

  • VERSION (repo root) is the single source of truth for the version number.augur version reads it;tags are the output of a release, never the input. A checkout whose HEAD is exactlyv<VERSION> reports the bare number; any other checkout reports<VERSION>-dev+<sha> .
  • Branches (model B):main is the everyday branch (all existing CI runs here). Therelease branch is gate-passage-only and protected — a commit cannot reach it without a greene2e/macos-vm status (on it, or carried over from a merged-in parent — see the note below the steps).
  • scripts/release-gate.sh runs make e2e on your Mac and posts its result as thee2e/macos-vm commit status. The status is issuedonly on exit 0, so it can't be faked or skipped.
  • .github/workflows/release.yml fires on push to release , readsVERSION ,verifies the e2e/macos-vm status on the tag-target commit (refusing to tag otherwise), and — if no tagv<VERSION> exists yet — creates the annotated tag and a GitHub Release. Bumping nothing, or a follow-up commit, is a safeno-op (collision guard). It boots no VM.

One-time setup (human, admin):

git push origin main:refs/heads/release
gh api -X PUT repos/h1d3mun3/augur/branches/release/protection --input - <<'JSON'
{ "required_status_checks": { "strict": true, "contexts": ["e2e/macos-vm"] },
  "enforce_admins": true, "required_pull_request_reviews": null,
  "restrictions": null, "required_linear_history": false,
  "allow_force_pushes": false, "allow_deletions": false }
JSON

security add-generic-password -U -a "$USER" -s augur-release-gate -w

Releasing:

git checkout main && git pull --ff-only
scripts/release-gate.sh                 # runs `make e2e`; posts e2e/macos-vm=success on green
git fetch origin && git push origin origin/main:release

Branch protection only lets a commit onto release if a green e2e/macos-vm status is satisfied for it, so you can't ship something the E2E never ran against. One subtlety: GitHub carries that satisfaction through merge commits — a merge commit whose merged-in parent has the status is accepted even though the merge commit itself has none. So if you gate the pre-merge bump commit, the tagged merge commit is a different SHA — content-identical for a clean merge, but not literally the tested one. So release.yml re-checks the e2e/macos-vm status on the tag-target commit itself and refuses to tag otherwise — you must gate the post-merge main tip (step 2), or the release fails loudly. (required_linear_history is deliberately off: main uses merge commits, which that rule would reject on the fast-forward.)

git tag vX.Y.Z && git push origin vX.Y.Z bypasses the gate completely: release.yml only fires on push to release (not on tags), and branch protection doesn't cover tag refs — so a manual tag ships without the e2e/macos-vm proof. Worse, it shadows the automated path — the next gated release carrying that VERSION hits the collision guard and no-ops, so that version can never be cut properly. A tag is the gate's output, never something you create by hand. Always release through the release branch (the flow above). (An admin can of course still bypass any protection deliberately; the gate's job is to stop an accidental skip, not a conscious override.)

⚠️ Never hand-cut tags.

── more in #ai-agents 4 stories · sorted by recency
── more on @augur 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/show-hn-augur-sandbo…] indexed:0 read:33min 2026-09-27 · —