Skip to content
Threat model

Threat model

Status: updated 1 October 2026 for release 1 (issue #11): after the review of main at 26ce0d4 and its fixes (#78, #79), for the vendor terms of subscription logins (D40, T17), on 3 October for signed releases (D24, T19, #180), for the refused IPv6 prefixes (T5, #124, #194), and on 4 October for the trusted computing base (#248). It refines design §7, which stays the list of security rules; this page says what those rules defend against, where each is enforced and tested, and which risks are accepted. A control is measured when a spike or test showed it working, planned when an issue implements it, and open when nothing covers it yet.

Scope and assumptions

workharbor runs on one developer’s Apple-silicon Mac. It drives coding agents (Claude Code first; Codex CLI second, a target not yet built, #35) in Apple Container environments and talks to one forge, GitHub (D15). The developer reaches it from a laptop or phone over a VPN.

  • One trusted human. The developer and the Mac’s macOS account are trusted. Protecting the developer from themselves is out of scope.
  • The agent is not trusted. Its model output follows whatever text reaches it, including issue text written by strangers, so every agent and everything it writes is treated as potentially hostile.
  • The VM boundary holds. Apple Container runs each container in its own lightweight VM (§5.1). A hypervisor escape is out of scope; what the supervisor deliberately exposes to a guest is in scope.
  • Vendors are trusted for what they host. The LLM vendors and GitHub see the code they are sent. Choosing to send it is the developer’s decision, not a threat this page addresses.

Assets

AssetWhy it matters
The host: $HOME, ~/.ssh, the login keychain, other repositories, local servicesFull compromise of the developer’s machine and every account on it
Forge credentials: the GitHub App private key, its installation tokens and the bot’s commit-signing key (D51)Write access to the repositories the App is installed on; commits that look prepared by the supervisor
Agent logins: a subscription login or an API keySpend, usage quota and the developer’s account with the vendor
Repository contents and unpushed workSource code, possibly private, and work not yet anywhere else
The supervisor: its database, audit log, sessions and tokensControl over every agent, and the record of what they did
The developer’s attentionApprovals are only as good as what the developer is shown

Trust boundaries

phone / laptop ──VPN──▶ supervisor (host, trusted) ──▶ GitHub API, ntfy
                          │ exec, mounts
                          ▼
          ┌──── environment (guest VM, untrusted) ─────┐
          │ agent process, agent home volume,          │
          │ bind-mounted per-task checkout (writable)  │
          └───────────────┬────────────────────────────┘
                          │ --internal network only
                          ▼
              proxy sidecar (trusted, full egress) ──▶ LLM API, allowlisted hosts
  1. Guest to host. Mounts, the exec channel, and files the host later reads from an agent-writable checkout.
  2. Guest to network. Only through the proxy sidecar on a per-environment --internal network (§7.2).
  3. Supervisor to clients. The JSON API and web UI, reachable only on loopback or the VPN (§7.5).
  4. Supervisor to the forge. API calls and pushes with the App’s tokens; webhooks coming back.
  5. Untrusted text into the agent. Issue and PR text, comments, CI logs and anything fetched while working.

Trusted computing base

The trusted computing base is what must be right for a boundary above to hold: trusted means that a flaw in it breaks a boundary, not that it is safe. Each component names the boundaries it guards, the control that checks them and where that control is built and tested (issue #248; drawn from main at 1835ca0, nothing newly measured).

ComponentBoundaryControl that checks itBuilt and tested inResidual and unverified
The supervisor (whr serve) and its SQLite database: every token, session, Decision and the audit log3, 1The policy table and the forge Guard; redaction at ingest; state directory 0700, API on a 0600 unix socket, web UI on loopback only (D29)internal/policy, internal/forge (Guard), internal/store (WithRedactor), internal/serveA shared account lets the developer’s other processes drive it (T18, accepted); a tampered binary (T19; the install from a real draft is unverified)
Host git (hostgit): reads and writes repositories an agent can write1The only way the host runs git on agent-writable repositories; agent work arrives as a checked bundle; an isolated git environment (§4.5, D42)internal/hostgit, internal/gittestGit’s own parsing of a crafted bundle is trusted, limited but not sandboxed (Accepted risks)
Runtime tooling: Apple’s container CLI and services, the VM and hypervisor, and the mount check1Each environment its own kernel; mounts refused for the home directory, its parents, ~/.ssh, runtime sockets and anything outside the configured roots; removal by exact ID (§7.4)internal/runtime (CheckMounts, CheckMountsWithin), internal/runtime/apple, the conformance suiteThe hypervisor holds by assumption (Scope); a runtime other than Apple Container has no measured isolation; what Apple’s services do with an exec’s environment is unverified (§7.3)
The proxy sidecar (whr-proxy): the only way out of a guest’s internal network, running no agent code2Hostname allowlist, ports 443 and 80, non-public and host-prefix addresses refused after resolution, every decision logged (§7.2)internal/egress, internal/serve (T5)Real global IPv6 prefixes and the /128 mask are unverified; an image build uses the builder VM’s network (accepted, D38)
Tool-store binaries: the agent CLIs, whr-shim (runs as root in the guest) and whr-proxyUpstream to guest (A5), 1Pinned and content-addressed, mounted read-only, checked against the recorded hashes at start (D19)internal/toolstore (pins.json), checkToolStore in internal/serveA malicious upstream release that was pinned stays trusted: the check proves identity, not behaviour
The console (D43): the human’s shell, no agent, reaching every workspace3, 1sshd trusts only the supervisor’s certificate authority, certificates last minutes and name one session; workspaces read-only by default; git with hooks and fsmonitor offinternal/console, internal/sshca (T16)An SSH session on the real host is unverified; agent-written config and the raw terminal stream (Accepted risks)
Repository images and content: untrusted input the supervisor builds and reads5, 1Only the default branch’s Dockerfile and context are built; features only from the allowed source, pinned by digest, a foreign one asks first (D38); host reads of agent work through hostgitinternal/devcontainer, internal/oci, internal/hostgitThe build has the builder’s network, and a feature’s install.sh runs as root in the builder (accepted, §7.2)
Credentials: the App key, installation tokens, the bot signing key, the agent API key40600 files of the supervisor’s user outside every workspace root; a push token per push over an inherited pipe; every token registered with the redactor (§7.3)internal/config, internal/githubapp, internal/hostgit (push token), internal/redact (T8, T10, T21)An https push to GitHub with the token is unverified until #28; a subscription login is never handled by whr (D40)
Channels: the forwarder (tailscale serve or a proxy), passkeys, ntfy3The forwarder reaches the web UI only, never the API; a user-verified passkey, fresh and bound to the Decision for a sensitive answer; ntfy messages carry only a task ID, a kind and a link (D29, D45)internal/passkey, internal/web, internal/notify (T11)The forwarder and the pf rules, and passkeys on a real phone, are unverified; a public ntfy relay sees the generic message

The console has no threat ID of its own: T16 covers access into it and the accepted risks cover what runs and is shown there.

Threat sources

IDSourceTypical path
A1A stranger who writes an issue or commentPrompt injection through text the agent reads
A2A compromised or misled agentAnything the guest can reach: mounts, network, files the host reads
A3A malicious dependency or tool in the environmentSame reach as the agent
A4Someone on the LAN or holding a lost phoneThe supervisor API, sessions, notifications
A5A compromised upstream: an agent CLI release, a plugin, a CI actionCode that runs with the supervisor’s or the agent’s rights
A6workharbor itself, used beyond a vendor’s termsA feature that handles a subscription credential or starts runs without the human

Threats and controls

#ThreatSourceControlsEnforced and tested inStatus
T1Injected instructions make the agent do something harmfulA1Untrusted input is data, never instructions; runs on issues by untrusted authors wait for a Decision; sensitive actions on untrusted input ask (§6, §7.1)#53Partly implemented (untrusted authors held for a Decision; #53)
T2The guest reads or writes host secrets through a mountA2, A3The adapter rejects $HOME and its parents, secrets directories, sockets and system directories, after resolving symlinks (§4.4, §7.4)#18, #50, #58; the Apple Container adapter mounts only the resolved path (#26)Implemented
T3The host runs code planted in an agent checkout (hooks, core.fsmonitor, filters, core.sshCommand)A2The host never runs git in a workspace (D42). An agent’s commits leave as a git bundle streamed out of the environment and fetched, with fsckObjects and a size cap, into a scratch repository and only then into the supervisor’s own repository; the fetch is the check. Cleanup and push run on that supervisor-owned repository through hostgit (isolated configuration, hooks and fsmonitor off), whose target comes from the forge mirror, never from a workspace. The human’s own repositories are never mounted (§4.5, §7.4)#91, #90 (per-task clones and reading an agent checkout removed), #27Implemented and tested: the bundle import refuses a truncated or corrupt bundle, and nothing planted in a workspace’s .git runs on the host (spike #89)
T4The guest reaches the host, the LAN or other environmentsA2, A3One --internal network per environment blocks the internet, other LAN devices and other environments, but not the host’s own addresses; so supervisor listeners bind to loopback only, a pf rule blocks the container subnets from the host, and host services on all interfaces are turned off or hardened (§7.2, D29)Egress conformance passes on Apple Container (#26); host reachability #69Contradicted for the host’s own addresses: guests reach host services on the LAN address (#69); the pf mitigation is unmeasured
T5Data leaves through the networkA2, A3Default-deny allowlist by hostname in the sidecar, every decision logged; no DNS in the guest; the host’s own global IPv6 prefixes refused like the private ranges (§7.2)#26, #78, #124Implemented and conformance-tested; ports 443 and 80 only, non-public addresses refused after resolution, rebinding closed by dialling the checked address (#78). The host’s global IPv6 prefixes are refused (#124, closed), tested with fake prefixes (TestHostIPv6PrefixesAreRefused) and not run against a sidecar with real global IPv6 unverified; a /128 interface mask (DHCPv6, utun) would refuse only the host’s own address, not its LAN, which is not measured on macOS unverified
T6The agent pushes, merges, tags or releases unreviewed codeA1, A2The agent never pushes (D18); the supervisor pushes after a “Ready to push?” Decision per commit SHA; the policy floor forbids merge, tag, release and deploy; the supervisor never moves the default branch and reads which branch that is from the forge each time, refusing when it cannot (§6, #203); a GitHub ruleset requires human review and lists no bypass for the App (§6, D15)#4, #17, #49, #51, #27, #203, #217Partly implemented: the fresh default-branch read is d00e4ba6659509ade1d27006da5b24f70ed0b64c (#203, tested against a fake GitHub server, not the real one); doctor checks the prototype default-branch ruleset (2c66c0702fdfcae33eac76ae4201f3c052d30ef1, #217); whether GitHub shows a ruleset’s conditions and bypass list to a non-admin App is unverified, and doctor reports “not verified” where they are hidden
T7An approval is granted by mistake, late or for different codeA1, A2, A4Approvals fail closed on timeout and when the channel is lost; they are tied to a commit SHA; input is capped and shown as untrusted; the transport is the agent’s stdio control protocol, with no network path or token in the guest (§4.2, D26). A sensitive answer needs a fresh passkey assertion with Face ID or a fingerprint, bound to the Decision and its commit SHA (D45)#17, #49, #57; transport #7, #25, #75; step-up #101Implemented and tested: domain logic, the stdio transport (crash and deadline cases verified, spike #7), cut input marked, prompt floods denied, step-up for a review and an egress host (#101); untested on a real phone; a a policy or preset change and revoking the forge tokens are answered on the web only behind a named step-up (#107)
T8Forge credentials leakA2, A3Each push gets its own installation token for the one repository with contents: write only, minted by the forge adapter’s Pusher, which only the Guard calls after its approval check; it is registered with the redactor, revoked when the push returns, and reaches git only through a credential helper fed by an inherited pipe, never the guest, an argument, the environment, a URL, a file or a log, and that helper answers only for the configured forge host, with redirects off (§7.3, D15, D51). The App’s private key is a 0600 secret file owned by the supervisor’s user outside every workspace root, which the configuration refuses otherwise; the hour-long installation tokens for the PR, issue and board calls stay in the supervisor’s memory, are registered with the redactor and never reach a guest (§5, §7.3)#27, #22, #252Implemented: the App key, the API tokens and the per-push token (#252, internal/hostgit/pushtoken.go, tested with git ls-remote over loopback http and the redirect and host scoping in TestTheHelperIsScopedToThePushHostAndRedirectsGetNoCredential); an https push to the forge with it is unverified until #28
T9Secrets end up in logs, transcripts or notificationsA2, A4Redaction at ingest, before events, Decision inputs and audit entries are written; generic notification payloads with no content (§5.4, §9.4)#22, #31Implemented: redaction at ingest in the store, generic ntfy payloads
T10The agent login is stolen from the environmentA2, A3Dedicated auth directory per environment, never $HOME; egress allowlist; revocation at the vendor when an environment is deleted (§7.3). A subscription login never touches the host: the human signs in inside the environment, and whr never reads, stores or relays it (D40). An API key is the only agent credential on the host: 0600, single-linked, outside every root, read with O_NOFOLLOW, passed through a pipe at /dev/fd/3, never a file or a command line. Planned with D48 (not built): in api-key mode the key will sit in the sidecar’s gateway and the agent will hold a per-run token revoked at run end; residual: spend up to the budgets while the run lives, and a compromised sidecar.#26, #78, #81, #82Accepted risk inside the environment; the host side is implemented and tested, and a subscription token in the configuration is refused (#81); in-environment sign-in is spike #82
T11Someone else drives the supervisorA4The web UI on loopback only, reached remotely through a forwarder (tailscale serve, or a proxy admitted by pf for a router VPN), and the JSON API only on a host-only unix socket (D29); sign-in has no failure lockout, because a passkey assertion cannot be guessed and a lockout only lets whoever reaches the forwarder keep the human out; sign-in ceremonies have their own pool, separate from step-up and enrolment (which need a session or the host token), and a full pool evicts its oldest ceremony instead of refusing a new one; the begin limit is only a CPU guard far above a human’s rate. A sustained flood can therefore delay a new passkey sign-in while it lasts; signed-in sessions, step-ups and the host CLI are unaffected. Per-client limits would need an identity the forwarder passes on (open) (decided in the review of #101, M1); the host CLI always works; the API token on every request from the local CLI, since a forwarder on a non-loopback address is reachable by guests; on the phone and tablet a passkey with user verification instead of a token, revocable per device by the admin, and a session cookie that is HttpOnly, Secure and same-origin; CSRF protection (§7.5, D29, D45)#24, #30, #33, #69, #101The JSON API is served only on a host-only unix socket and the forwarded listener carries the web UI alone (aacc1cd, D29), closing the gap the review of #101 found. API token implemented (#96); web sessions with CSRF, same-site checks and a strict CSP (#30); passkeys implemented, enrolled and revoked only from the host, and once one exists the token no longer signs in to the web UI (#101), untested on a real phone; loopback measured unreachable from guests (#69), forwarder and pf unmeasured
T12The record of what happened is alteredA2, A5Append-only audit tier in the supervisor’s database, which no environment mounts, with a SHA-256 hash chain over the audit entries (audit_chain) and an index by commit SHA; a transcript purge records itself and leaves the chain alone; the human keeps the chain’s head where the supervisor cannot write, which detects a chain cut off at its end (§5.4, §7.7)#21, #37, #66Implemented and tested, not measured on the reference host: audit tier, redaction before writing and self-recording purges; the hash chain, VerifyAudit and the commit index (659111e0a70d7e1bc631bf2e26fd073699a1d589, 8fc337407712490b2f9d384c6ef8c2b77295017e, #37); the guard bypasses found in review are closed (35ef47102aac1e926b66872aa751d19c91b0edb6, #66)
T13An agent exhausts the host or the budgetA2CPU and memory limits per environment, admission control, time and cost budgets, whr kill-all (§7.4, §8, §5.7)#37, #48Measured limits; budgets planned
T14A tampered tool or plugin runs in an environment or in the supervisorA5Agent CLIs come from a content-addressed, read-only store verified against the vendor’s checksums; plugins run out of process with the same isolation (D19, D20, §7.8); CI actions pinned by SHATool store (#74); plugins laterImplemented for Claude Code (pinned version, https only, size cap, SHA-256 of the stored bytes against the pin and the vendor manifest, read-only store); plugins later
T15The developer’s editor runs planted repository config when opening an agent checkoutA2The editor gets a supervisor-owned copy, cloned from the supervisor’s repository after a bundle export (§4.5), so it is fresh while the environment runs and never the agent’s worktree. A human who opens a workspace’s worktree directly does so at their own risk; the console (D43) is the place to work in workspaces#59, #91; the CLI and UI launch in #24, #30Implemented in the service; front ends planned
T16SSH or IDE access into an environment is abused or left openA4, A5Short-lived per-session certificates, no passwords, reachable only over the VPN; code-server never public; editor extensions treated as a supply-chain risk (§7.6). SSH and whr console land in the console environment, never on the host; only the admin logs in to the host (D43). The console holds no credentials, mounts workspaces read-only by default and runs git with hooks and fsmonitor off#32, #92Implemented (SSH CA, whr ssh, the console with read-only mounts and the git wrapper; #32, #92), not measured on the real setup
T17The vendor suspends the developer’s account for use outside its termsA6whr never handles a subscription credential; one human per supervisor starts every run; forge events only fill the inbox; no scheduled or unattended runs; unmodified vendor binaries; a quota stop pauses with a Decision, no retries or account rotation (D40)#81, #82, #71Decided; the configuration refuses a subscription token (#81); the reading of the terms is ours and unverified
T18The supervisor’s account has more rights than it needs: an administrator, or the developer’s own account with their keychain, SSH keys and forge loginsA2, A4Recommended: a dedicated standard account. Allowed by Werner’s decision (D49): any account but root; whr doctor warns, and fails when remote access is configured; whr setup asks one confirmation; the optional drop-admin step removes admin membership after setup. Guests stay in VMs and the mount check still refuses the home directory, so the account matters once something escapes the supervisor or drives it remotely (T4, T11). Accepted with the account: on a shared account every local process of the developer can read the API token and drive the supervisor (answer Decisions, allow egress), and an administrator or shared account can replace the whr binary in the Homebrew or admin-owned prefix (D24), and a development installation (whr setup --dev, #261) leaves it in a prefix the account itself writes (Accepted risks); remote access counts the host’s Remote Login and Screen Sharing too#157decided; accepted risk for local use. account check, warn status and drop-admin implemented (d54de2a, a519b9c), tested with a fake runner only; dseditgroup, dscl and launchctl outputs and the drop taking effect after a logout unverified (#157)
T19A tampered whr binary is installed: a release asset replaced or built outside CIA5The human’s signed v* tag is checked against .github/release-signers, on main with CI green, before anything is built; GoReleaser builds in CI from that commit with no cache; every artifact’s digest is in checksums.txt and in a keyless Sigstore build-provenance attestation bound to the release workflow, whose bundle is attached to the draft (D24, #180); make install-release checks both, with the workflow identity pinned, into a prefix whr cannot write (a development installation from make install, whr setup --dev, is the exception, Accepted risks); the tap is updated only from a published release#62, #180Implemented (tag check, checksums and attestation, install-release, the attached bundle and the documented user check: 5aac8f5, 8e807ce), not measured: the gh attestation verify flags, the offline check with the bundle and the check on a real draft release are unverified until #180 checks a real draft release
T20Untrusted text forges a line in the supervisor’s log or drives the human’s terminal: a newline or escape sequence in agent output, issue text, a CI log or an error text that quotes themA1, A2Every line the supervisor logs and every untrusted text whr prints for a human has its control characters escaped or replaced (C0 except tab, DEL, C1, bidirectional controls, U+2028 and U+2029); JSON output escapes them by encoding; the web UI renders text escaped (§7.1)#205, #223Implemented through internal/textsafe: the supervisor’s log escapes them (0552770, 2f97853, TestRedactedLogfEscapesControlCharacters); whr replaces them in its error messages, every table cell (whr inbox’s subjects included), whr show, event data (whr logs) and every --json output (textsafe.EscapeJSON), tested by TestCleanStripsControlsAndBidi, TestEventsAndJSONOutputAreInert and textsafe’s table tests (ee141ff); EscapeJSON writes a stray invalid UTF-8 byte as U+FFFD (981a7b8). The console’s terminal stream is outside the rule (Accepted risks)
T21The bot’s signing key is stolen or misused, so commits look prepared and signed by the supervisor when they were notA2, A3, A5The key is an unencrypted SSH ed25519 key in a 0600 file owned by the supervisor’s user, named by bot_signing_key_file, outside every workspace root and never mounted into an environment; only the supervisor’s user reads it (hostgit’s prepare, whr doctor); the configuration refuses a wrong mode or owner and whr doctor checks the key; a missing key fails the prepare closed. The signature is not a control: the per-SHA approval and the forge’s review stay the controls (§7.7, D51)#251, #253Implemented: the configuration, the mode and owner refusal and the doctor check (16f8f39), signing in hostgit.Prepare (#27) and the production path (#253); whether GitHub shows such a signature as verified is unverified

Accepted risks

These are accepted for a single-developer, watched personal tool. Each has a limit, and each is revisited if the tool gets more users.

  • A preview can still leak by navigation (D33, issue #72). The proxy’s CSP stops a preview’s script from fetching or loading images from another site, but a link, a top-level navigation or a DNS prefetch hint (which not every browser’s CSP controls) can still carry data out past the egress allowlist. Limit: only the human opens a preview, it closes with its environment, its session and its device, and the human sees where it navigates.

  • A preview shares the UI’s cookies (D33, issue #72). Browsers do not isolate cookies by port, so a web app the agent runs, previewed on another port of the same host name, can set or overwrite cookies the UI also sees. Limit: the UI’s session and CSRF state are issued and kept on the server and its cookies carry the __Host- prefix, so a tossed cookie can sign the human out but cannot fix a session, forge a request or read one; only the human opens a preview, and it closes with its environment. A separate host name for previews would remove this (open).

  • An image build has the builder’s own network (D38, issue #76). Building an environment from a repository’s Dockerfile runs its RUN steps in Apple Container’s builder VM, which the egress allowlist does not cover; container build 1.5.0 has DNS options but no network option (unverified: from its --help), so it cannot be put behind the sidecar. Devcontainer features add third-party install.sh scripts that run as root in the same builder (D38, #108, #127): only from ghcr.io/devcontainers/features/ or a source the human allowed per repository, each pinned by its manifest digest. Limit: only the default branch’s committed Dockerfile and context, and those pinned features, are built (never a topic or a working tree), no --ssh, --secret or credentials reach the build, its arguments pass the reserved-name check, and its only output is an image tag.

  • The agents of one workspace are one trust domain (D42). They share one environment, its agent home (the login and every agent’s sessions) and the agent clone’s .git, and can change each other’s worktrees, branches, hooks and config; the isolation boundary is the workspace’s environment, not the agent. Limit: a workspace never contains the human’s own repository, the host never runs git in it, and nothing leaves it except a verified bundle whose commits are approved per SHA. Agents that must not touch each other belong in separate workspaces.

  • The console’s terminal stream reaches the human’s terminal as it is (open: accepted or filtered is Werner’s decision, issue #223; D43). whr console and SSH into the console environment are interactive terminals, and today whr passes the guest’s bytes through unchanged (§7.1). The human’s terminal emulator parses them, and it is a host process with the human’s rights, so an agent-written file the human displays (cat, less, git log in the console: routine, not rare) can drive it: set the title, write the host clipboard with OSC 52, read the host clipboard with OSC 52 where the terminal allows it and so move a password or token into a guest that has egress, make the terminal answer with reports (device attributes, a status string) that arrive as typed input, or reach an emulator bug, which is code execution on the host outside every guest. Which terminals allow OSC 52 reads, and which ask first, is unverified. What limits it today: only in a session the human opens, and the console holds no credentials. The cheap mitigation is for whr console to drop OSC 52 and the other OSC, DCS, APC, PM and SOS sequences from the stream, as terminal multiplexers such as tmux can (unverified); plain SSH to the console would stay unfiltered, since the human’s own ssh client carries it.

  • A development installation can be replaced by its own account (D24, D46, issue #261). make install puts whr in $HOME/.local (or a --prefix of the developer’s), and whr setup --dev and whr doctor --dev accept it and install the LaunchAgent with that binary, so anything that runs as the account, an agent’s escape from its VM or a process of the developer’s included, can replace the supervisor’s binary, which the managed prefix of D24 prevents. Limit: only by the explicit --dev flag on each call, or by the development_prefix key in the supervisor’s configuration, which whr writes only on an explicit whr setup --dev and which anything running as the account can write too, but which a managed installation refuses (D24, #276, to be built), never an environment variable or a default; setup and doctor warn that the installation lacks the replacement protection, doctor on every run while the key is set; the binary, the prefix and every directory above it must belong to the running account or root and be closed to group and other writers, the prefix is neither / nor the home directory nor a directory above it (6665a62, e664c8a: the home from $HOME and from the directory service; dscl by its absolute path and a comparison by identity rather than spelling, #278, to be built), and a binary in a git working tree or run as root is refused as in managed mode; no agent authority changes. These checks guard against a mistake, not against the account, which sets its own $HOME and PATH. It is for a developer’s machine: the dogfood host and the reference host keep the managed release installation (D24, D34).

  • The console runs agent-written repository config (D43) when the human runs git or tools there. Limit: it is a VM with no host home, secrets, credentials or runtime socket; workspaces are read-only by default; hooks and fsmonitor are off. Spike #89 measured that hooks and fsmonitor stop through GIT_CONFIG_COUNT, while filter drivers, textconv and aliases set in a workspace’s config run unless overridden by name; the console’s git wrapper overrides each one it finds, and a setting it misses runs inside the console VM only.

  • Data can leave through an allowed host. The sidecar matches on the hostname in CONNECT, so api.anthropic.com can carry data out, and domain fronting is not stopped. Limit: the allowlist is minimal, every request is logged, and the logs are reviewable.

  • The sidecar is trusted and has full egress. Limit: it runs only whr-proxy, mounted read-only into the configured or built-in base image (never a repository’s devcontainer image), with no access to secrets.

  • A subscription login sits inside the environment. Any process in the guest can read it while a run is active. Limit: the human signs in there through the vendor’s own flow and whr never sees the credential (D40), one dedicated auth directory per environment, never the host’s, and revocation at the vendor when the environment is deleted. That vendor revocation works as expected is unverified.

  • Host git reads agent work only as a bundle. Since D42 the host runs no git in a workspace: an agent’s commits arrive as a bundle and are fetched into the supervisor’s repository with object checks (§4.5). What remains is git’s own handling of a crafted bundle and of the commits and trees in it, limited by fsckObjects, git’s path protections, a size cap and a scratch repository that holds nothing else.

  • The agent sees what it is given. Repository contents go to the LLM vendor. That is the developer’s choice per repository.

Open items

  • Guests reach the host’s LAN address and every service on all interfaces (#69): the pf rule blocking container subnets and the hardening of macOS’s own services are not yet measured. T1 is partly implemented: issues from untrusted authors are held for a Decision before any run (#53); T15’s front ends come with #24 and #30. T7’s transport is verified by spike #7, including the crash and deadline cases.
  • Links inside a secrets directory are followed one level and the locations found in review are rejected (#58). Mounts can also be limited to the workspace roots workharbor owns, as a second layer behind the deny-list (CheckMountsWithin). Accepted: a hard link to a secret inside a project, and links more than one level deep inside a secrets directory.
  • Webhook signature verification and the author association used for trust tiers are part of the forge adapter, #27.
  • Whether an image build can be confined to an allowlist, for example through a proxy reachable from the builder VM or a later container build network option (open).
  • This page is reviewed whenever a D-row changes a boundary, and before release 1.