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
| Asset | Why it matters |
|---|---|
The host: $HOME, ~/.ssh, the login keychain, other repositories, local services | Full 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 key | Spend, usage quota and the developer’s account with the vendor |
| Repository contents and unpushed work | Source code, possibly private, and work not yet anywhere else |
| The supervisor: its database, audit log, sessions and tokens | Control over every agent, and the record of what they did |
| The developer’s attention | Approvals 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- Guest to host. Mounts, the
execchannel, and files the host later reads from an agent-writable checkout. - Guest to network. Only through the proxy sidecar on a per-environment
--internalnetwork (§7.2). - Supervisor to clients. The JSON API and web UI, reachable only on loopback or the VPN (§7.5).
- Supervisor to the forge. API calls and pushes with the App’s tokens; webhooks coming back.
- 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).
| Component | Boundary | Control that checks it | Built and tested in | Residual and unverified |
|---|---|---|---|---|
The supervisor (whr serve) and its SQLite database: every token, session, Decision and the audit log | 3, 1 | The 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/serve | A 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 write | 1 | The 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/gittest | Git’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 check | 1 | Each 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 suite | The 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 code | 2 | Hostname 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-proxy | Upstream to guest (A5), 1 | Pinned and content-addressed, mounted read-only, checked against the recorded hashes at start (D19) | internal/toolstore (pins.json), checkToolStore in internal/serve | A 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 workspace | 3, 1 | sshd trusts only the supervisor’s certificate authority, certificates last minutes and name one session; workspaces read-only by default; git with hooks and fsmonitor off | internal/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 reads | 5, 1 | Only 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 hostgit | internal/devcontainer, internal/oci, internal/hostgit | The 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 key | 4 | 0600 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, ntfy | 3 | The 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
| ID | Source | Typical path |
|---|---|---|
| A1 | A stranger who writes an issue or comment | Prompt injection through text the agent reads |
| A2 | A compromised or misled agent | Anything the guest can reach: mounts, network, files the host reads |
| A3 | A malicious dependency or tool in the environment | Same reach as the agent |
| A4 | Someone on the LAN or holding a lost phone | The supervisor API, sessions, notifications |
| A5 | A compromised upstream: an agent CLI release, a plugin, a CI action | Code that runs with the supervisor’s or the agent’s rights |
| A6 | workharbor itself, used beyond a vendor’s terms | A feature that handles a subscription credential or starts runs without the human |
Threats and controls
| # | Threat | Source | Controls | Enforced and tested in | Status |
|---|---|---|---|---|---|
| T1 | Injected instructions make the agent do something harmful | A1 | Untrusted input is data, never instructions; runs on issues by untrusted authors wait for a Decision; sensitive actions on untrusted input ask (§6, §7.1) | #53 | Partly implemented (untrusted authors held for a Decision; #53) |
| T2 | The guest reads or writes host secrets through a mount | A2, A3 | The 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 |
| T3 | The host runs code planted in an agent checkout (hooks, core.fsmonitor, filters, core.sshCommand) | A2 | The 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), #27 | Implemented 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) |
| T4 | The guest reaches the host, the LAN or other environments | A2, A3 | One --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 #69 | Contradicted for the host’s own addresses: guests reach host services on the LAN address (#69); the pf mitigation is unmeasured |
| T5 | Data leaves through the network | A2, A3 | Default-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, #124 | Implemented 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 |
| T6 | The agent pushes, merges, tags or releases unreviewed code | A1, A2 | The 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, #217 | Partly 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 |
| T7 | An approval is granted by mistake, late or for different code | A1, A2, A4 | Approvals 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 #101 | Implemented 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) |
| T8 | Forge credentials leak | A2, A3 | Each 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, #252 | Implemented: 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 |
| T9 | Secrets end up in logs, transcripts or notifications | A2, A4 | Redaction at ingest, before events, Decision inputs and audit entries are written; generic notification payloads with no content (§5.4, §9.4) | #22, #31 | Implemented: redaction at ingest in the store, generic ntfy payloads |
| T10 | The agent login is stolen from the environment | A2, A3 | Dedicated 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, #82 | Accepted 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 |
| T11 | Someone else drives the supervisor | A4 | The 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, #101 | The 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 |
| T12 | The record of what happened is altered | A2, A5 | Append-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, #66 | Implemented 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) |
| T13 | An agent exhausts the host or the budget | A2 | CPU and memory limits per environment, admission control, time and cost budgets, whr kill-all (§7.4, §8, §5.7) | #37, #48 | Measured limits; budgets planned |
| T14 | A tampered tool or plugin runs in an environment or in the supervisor | A5 | Agent 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 SHA | Tool store (#74); plugins later | Implemented 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 |
| T15 | The developer’s editor runs planted repository config when opening an agent checkout | A2 | The 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, #30 | Implemented in the service; front ends planned |
| T16 | SSH or IDE access into an environment is abused or left open | A4, A5 | Short-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, #92 | Implemented (SSH CA, whr ssh, the console with read-only mounts and the git wrapper; #32, #92), not measured on the real setup |
| T17 | The vendor suspends the developer’s account for use outside its terms | A6 | whr 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, #71 | Decided; the configuration refuses a subscription token (#81); the reading of the terms is ours and unverified |
| T18 | The supervisor’s account has more rights than it needs: an administrator, or the developer’s own account with their keychain, SSH keys and forge logins | A2, A4 | Recommended: 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 | #157 | decided; 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) |
| T19 | A tampered whr binary is installed: a release asset replaced or built outside CI | A5 | The 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, #180 | Implemented (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 |
| T20 | Untrusted 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 them | A1, A2 | Every 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, #223 | Implemented 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) |
| T21 | The bot’s signing key is stolen or misused, so commits look prepared and signed by the supervisor when they were not | A2, A3, A5 | The 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, #253 | Implemented: 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
RUNsteps in Apple Container’s builder VM, which the egress allowlist does not cover;container build1.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-partyinstall.shscripts that run as root in the same builder (D38, #108, #127): only fromghcr.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,--secretor 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 consoleand SSH into the console environment are interactive terminals, and todaywhrpasses 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 login 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 forwhr consoleto 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 ownsshclient carries it.A development installation can be replaced by its own account (D24, D46, issue #261).
make installputswhrin$HOME/.local(or a--prefixof the developer’s), andwhr setup --devandwhr doctor --devaccept 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--devflag on each call, or by thedevelopment_prefixkey in the supervisor’s configuration, whichwhrwrites only on an explicitwhr setup --devand 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$HOMEand from the directory service;dsclby 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$HOMEandPATH. 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, soapi.anthropic.comcan 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
whrnever 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
pfrule 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 buildnetwork option (open). - This page is reviewed whenever a D-row changes a boundary, and before release 1.