# Security (https://condensate.dev/security/)

> What stays on your Mac, and what agents never see.

Condensate keeps your agents, their work and your model logins on your Mac. By default, agents run with placeholder tokens, and the runtime checks each one before it starts.

## What stays protected

### Logins agents never see

By default, each agent starts with a placeholder token, never your real model login. The credential proxy swaps in the real token as the call leaves your Mac for the provider. The proxy is on by default, and the runtime refuses to start without it unless you set `CONDENSATE_PROXY=off`.

### Logins refreshed in one place

One process in the runtime, the broker, refreshes every login. Your agents never refresh anything, and two refreshes of one login never race. The broker never logs a token.

### Spending only what a workspace allows

A task spends only its workspace's accounts and runs only the models the workspace lists. When every account is out, the task stops rather than borrow an account from outside the workspace. Your scripts follow the same rules.

### A hook policy that fails closed

The Condensate plugin sends Claude Code's and Codex's hooks to the middleware. If the middleware cannot answer, prompts and tool calls are denied, with a reason that names the fault.

### Everything stays on your Mac

The runtime, your agents, their worktrees and every thread's log stay on your Mac. Threads are stored in a SQLite database in the runtime's data directory.

## The check before an agent starts

Every placeholder starts with `proxy-`. Before it starts an agent, the runtime checks the agent's environment and login files, and refuses the agent if:

1. its environment names no `HTTPS_PROXY`, so its calls would not reach the proxy;
2. its environment holds a real model token, such as `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`, rather than a placeholder;
3. its environment holds the runtime's own GitHub App settings, such as `GITHUB_APP_ID` or `GITHUB_APP_PRIVATE_KEY_FILE`;
4. a Claude Code agent's config directory holds a `.credentials.json`;
5. a Codex agent's `auth.json` holds a real API key, a real access or id token, or any refresh token;
6. Condensate's own agent runs a model whose login the runtime cannot tell, or one from a provider the proxy does not carry.

The refusal names the agent, the account, and the variable or file. It never includes the value.

## How a login is refreshed

1. **The broker refreshes the login once, inside the runtime.** Agents keep their placeholder.
2. **It takes two locks first.** One holds off other refreshes in the same process, and a lock file holds off other processes on the Mac. A refresh token can be spent only once, so two refreshes of one login must never race.
3. **It sends the refresh only to an approved provider endpoint.** The endpoint is checked again just before the request leaves, and a redirect counts as a failure, so the refresh token cannot be sent anywhere else.
4. **It saves the new token only if the token it spent is still the stored one.** If the save fails, the new token goes to a recovery slot, so a crash mid-refresh does not make you log in again.
5. **It hands the new token to the proxy** before the proxy's copy expires.

A failed refresh reports only the provider's status code.

## What a workspace may spend

- **A task spends only its workspace's accounts.** A spawn that names another account is refused with `account-not-in-roster`, and the response lists the accounts it may use.
- **It never borrows an account.** When every account in the workspace is out, the runtime returns `NoSpendableAccount` rather than reaching outside the workspace.
- **It runs only listed models.** A model the workspace does not list is refused with `model-not-allowed`.
- **Your scripts follow the same rules.** `POST /v1/chat/completions` spends the accounts of the workspace named in the `x-condensate-workspace` header.

See [Accounts and model routing](https://condensate.dev/guides/accounts/) for the full refusal list.

## What the proxy does not cover

The proxy carries model provider logins for Claude Code, Codex and Condensate's own agent. GitHub tokens and the keys for Slack, Notion, Linear and local MCP servers stay real inside the agent. The runtime's own GitHub App credentials never enter it.

The runtime will not start without the proxy unless you set `CONDENSATE_PROXY=off`. Agents then hold real logins.

## The hook policy

- **A policy fault denies the prompt or tool call.** If the middleware is down, slow or misconfigured, or `CONDENSATE_KEY` is not set, `UserPromptSubmit` and `PreToolUse` are denied, with a reason that names the fault.
- **Events that cannot deny still run.** A tool that already ran, or a `Stop`, is allowed, and the hook writes one warning line.
- **`CONDENSATE_HOOK_FAIL_OPEN=1` allows every event on a fault.** It removes the policy check, so use it for local development only.

## Steering

Steering is on by default. The runtime attaches every agent that runs as a local process to the middleware, unless its spawn sets `middleware: "off"`. Container and Kubernetes agents are not attached, so they are not steered. The runtime reads the middleware's key from `~/.config/condensate/middleware/api-key`, a file that holds the key on one line.

- **No key file:** the runtime runs agents without steering. It logs `steering off: <reason>` at start, its `/health` reports `steering` as off with the reason, and Dex's Box section shows it. The middleware's `start-local.sh` writes the file from its `CONDENSATE_LOCAL_API_KEY` when it is missing, so on a box whose middleware starts through `start-local.sh` with a local key, this lasts only until the runtime restarts after the first write. A middleware started any other way, such as the fresh-box install's Compose service, never writes the file.
- **A key file with any group or other access**, such as mode `0644` (use `0600`): every agent the runtime would attach is refused, and the refusal names the file, never the key.
- **`CONDENSATE_STEERING=off`:** no agent is attached.

## Read next

- [Connect your own tools](https://condensate.dev/connect/)
- [How you use it](https://condensate.dev/use/)
- [For engineers](https://condensate.dev/build/)
- [What works today](https://condensate.dev/status/)
