Credentials & the Wire Boundary
An agentic coding session needs credentials — an Anthropic key to talk to Claude, a GitHub token to push, AWS keys to deploy. It also runs arbitrary code, installs arbitrary packages, and follows instructions from files it did not write. Giving such an environment your real secrets is how keys end up in a stealer's inbox.
Bromure Agentic Coding resolves this tension with the wire boundary: the agent inside the VM only ever sees fake placeholder tokens. Your real credentials live encrypted on the macOS host and are substituted onto the wire by a host-side MITM proxy at the last possible moment — after the request has left the VM, and only when it is bound for the host the credential belongs to. No file, environment variable, or process inside the VM ever contains a real API key, OAuth token, AWS secret, or SSH private key.
This chapter explains the mechanism end to end, then walks through every supported credential type and its lifecycle, the per-use approval system, the compromise detector, and how to verify the boundary yourself.
Note: This chapter covers the concepts and runtime behavior. For a field-by-field reference of the settings pane, see Settings → Credentials.
The wire boundary at a glance
Three principles define the model:
- Fakes in the VM. Every credential you configure is replaced inside the VM by a structure-preserving fake. Fakes keep the shape validators expect — an Anthropic fake starts with
sk-ant-api03-brm-, a GitHub fake isghp_plus 36 characters — so tools likeclaude,gh, anddoctlaccept them without complaint. - Reals on the wire only. Every HTTPS request the VM makes is tunneled to the host proxy, which decrypts it, swaps the fake for the real value, and re-encrypts it upstream. The swap is scoped to the destination host the credential was minted for.
- Fail closed. If anything bypasses the proxy, the request carries only a fake and upstream authentication fails. There is no path on which a real secret leaks by accident.
Fakes are deterministic: each one is derived from the real value plus a per-install 32-byte salt via HKDF-SHA256. The same real key always maps to the same fake on your Mac, so clients that fingerprint their key (Claude Code caches a key hash, for example) never see the credential "rotate" between sessions.
| Credential | Fake shape in the VM |
|---|---|
| Anthropic API key | sk-ant-api03-brm-… |
| OpenAI API key | sk-brm-… |
| xAI API key | xai-brm-… |
| GitHub token | ghp_ + 36 characters (40 total) |
| GitLab token | glpat- + 20 characters |
| DigitalOcean PAT | dop_v1_ + hex (64 characters total) |
| Linear API key | lin_api_ + 40 characters (48 total) |
| MCP bearer token | brm-mcp_… |
| Docker registry password | brm-docker-… (at least 40 characters) |
| Kubernetes bearer token | brm-k8s-… |
| Database secret | brm-db-… (at least 32 characters) |
| Generic manual token | brm_… |
There is no on/off switch for the boundary. The proxy is the VM's only egress path; the swap is simply how credentials work in this app.
How the swap works end to end
The full round trip of one authenticated request:
- Session launch — the token plan. When a workspace VM starts, the app builds a per-workspace token plan pairing each real credential with its derived fake. The fakes are written into the VM: environment variables (
ANTHROPIC_API_KEY,GH_TOKEN,LINEAR_API_KEY, …) and config files (~/.git-credentials,~/.docker/config.json,~/.kube/config,~/.aws/config,~/.config/doctl/config.yaml, MCP configs). The real values are loaded into the proxy's in-memory swap map, each keyed to the destination host it belongs to. - The request leaves the VM. The guest's HTTPS traffic is tunneled over a virtio socket (vsock port 8443) to the host proxy. The VM has no other route to the network.
- TLS termination. The proxy presents a forged per-host leaf certificate — the destination hostname as CN and SAN, a per-host EC key — signed by the Bromure Agentic Coding Root CA. Because that CA's public certificate was installed into the VM's trust store at boot, the guest's TLS clients accept the connection, and the proxy can read the plaintext request.
- The swap. The proxy looks up every fake token appearing in the request (headers, and for opted-in credential types the body) and, if the destination host matches the credential's scope, replaces it with the real value.
- Re-encryption upstream. The rewritten request is re-emitted to the real destination via Apple's
URLSession, i.e. macOS's own TLS stack, which validates the upstream server's genuine certificate. The response streams back through the tunnel to the guest.
If the destination does not match a fake's scope, the fake goes out unchanged (and fails upstream auth) — or, when the mismatch looks like exfiltration, the request is blocked outright (see The compromise detector below).
The Bromure Agentic Coding Root CA
The CA is per-install and lives at ~/Library/Application Support/BromureAC/ca/ (cert.pem plus a mode-0600 key.pem). Its subject is "Bromure Agentic Coding Root CA", organization "Bromure". Properties worth knowing:
- CA validity is 10 years. Forged per-host leaf certificates are valid for 1 year and back-dated by 24 hours, so a guest whose clock drifted during suspend/resume still accepts them. Leaves are cached per host for the life of the app process.
- The public certificate is installed into every VM's trust store at boot (delivered through the meta share and applied with
update-ca-certificates). It never leaves your machine and is trusted by nothing except your VMs. - To rotate the CA, quit the app and delete the
ca/directory. A fresh CA is minted on the next launch and each VM picks up the new public certificate at its next boot. Rotation invalidates nothing else — workspace secrets andprofile.jsonmetadata are untouched.
Host scoping
Each swap entry is bound to a host scope. Matching is exact-or-subdomain and case-insensitive, never substring: a credential scoped to openai.com matches api.openai.com but not openai.com.evil.example — a look-alike host cannot trick the proxy into decorating its request with a real key. Manual token rules may deliberately use a blank host filter, which means "inject on any host"; that is an explicit choice you make per entry (see Generic API keys).
Fail-closed by construction
The design never relies on the proxy being unavoidable for secrecy — it relies on the secrets not being there:
- A request that somehow skips the proxy carries a fake token; the upstream API rejects it.
- AWS requests signed inside the VM are signed with a fake secret key; if they reached AWS directly they would fail with
InvalidSignatureException(see AWS below). - SSH private key bytes are never in the VM at all; only signatures cross the boundary.
Managing credentials in the workspace editor
Credentials are configured per workspace. Open the workspace's editor (Edit workspace) and select Credentials in the sidebar.
The pane opens with Git Identity (a name and email written to ~/.gitconfig in the VM; leave both blank to keep git's defaults — the placeholders are just examples, not credentials), followed by a list of only the credentials you have configured, grouped under category headers (Agents, Git, Cloud, Databases, SSH, Other). Two buttons sit at the bottom: Add credential opens a picker of the credential types — Git token, SSH key, AWS credentials, DigitalOcean token, Linear API key, Kubernetes, Container registry, Database, and Other API key — and Import env file… bulk-imports from a .env or ~/.bashrc. The primary agent's own key lives in the Agents pane, described next. Click Save; the next session launch writes the fakes into the VM and loads the reals into the proxy.
The per-credential approval gate (Ask before use) and each service's write policy are set in the Guardrails pane, not here — the Credentials pane no longer carries those controls. See Settings → Credentials for the field-by-field reference.
Six providers are auto-handled — Anthropic, OpenAI, GitHub, GitLab, DigitalOcean, and Kubernetes need no manual token rules; their dedicated sections cover them. Everything else goes through Other API keys.
Importing from an env file. Import env file… reads an existing .env file — or a ~/.bashrc — and turns its variables into credentials. The parser handles KEY=VALUE and export KEY=VALUE, strips surrounding quotes and trailing comments, and deliberately skips anything that would need shell evaluation ($VAR interpolation or $(…) substitution), so it is safe to point at a real .bashrc. A review sheet then shows the variables with masked values: recognized names (ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY, GH_TOKEN/GITHUB_TOKEN, GITLAB_TOKEN, AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN, DIGITALOCEAN_ACCESS_TOKEN, LINEAR_API_KEY) auto-map to their credential type, while unrecognized names can be imported as generic manual tokens with a comma-separated host scope you supply. Already-configured entries are flagged and left unchecked so a re-import never clobbers a secret silently.
Credential types
Primary agent API keys (Anthropic, OpenAI, xAI)
The key for the workspace's primary agent is set in the Agents pane, where each agent card offers an auth-mode selector.
Choose API token and paste the key into the field (Anthropic API key for Claude Code, OpenAI API key for Codex, the xAI key for Grok Build). Lifecycle:
- In the VM: exported as
ANTHROPIC_API_KEY/OPENAI_API_KEY/XAI_API_KEY, holding the provider-shaped fake (sk-ant-api03-brm-…,sk-brm-…,xai-brm-…). - On the wire: swapped to the real key on requests to the provider's hosts (
anthropic.com,openai.com,x.airespectively). - Approval: to gate each session's first use, turn on Ask before use for the agent's key in the Guardrails pane (see Per-use approval). It is off by default.
The other auth modes on the selector — Subscription (interactive login), Bedrock (AWS), and Local model — are covered below and in Local Models. From the CLI, the same choice is --auth token|subscription|bedrock|local on bromure-cli workspaces create (and token|subscription|bedrock on bromure-cli vm run for on-the-fly workspaces).
Claude, Codex, and Grok subscriptions
If you have a Claude, ChatGPT (Codex), or Grok subscription rather than an API key, the agent normally authenticates by interactive OAuth login — a browser flow that a sandboxed VM cannot (and should not) complete with real tokens. Bromure Agentic Coding supports subscriptions with two mechanisms. Both end in the same place: the real OAuth tokens live only on the host, and the guest runs with a bogus key.
Register with Claude / ChatGPT / Grok (recommended)
Set the agent's auth mode to Subscription (interactive login) and click Register…. What happens:
- The app checks that the proxy is running (registration is refused otherwise) and shows an explainer sheet titled Register with Claude (or ChatGPT / Grok). Click Continue.
- A throwaway registration VM boots — temporary, isolated, with no workspace folder mounts and no credential swap map. Inside it, the real login command runs (
claude login,codex login, or the Grok equivalent). - Your Mac's default browser opens the provider's sign-in page. Sign in as usual. You have roughly 4 minutes before the flow times out.
- The resulting OAuth tokens are captured on the host, encrypted, and stored; the throwaway VM is destroyed.
- If you launched registration from a workspace editor, the app asks Share with every workspace? — choose Every workspace to store the subscription as the shared default, or Just this workspace. Registration started from the app's Preferences always stores the shared default without asking.
Thereafter, every session's guest runs in API-key mode with a deterministic bogus credential — a fake ANTHROPIC_API_KEY for Claude, a seeded ~/.codex/auth.json containing a bogus far-future-expiry JWT for Codex (so the client never tries to refresh it itself), a placeholder ~/.grok/auth.json for Grok. The proxy recognizes the bogus value and injects a live Authorization: Bearer access token (plus the OAuth beta header, for Claude) on the wire.
Token custody and refresh are entirely host-side. The host refreshes tokens about 5 minutes before expiry against the provider's token endpoint (platform.claude.com/v1/oauth/token for Claude, auth.openai.com/oauth/token for Codex, auth.x.ai/oauth2/token for Grok), so one refresh serves every running VM and the guest never holds a refresh token. A freshly captured record is deliberately stored with an already-past expiry, forcing a refresh on first use — proving the refresh path works before you depend on it.
The controls next to the auth-mode selector complete the lifecycle: Re-register… repeats the capture (for example after revoking sessions upstream), and Forget deletes the stored subscription from the host.
The in-session token swap
Alternatively, if the agent logs in itself inside the VM (you run claude login in the session), the proxy notices a real subscription OAuth token heading out to the provider and offers to take custody of it. A sheet appears — Swap Claude subscription token? (or Codex) — explaining that the real token can stay on this Mac while a fake replaces it in the VM's credentials file. Both the access and refresh tokens are swapped together. Your options:
| Button | Effect |
|---|---|
| Swap | The real tokens move to the host store; the VM's credentials file is rewritten with fakes; the proxy injects the real token on the wire from now on. |
| Not now | Nothing changes this session; the prompt returns next session. |
| Never for this workspace | Stops the prompts for this workspace. |
The per-workspace state behind this prompt is visible in the editor as Claude subscription token swap / Codex subscription token swap (unset by default; "accepted" or "declined" after you choose). The host-to-VM swap channel is deliberately one-directional: the host can only write fakes into the VM, and the VM can only send reals out to the host — there is no RPC by which the VM can ask for a real token back. The in-VM agent additionally refuses to write any credential that does not carry the brm- fake prefix, so even a misbehaving host cannot corrupt the guest's credentials file with a real value.
Note: Codex remains in subscription mode inside the guest (its API-key mode targets a different backend), and Grok's tokens travel via the file
~/.grok/auth.jsonrather than a vsock agent. These are implementation details; the custody model is identical.
AWS
AWS gets the deepest treatment of any provider, because AWS requests are not authenticated by a bearer token — they are signed (SigV4). Open Credentials → AWS in the workspace editor; a segmented control chooses between Static keys and SSO / Identity Center.
Static keys and the host resigner
Paste your Access key ID and Secret access key (plus an optional STS Session token and a Default region). The lifecycle:
- In the VM:
~/.aws/configpoints at a credential_process helper that vends — over vsock port 8445 — the real Access key ID paired with a 40-character fake Secret access key, omitting the session token. Every AWS SDK, theawsCLI, terraform, and boto3 pick this up natively; no tool-specific setup is needed. - What the guest does: signs its requests with the fake secret, producing a syntactically valid but cryptographically doomed SigV4 signature.
- On the wire: the host's AWS resigner detects requests bound for
*.amazonaws.comand*.amazonaws.com.cn(GovCloud, ISO, regional, and bucket-style S3 hosts included), strips the guest's signature, injects the realX-Amz-Security-Tokenwhen a session token is present, and re-signs with the real secret before the request leaves your Mac. Each successful resign emits acredential.aws_signaudit event with a masked access key, visible in Tracing.
This is fail-closed in the strongest sense: the real secret key never exists in the VM in any form, and a request that bypasses the proxy is rejected by AWS with InvalidSignatureException.
Three request styles are not supported and return a clear error to the guest rather than a silent failure: chunked streaming S3 uploads (STREAMING-AWS4-HMAC-SHA256-PAYLOAD, answered with 501), SigV4A asymmetric signing, and presigned query-string URLs (which embed the signature where the resigner cannot replace it).
SSO / IAM Identity Center
If your organization uses IAM Identity Center, select SSO / Identity Center instead of pasting long-lived keys:
- Click Grant access to ~/.aws and approve the folder access prompt (a security-scoped grant; the folder is read on the host and never mounted into the VM).
- Pick your profile from the SSO profile picker. The app discovers
[profile …]sections in~/.aws/configthat carrysso_start_url,sso_account_id, andsso_role_name, resolving[sso-session …]references; account ID and role are shown read-only. - On session start, the host resolves temporary role credentials from the SSO token cache (
~/.aws/sso/cache). If the cached token has expired, your browser opens foraws sso login— run on the host, from/usr/local/bin/aws.
The resolved temporary keys feed the same resigner as static keys; the VM still never sees a secret. Credentials auto-refresh on the host about 5 minutes before they expire.
Bedrock
Bedrock (AWS) on the agent's auth-mode selector runs Claude Code against AWS Bedrock using whatever AWS credentials the workspace has configured (static or SSO) — it is an auth mode of the primary tool, not a separate credential. Set the auth mode, configure Credentials → AWS, and optionally set a Bedrock model ID on the workspace. Signing goes through the same resigner path, since Bedrock endpoints are *.amazonaws.com hosts.
Git HTTPS tokens (GitHub, GitLab, Bitbucket, self-hosted)
Personal access tokens for git-over-HTTPS live under GitHub Tokens / GitLab Tokens / Bitbucket Tokens (collectively, HTTPS tokens). Each entry takes a host, a username, and the token; self-hosted GitLab or Gitea instances work by setting the host field. Lifecycle:
- In the VM: a fake is written into
~/.git-credentialsand thegh/glabconfiguration;GH_TOKENandGITLAB_TOKENare exported so the CLIs authenticate automatically. Fake shapes match each vendor's validators (ghp_+ 36 for GitHub,glpat-+ 20 for GitLab,brm_…otherwise), so prefix-and-length checks inghandglabpass. - On the wire: swapped to the real token on requests to that entry's git host.
- Approval: each entry gets its own Ask before use row in the Guardrails pane.
If you only need git push over SSH, you may not need an HTTPS token at all — see SSH keys below.
Generic API keys (Other API keys / Manual token rules)
For any API the app does not auto-handle, add an entry under Other API keys (the manual token rules, also surfaced as MITM token swap). Click Add token and provide:
| Field | Meaning |
|---|---|
| Name | A label for the entry. |
| Value | The real secret (stored encrypted on the host). |
| Env var name | The variable the fake is exported under inside the VM. Reference it from your code. |
| Host filter (optional) | The host scope for the swap. |
Two edge cases are deliberate:
- Empty env var name: nothing is exported; you copy the fake from the session's welcome banner and place it wherever your tooling needs it.
- Blank host filter: the real value is injected on any host, without asking. That is an explicit always-on choice — useful for APIs with many regional hostnames — but it means the credential's scope no longer protects it. To gate an unscoped secret, turn on Ask before use for it in the Guardrails pane (or give it a host filter after all).
Anthropic, OpenAI, GitHub, GitLab, DigitalOcean, and Kubernetes never need manual entries here.
1Password secret references (op://)
Instead of pasting a real secret, an Other API key value can be a 1Password secret reference — op://vault/item/field, or the brace form {{ op://vault/item/field }} that op inject and .env.op files use. Only the reference is ever stored on disk; the secret itself is never written anywhere on the host or the VM. The editor recognises the reference, shows it in the clear (it isn't a secret), and notes that it resolves through 1Password.
At each workspace launch Bromure resolves the reference host-side with the 1Password CLI (op read), mints a fake from the reference string (so the fake stays stable even when the secret rotates), and exports only that fake into the VM. The MITM proxy then swaps the fake for the resolved value on the wire, exactly as it does for a pasted secret — and it re-resolves every 2 minutes, so rotating the item in 1Password takes effect within a couple of minutes without a reboot.
This needs the op CLI installed and signed in — biometric unlock via the 1Password app, or op signin in a terminal. If op isn't found, the editor shows a Get 1Password CLI link and the workspace surfaces install instructions on launch; if resolution fails (not signed in, or the item doesn't exist), Bromure surfaces the error so you can fix it and restart the workspace.
Importing a .env or .env.op file (Import from .env… in the editor) recognises op:// values automatically — under recognised names like ANTHROPIC_API_KEY or arbitrary ones like STRIPE_KEY — and stores each as a manual token holding the reference.
DigitalOcean
Paste a personal access token under Credentials → DigitalOcean (the Open DigitalOcean token page in your browser link takes you to token generation). The fake (dop_v1_ + hex, 64 characters) is exported as DIGITALOCEAN_ACCESS_TOKEN and written into ~/.config/doctl/config.yaml, so doctl auth init is unnecessary. The swap covers requests to digitalocean.com, plus a second entry for the base64 Basic-auth blob used by docker login / doctl registry login against registry.digitalocean.com, so registry pushes and pulls resolve too.
Linear
Paste a personal API key under Credentials → Linear (link: Open Linear API settings in your browser; keys come from linear.app → Settings → API). The fake (lin_api_ + 40) is exported as LINEAR_API_KEY, which the Linear SDK, MCP servers, and CLI tools pick up automatically; the swap is scoped strictly to linear.app (GraphQL at api.linear.app and MCP at mcp.linear.app). A Linear key on the workspace is also the prerequisite for Linear-issue automation triggers — see Automation & CLI.
MCP server bearer tokens
HTTP-transport MCP servers configured in the MCP pane get the same treatment: the real bearer token stays on the host, a brm-mcp_… fake is injected into the MCP config the agent reads, and the proxy swaps it on the wire, scoped to the server's host. Only enabled HTTP-transport servers with a non-empty bearer token receive a swap entry.
One extra courtesy: for hosts with a brm-mcp_ entry, the proxy answers the OAuth/OIDC discovery paths (.well-known authorization-server, protected-resource, and OpenID configuration endpoints) with 404. Claude Code then treats the server as pre-authenticated instead of attempting its own OAuth flow — which could never complete inside the VM anyway.
Container registries (Docker Hub, GHCR, and friends)
Credentials → Container Registries manages HTTP Basic auth for docker pull/push. Presets exist for Docker Hub (docker.io), GitHub Container Registry (ghcr.io), and GitLab Container Registry (registry.gitlab.com); any other registry can be added by host. Lifecycle:
- In the VM:
~/.docker/config.jsoncontains a fake auth blob — the base64 of your username paired with a derivedbrm-docker-…password — so Docker believes it is logged in. - On the wire: the proxy substitutes the real base64 blob at the matching registry host. The distribution-spec token dance is handled too: swap entries are added for known auth realms (Docker Hub authenticates against
auth.docker.io, DigitalOcean's registry againstapi.digitalocean.com). - Import: click Import config.json… to pull entries from an existing
~/.docker/config.jsonon the host. Entries delegated tocredsStore/credHelpersare skipped — their passwords live in the OS keychain, not the file — and the import summary reports how many were skipped and why. - Approval: per registry, via Ask before use in the Guardrails pane. Remove this registry deletes an entry.
Kubernetes contexts
Credentials → Kubernetes turns kubeconfig contexts into proxy-mediated cluster access. Click Import kubeconfig to parse an existing kubeconfig into one row per context (the current-context first, auth type auto-detected), or Add context to enter a server URL, credentials, namespace, and cluster CA manually. In every case the VM receives a synthetic ~/.kube/config: kubectl in the guest talks to the proxy (trusted via the Bromure CA), never to the API server directly.
Per auth type:
- Bearer token contexts get a
brm-k8s-…placeholder in the VM, swapped to the real token on the wire. - Client certificate contexts get a throwaway self-signed certificate and key in the VM; the real certificate and key are registered on the host as a
SecIdentityand used for upstream mTLS. - Exec plugin contexts never run the plugin in the VM at all. The exec-plugin poller runs the command on the host every refresh interval (default 600 seconds, floor 60), parses the
ExecCredentialJSON, and feeds the fresh token into the swap map. Refreshes preserve the entry's consent state.
If the kubeconfig supplies the cluster's own CA, it is forwarded so the proxy can verify the upstream API server. File-path fields in an imported kubeconfig (CA, cert, key) are read eagerly at import time — later changes to those files on disk are not picked up. Each context gets its own Ask before use row in the Guardrails pane, where a write policy can additionally strip destructive verbs (kubectl delete, AWS Delete*/Terminate*, docker push, git push) host-side before a byte is forwarded — a separate layer from the credential swap.
HTTP databases (MongoDB, ClickHouse, Elasticsearch)
Credentials → Databases (the MongoDB / ClickHouse / Elasticsearch sections) covers HTTPS-speaking database endpoints — the Mongo Data API, ClickHouse's HTTP interface, Elastic. Each endpoint takes an engine, host, secret, username, auth type, and one or more env var names (comma-separated) under which the brm-db-… fake is exported; reference those variables from your code or connection strings.
Database credentials are the one family where the swap sweeps the request body as well as headers and query parameters, because connection secrets routinely ride inside JSON or SQL payloads. The proxy patches Content-Length after a body swap; unrelated multipart or binary bodies of other traffic are never touched (all other swaps are header-scoped). Basic-auth endpoints also get their base64 user-and-secret blob swapped. Per-endpoint Ask before use and the write policy are set in the Guardrails pane.
SSH keys
SSH is handled without any token at all: the VM's SSH_AUTH_SOCK is backed by an ssh-agent bridge over vsock (port 8444). The guest can list identities and request signatures, but the private key bytes live only on the host and physically cannot be read or extracted from inside the VM. Two key sources exist, both under Credentials → SSH Keys:
- The per-workspace default key. A shared default ed25519 keypair is generated at app startup (stored under
default-ssh/in Application Support) and copied into each new workspace's agent directory. The pane shows the workspace's public key so you can paste it into your git host (for GitHub: github.com/settings/keys). You can also mint a fresh key from the CLI withbromure-cli workspaces ssh-keygen <workspace-id|name>, which prints the new public key. - Imported keys. Click Import… / Import file… under Imported SSH keys and point the app at an existing private key file — RSA, ed25519, and ECDSA are supported, encrypted keys included. An encrypted key prompts for its passphrase once at import; the passphrase is stored in the macOS Keychain (service
io.bromure.agentic-coding.ssh-key-passphrases) and supplied viaSSH_ASKPASSat launch, never logged. Imported-key signing flows through a private ssh-agent process the app spawns for itself (ssh-agent -Don a socket under the temporary directory) — separate from anything else on your system.
Per imported key, turning on Ask before use in the Guardrails pane makes every in-VM signature request prompt for consent. Every signature — default or imported key — emits a credential.ssh_sign audit event carrying the key's SHA256 fingerprint and whether it was a managed or imported key.
Note: Your macOS login ssh-agent (the launchd one) is deliberately never exposed to the VM. Only per-workspace keys and keys you explicitly imported are reachable from a session.
Per-use approval and grant durations
Any credential in this chapter can be flagged Ask before use — a per-credential checkbox in the Guardrails pane (labelled Require approval to use on the control itself), off by default, described in the UI as: "Pop a confirmation dialog the first time this credential is used in a session."
When a gated credential is first used in a session, the proxy holds the request and shows a consent dialog titled Allow “workspace name” to use credential? (with your workspace's name and the credential's label filled in), offering four buttons:
| Button | Grant |
|---|---|
| Allow for 5 minutes | Time-bounded; the credential flows without further prompts for 5 minutes. |
| Allow for 1 hour | Time-bounded, 1 hour. |
| Allow for the rest of the session | Until the session window closes. |
| Don't allow | The request is refused; the denial is remembered for 5 minutes so an agent retry-storm doesn't produce a dialog storm. |
Behavior worth knowing:
- Coalescing. Concurrent requests needing the same credential collapse onto one dialog — a chatty agent firing twelve parallel API calls produces one prompt, and all twelve follow your decision.
- Deny wins. A live denial short-circuits before any older allow is consulted.
- Ephemeral by design. Grants and denials are held in memory only and are revoked at session teardown. "Rest of the session" never survives a window close, and nothing is persisted across app runs.
- Headless sessions. In an SSH or headless session the prompt appears in the workspace's tmux rather than as a GUI alert — see Remote Access.
For AWS, the gate applies to every host-side signing call; for SSH keys, to signature requests; for everything else, to the first wire swap of the session.
Tip: The consent gate is per credential, not per host. If you want an unscoped manual token to remain under control, approval is the mechanism designed for it.
The Credential Approvals window
Window → Credential Approvals… opens a live view of every consent decision made during the current app run: time-bounded allows (5 minutes / 1 hour / rest of session) and remembered denials. Each row shows the workspace name and time remaining, and offers Revoke; Revoke all (⌘⌫) clears everything at once. The list auto-refreshes every 2 seconds and shrinks naturally as grants expire.
Because decisions are in-memory only, the window is empty after grants expire, after sessions close (session-scoped grants), and after the app quits. It is a control panel for the present, not an audit log — for history, use Tracing.
The compromise detector
The fakes do double duty: besides standing in for secrets, they are tripwires. A fake token has exactly one legitimate destination family — the host scope it was minted for. There is no honest reason for sk-ant-api03-brm-… to appear in a request to pastebin.example. So the proxy scans every outbound request — headers and body, via an Aho-Corasick automaton, cheap enough to run on everything — for any fake headed outside its scope.
When it finds one, it treats the request as attempted credential exfiltration:
- The request is blocked with HTTP 451. Not a single byte is forwarded to the destination.
- The VM is paused immediately.
- An alert fires: "Bromure detected an outbound attempt to leak a session credential to a host it was not minted for. The VM has been paused."
The workspace is then marked compromised — the workspace browser shows Compromised — launching will prompt to wipe disk and home — and the next launch requires remediation: "To continue, the VM disk image and the persistent home folder must be wiped. Your tokens, ssh keys, and workspace settings are preserved." Click Wipe and Launch to proceed. Heed the accompanying warning: shared folders are NOT wiped, so if the compromise came through a package or file in a shared folder, it may still be there — review those folders before resuming work.
Recommended response to a compromise alert: open the Trace Inspector (⇧⌘I) or run bromure-cli trace leaks to see the offending host and request, decide whether a project dependency or a prompt-injected instruction was responsible (see Prompt Injection and Supply Chain), then wipe and relaunch.
Scope details:
- Unscoped fakes are exempt. A manual token with a blank host filter is "any host" by design and cannot trip the detector; bogus subscription placeholders are likewise excluded.
- First-party siblings are tolerated. Matching mirrors the swap's scope policy, with one relaxation: hosts under the same registered domain as the credential's provider (Claude or Codex infrastructure under
anthropic.com, for example) use a family match, so a legitimateapi.anthropic.comtoken seen onmcp-tools.anthropic.comis not a false alarm. Everything else is strict. - Real-looking leaks are flagged too. Independently of the fake tripwires, the proxy flags unswapped, real-looking secrets in outbound traffic —
Bearerorx-api-keyvalues with known prefixes (sk-ant-,ghp_,AKIA, …) or opaque tokens of 20 characters or more. These appear as leak warnings in the Trace Inspector and inbromure-cli trace leaks; they usually mean a real secret was pasted into the VM by hand, which the wire boundary exists to make unnecessary.
There is nothing to enable — the detector is always on.
Where secrets live on the host
Everything sensitive is encrypted at rest with AES-GCM under a per-install 256-bit master key — the secrets vault. The master key lives in the macOS Data Protection Keychain, scoped to the app's signing identity, with accessibility kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly and iCloud sync disabled. It never prompts, never syncs, and never leaves the Mac. If the keychain is unreachable (an unsigned or unprovisioned build), the app falls back to a mode-0600 key file stored beside the ciphertext — weaker, since key and ciphertext then share a disk, but it keeps at-rest encryption functioning; a fully provisioned release uses the keychain.
Practical consequences:
- Backups and copies are ciphertext. A Time Machine backup or a copied
Application Supportfolder contains only encrypted blobs; without this Mac's keychain entry they cannot be decrypted (unless the file-fallback key was in use and copied along). - Wiping the keychain entry rotates the key. Existing encrypted blobs become unreadable and you re-enter your credentials. Non-sensitive metadata in
profile.jsonis unaffected. - Nothing is synced anywhere. No credential, token store, or key material is uploaded, synced, or backed up by the app itself.
On-disk locations, all under ~/Library/Application Support/BromureAC/ unless noted:
| Location | Contents |
|---|---|
ca/cert.pem, ca/key.pem | The Bromure Agentic Coding Root CA (key mode 0600). Delete the directory to rotate. |
fake-salt.bin | The per-install 32-byte HKDF salt behind fake derivation (0600). Deleting it rotates every fake on this Mac. |
claude-subscription.enc, codex-subscription.enc, grok-subscription.enc | AES-GCM-encrypted subscription OAuth stores (shared default plus per-profile overrides), 0600. |
secrets-master.key | The 0600 fallback master key — present only when the Data Protection Keychain is unreachable. |
default-ssh/id_ed25519.raw, default-ssh/id_ed25519.pub | The shared default SSH keypair copied into new workspaces. |
profiles/<id>/ssh/ | Per-workspace minted SSH keys (the agent directory). |
Keychain: io.bromure.agentic-coding.master-key | The AES-256 vault master key (account v1), Data Protection Keychain. |
Keychain: io.bromure.agentic-coding.ssh-key-passphrases | Imported-SSH-key passphrases, one item per profile/key file. |
Host-side reads for imports — ~/.aws/config, ~/.aws/sso/cache/*.json, ~/.docker/config.json, kubeconfig files — happen on the host only; none of those files are ever mounted into a VM. Encrypted trace bodies (see Tracing) use the same vault key.
Verifying the boundary yourself
The wire boundary is designed to be checkable, not taken on faith. From a shell inside any session:
1. The environment holds fakes.
echo "$ANTHROPIC_API_KEY"
# sk-ant-api03-brm-… ← a fake, not your key
env | grep -E 'TOKEN|KEY'
# every value is brm_/ghp_/glpat_/dop_v1_/lin_api_-shaped placeholder
2. The config files hold fakes.
cat ~/.git-credentials # fake tokens per git host
cat ~/.docker/config.json # fake base64 auth blobs
grep -A2 credential_process ~/.aws/config # helper, not a secret key
3. And yet, everything works.
aws sts get-caller-identity # succeeds — the host resigned the request
gh api user # succeeds — the fake was swapped on the wire
4. TLS inside the VM terminates at the proxy.
openssl s_client -connect api.anthropic.com:443 -brief 2>&1 | head
# issuer: the Bromure Agentic Coding Root CA — not a public CA
5. SSH keys are absent, but signing works.
ssh-add -L # lists public keys served by the vsock agent
ls ~/.ssh/id_* # no private key files to find
6. On the host, watch the swaps happen. Enable tracing for the workspace, then:
bromure-cli trace summary # per-request swap reports and leak warnings
bromure-cli trace leaks # only the suspicious ones
or open the Trace Inspector (⇧⌘I), where swapped requests are annotated Never sent to VM — swapped by proxy.
If you ever find a real credential inside a VM, it got there one of two ways: you (or the agent, at your direction) pasted it in by hand, or it arrived via a shared folder. The swap machinery never writes reals into the guest — and the compromise detector treats real-shaped secrets on the wire as a leak precisely because they should not exist there.
Quick reference
vsock ports on the guest-host boundary:
| Port | Purpose |
|---|---|
| 8443 | The HTTPS MITM proxy — the guest's entire egress. |
| 8444 | The ssh-agent bridge behind SSH_AUTH_SOCK. |
| 8445 | The AWS credential_process helper. |
| 8446 | The Claude subscription-token-swap agent — a port it shares with the local-inference bridge (they are set up in different flows; keep this in mind when diagnosing a session that uses both — see Local Models). |
| 8447 | The Codex subscription-token-swap agent (access, refresh, and ID token). |
Related CLI commands (full reference in Automation & CLI):
| Command | Purpose |
|---|---|
bromure-cli workspaces create --auth token|subscription|bedrock|local | Create a workspace with its auth mode. |
bromure-cli vm run --auth token|subscription|bedrock | Start a VM, selecting the auth mode for an on-the-fly workspace. |
bromure-cli workspaces ssh-keygen <workspace> | Mint a fresh workspace SSH key host-side and print the public key. |
bromure-cli trace summary [workspace] | Summarize traced traffic, including swaps and leak warnings. |
bromure-cli trace leaks [workspace] | Show traced requests with potential credential leaks. |