Keep running Claude Code and Codex the way you do today, and put Condensate underneath them. You install the Condensate plugin, and each CLI sends its hook events to the middleware, a local policy service, and gets its tools from one MCP gateway.
What you get
One policy for Claude Code and Codex
The middleware answers each hook in that CLI's own format, so one set of rules covers both Claude Code and Codex. A tool named in the guard list is blocked before it runs, with a reason. For each session the middleware keeps only counters and short hashes of tool calls, commands, results and edited files, never their text.
Session steering
Detectors watch each session for problems such as edit thrash, tool loops and premature stops, and they make no model calls. When one fires, a short note goes into the agent's context on its next turn. Steering is on by default, and a consent tier per principal sets how far a verdict may go.
A way to measure whether steering helps
You can hold out a share of sessions that is never steered and runs at observe as a control. Every verdict goes in the ledger, and the eval report compares the two groups for each signal.
Failing closed
If the hook shim cannot reach the middleware or gets a bad answer, it blocks the prompt or tool call and names the fault. CONDENSATE_HOOK_FAIL_OPEN=1 lets every fault continue instead. Connect tools and hooks has the full fault rules.
One tool gateway
You list your MCP servers once in the middleware's configuration. Each CLI connects to one address with one local key and sees every server's tools as one catalog.
Logins agents never see
By default, the credential proxy holds your real Claude and Codex logins, and agents hold placeholders. The proxy adds the real token on the way out, and the runtime refuses an agent that would start with a real credential. The proxy is on by default, and the runtime refuses to start without it unless you set CONDENSATE_PROXY=off.
Your scripts on the same accounts
An OpenAI-compatible completions endpoint on the runtime lets judges, classifiers and helper scripts call your agent accounts instead of separate API keys. Each call is charged to one workspace and draws only from that workspace's accounts.
The parts
| Part | Role |
|---|---|
| Condensate plugin | One plugin for Claude Code and Codex. It installs the hook shim and the connection to the MCP gateway. It stores no key, and no model traffic passes through it. |
| Hook shim | A command the CLI runs on each hook event. It sends the event to the middleware and applies the verdict. For edits, it sends hashes of the old and new text, so file content stays on your machine. |
| Middleware | The policy backend. It runs detectors, rule-based checks that score each session without calling a model, and decides what each verdict may do. The consent tier, described below, sets how far a verdict may go. |
| MCP gateway | The middleware's /mcp endpoint. It gathers the tools of every upstream server you list and serves them as one catalog. |
| Credential proxy | The runtime process that holds your real Claude and Codex logins. Agents hold placeholders, and the proxy adds the real token on the way out. CONDENSATE_PROXY=off turns it off, and agents then hold real logins. |
| Completions endpoint | POST /v1/chat/completions on the runtime. Your scripts call your agent accounts through it, under one workspace's rules. |
What the middleware sees and decides
The middleware sees every lifecycle event the CLI emits. The hashes it keeps per session are called digests. With a verdict, it can:
- Deny a tool call or a prompt. A tool named in the middleware's guard list (
CONDENSATE_GUARD_TOOLS) is blocked before it runs, with a reason. - Add a steering note. A short note goes into the agent's context on its next turn.
- Write a verdict into the session record. The verdict is kept without changing what the agent sees.
Its detectors watch for edit thrash, tool loops, repeated commands with the same result, progress stalls, premature stops, scope creep, skipped tests and filler. They count and compare digests. A separate check catches a pipe that hides a failing exit code, such as tsc --noEmit | tail -20, before the command runs.
How the MCP gateway works
- You list the upstream servers once, in the middleware's configuration. An upstream can be an HTTP server or a stdio process.
- Each CLI connects to one address,
http://127.0.0.1:4787/mcp, with one local key in a header. The plugin sets this up for Claude Code and Codex. If you used the installer, the middleware is on port 24787 by default. - Every agent sees one catalog. Tools are named
upstream__tool, so areadtool on thetoolsupstream istools__read.
How the credential proxy works
- The agent gets a placeholder where its Claude or Codex login would go. Every placeholder value starts with
proxy-. - The proxy swaps in the real token on the way out to the model provider.
- An agent that would start with a real credential is refused. The runtime checks the agent's environment and its login files before it starts. The refusal names the variable or file, never the value.
The runtime refreshes each login and pushes the new token to the proxy before its copy expires. Agents never refresh a login themselves.
How the security model fits together
Call your accounts from scripts
curl -fsS http://127.0.0.1:4741/v1/chat/completions \
-H 'content-type: application/json' \
-H 'x-condensate-workspace: YOUR_WORKSPACE' \
-d '{"model":"MODEL_ID","messages":[{"role":"user","content":"Summarize this diff."}]}'
Get the model names from GET /v1/models on the same address. These are the developer setup's addresses. If you used the installer, the runtime is on port 24741 by default.
- The workspace goes in a header. Name it in
x-condensate-workspace. If every account is out, the caller gets a 429 that names each account, why it is out, and the earliest reset time. - Failover happens only before the first word. If an account refuses before any text has streamed, the call moves to the next account in the workspace. Once text has streamed, the call is never moved.
- Each call leaves a receipt without content. The receipt goes on the workspace's
completionsthread: model, engine, account, status, usage, cost and any failure. It holds no prompt and no reply.
Consent tiers
Detectors run on every session and score the session as a whole, not one tool call. A principal is the identity a session belongs to: the signed-in user, or, with sign-in off, the local key the hook shim sends. A consent tier, set per principal, decides how far a verdict may go. Each tier allows everything the tiers above it in the table allow.
| Tier | What a verdict may do |
|---|---|
| Observe | Detectors run and log their verdicts. Nothing reaches the agent. |
| Annotate | Verdicts are written into the session record, and a guarded call can be denied. |
| Steer | A note is added to the agent's context. |
| Autonomous | The middleware can push a session to keep going when it stops too early, and interrupt one that ignored earlier advice. |
| Supervised | A model reads session content and writes notes. You turn it on per principal. It is never a deployment-wide default. |
With no default tier set (CONDENSATE_CONSENT_TIER), the middleware acts at Autonomous: it can push a session to keep going and interrupt one that ignored advice. Supervised can never be the default. Connect tools and hooks shows how to set a tier.
The holdout
- You set the share. It is the share of steer and autonomous sessions that run at observe as a control. Supervised sessions are not part of it.
- A session stays in one group. The group comes from a hash of the session id, so it does not change across events or restarts.
- Every verdict goes in the ledger. The ledger is the middleware's record of each detector verdict and its outcome, without transcript text. It is written to a file when
CONDENSATE_LEDGER_PATHis set. - The eval report reads a ledger file. For each signal, it counts emitted, held-out and suppressed verdicts.
- A replaced verdict stays in the ledger. A later detector on the same event can replace an earlier verdict. The old entry is kept and marked as displaced.