Claude Code is a supported agent, and the one Cortex is built and tested against first.
Cortex recognises its User-Agent, groups its traffic by Claude Code's own session id,
parses its inference, and can prune the tools it never calls.
agentop configure claude-code routes it through Cortex and takes that out again.
agentop configure claude-code enable # route every Claude Code session through Cortex
agentop configure claude-code status # what is set
agentop configure claude-code disable # take it out againAfter enable, run claude the way you always do, with no flags and no environment
variables. Every session goes through Cortex until you disable it. The installer's
--claude-code flag runs the same enable as one of setup's steps.
What enable writes. Seven keys in the env block of ~/.claude/settings.json.
Claude Code applies that block to every session on the machine, including background
agents, so nothing needs exporting in a shell. The addresses come from
~/.cortex/config.yaml, so they match the running proxy:
| Variable | Value |
|---|---|
HTTPS_PROXY |
http://127.0.0.1:47600, the forward proxy |
NODE_EXTRA_CA_CERTS |
~/.cortex/ca/ca.crt, the bridge CA |
SSL_CERT_FILE |
~/.cortex/ca/bundle.crt, the CA plus platform roots |
GIT_SSL_CAINFO |
~/.cortex/ca/bundle.crt |
REQUESTS_CA_BUNDLE |
~/.cortex/ca/bundle.crt |
CURL_CA_BUNDLE |
~/.cortex/ca/bundle.crt |
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC |
1, see Known issues |
Every other setting is left as it was, including any other env entry, such as
ANTHROPIC_BASE_URL or an auth token. enable refuses to overwrite a value someone
else set, such as a corporate proxy; the only values it replaces are Cortex's own, such
as a proxy at an older Cortex port. The first run copies the original file to
settings.json.bak and never overwrites that copy. enable and disable ask before
writing; --yes skips the question. --settings and --config point them at other
files.
When it takes effect. Claude Code applies its env block when it starts, so restart
any claude that is already running; claude --resume picks the conversation back up.
The same goes after disable. While enabled, Claude Code needs Cortex running,
because its requests go to the proxy: disable is the off switch, and agentop service status says whether the proxy is up.
The record. The first enable records what each of the seven keys held before, in
~/.cortex/claude-code-state.json. disable removes only the keys enable added, and
puts back any value it recorded. agentop uninstall runs the same disable.
If agentop is already gone, delete the seven keys from the env block of
~/.claude/settings.json yourself, before deleting ~/.cortex. The four that point
at bundle.crt replace their tools' trust stores, so with the file gone git, curl and
Python fail every TLS call, including ones that never touch Cortex.
If agentop is already gone has the
full procedure.
Claude Code reads NODE_EXTRA_CA_CERTS, which adds the bridge CA to its built-in roots,
so Claude Code itself needs nothing more.
The other four variables are for the tools Claude Code runs. They inherit
HTTPS_PROXY, so they must be able to verify the bridge too. Each of those variables
replaces its tool's trust store rather than adding to it, which is why they get
bundle.crt and not ca.crt: ca.crt alone would leave git, curl or Python trusting
one private CA and nothing else. On macOS, Go tools (go, gh) ignore SSL_CERT_FILE
and use the keychain, so unless macOS trusts Cortex's CA, Cortex passes them through
unread; enable prints the security add-trusted-cert command that has Cortex read
them, and
Go tools on macOS
explains it.
The symptom when trust fails. agentop observe shows Claude Code's requests as
tunnel rows with client-rejected-ca, and no plugin runs on them. Traffic still
flows; it just is not decrypted. The usual cause is a claude that started before the
CA existed: before the first install, or before ~/.cortex was deleted and recreated.
Restart it.
Everything shows as tunnel
covers the other cause.
Its own agent. Claude Code sends a User-Agent of the form
claude-cli/<version> (external, cli). Cortex recognises it as claude-code, so Claude
Code gets its own row in agentop's agents pane.
Its own sessions. Every inference request carries X-Claude-Code-Session-Id, which
is stable for the life of a session and reused by claude --resume. It is the id Claude
Code names its transcript after (~/.claude/projects/<dir>/<id>.jsonl), so a session in
agentop is the session you see in Claude Code. Cortex reads this header in every
deployment by default.
Titles. agentop observe reads Claude Code's transcripts in the background and shows
each session's title and directory. --skip-claude-metadata turns that off; see
Naming sessions from Claude Code.
Subagents. Claude Code sends a subagent's requests under the session id of the
conversation that started it, so they are filed in that session and counted in its cost.
Cortex tags each one as a subagent, from the marker Claude Code writes on the first line
of a subagent's system prompt, and agentop's context gauge uses the tag to follow the
main conversation rather than a subagent's. A subagent's requests, and an agent-team
teammate's, also carry X-Claude-Code-Agent-Id, which Cortex records on the event as
subagent. For a subagent it is the id that names its
<session>/subagents/agent-<id>.meta.json, which holds its type and description. A
teammate's rows carry it too but are not tagged as a subagent's, because Claude Code writes
the marker only for subagents; the tag, not this id, is what says a row is a subagent's.
An agent that another agent started also carries X-Claude-Code-Parent-Agent-Id, recorded
as parentSubagent; one the conversation started has none. In agentop's events pane each
subagent's rows group under a collapsible header — the subagent's description from its
meta.json, which agentop observe harvests alongside the session titles, or its raw id
when nothing names it — so the fan-out can be collapsed to one line and opened to
troubleshoot a slow or costly subagent; see the
events pane. That closes
#1047.
Typed inference. Requests to a path ending in /v1/messages are parsed as the
Anthropic Messages API, whoever serves it: Anthropic's own API or a gateway such as
LiteLLM. Cortex records the model, messages, tools, tool calls and finish reason, and
from the response's usage block the input, cache-read, cache-write, output and reasoning
tokens. Streamed responses are parsed the same way. Claude Code configured for Amazon
Bedrock or Google Vertex AI sends other paths, which are listed but not parsed.
Cost. A gateway's own reported cost is used when it sends one (LiteLLM's
X-Litellm-Response-Cost). Otherwise the tokens are priced at the bundled Anthropic
list rates, tier by tier. If your gateway charges less than list, pin its rate or the
figures are overstated; Pricing shows how.
Tool pruning is supported. agentop tools scan reads Claude Code's transcripts,
proposes the tools you have not called, and writes them to the tool-prune plugin's
list. Claude Code is the only agent it can build a list for. See
Cut token cost.
With more than one inference server — a LiteLLM gateway, or a local model
server such as Ollama (setting one up) —
agentop server — or S on agentop's agents pane — chooses which one Claude
Code's new sessions use, with one Claude Code configuration and without
restarting anything. A running conversation stays on
the server it started on, with the exceptions below.
See Choosing an inference server.
Claude Code needs no setting of its own for this beyond going through Cortex.
Its inference is captured wherever ANTHROPIC_BASE_URL points — Anthropic
itself, or a gateway — and sent to its server. An https base URL is captured
only when the TLS bridge decrypts it, which needs its port in tls_bridge.ports,
443 and 8443 unless set: on another port, such as :4000, the request stays an
opaque CONNECT tunnel and is not routed. An http base URL reaches the router
only if Claude Code sends it through the proxy, and agentop configure claude-code enable sets HTTPS_PROXY only. The host Claude Code names must be up even for
sessions routed elsewhere: the proxy dials the CONNECT target before it sees the
request inside.
Claude Code's own model names go first, the conversation's, auto mode's
classifier's and the background calls' each as it chose them, so /model and
Claude Code's model settings keep working wherever the server serves those names.
A name the server refuses is sent again, before Claude Code sees the refusal, with
the server's own model: its main for a request with tools, the conversation, and
its helper for one without, such as the title. A GLM server behind LiteLLM, say,
is added with agentop server add glm <url> --main glm --helper nemotron.
The key is the server's: on a routed request the router puts the configured
server's key in the header Claude Code sent its own in, X-Api-Key for
ANTHROPIC_API_KEY and Authorization for ANTHROPIC_AUTH_TOKEN, so either can
stay as it is.
Claude Code keeps naming the model it asked for, so agentop is where the server
shows: the sessions table's SERVER column names each session's server, the
events table shows where each request went and, under METHOD, the model it was
sent for, and the detail pane's redirected: and model: lines name the host
and the model Claude Code asked for when they were others.
A session is pinned on its conversation's opening turn, so switch before
/clear or a new claude, not after. Claude Code's request says which turn it
is: an opening turn carries no assistant reply yet. A conversation Cortex did not
see begin — one already running when you first route Claude Code — is not moved
unless you move it (below):
if the session's history shows which server its conversation's last request
reached, Cortex keeps it there with that server's key, and otherwise it goes where
Claude Code sends it, unrouted. Claude Code's one-shot requests (the title, auto
mode's classifier) and its subagents' requests decide nothing; they follow the
session's pin once there is one. Before then a one-shot goes to the server you
chose, since a new session's title request can arrive before its opening turn, and
a subagent's request addressed to one of the servers stays on it. Pins are kept in ~/.cortex/plugin-state.json,
so a proxy restart moves no conversation.
A running session can also be moved on purpose: agentop server move <session-id> <server> (or own, to stop routing it), or S on its row in
agentop's sessions pane. The move is the one thing that moves a running
conversation, it says what it costs first — the session's whole context is re-sent
uncached on its first turn at the new server — and the reasoning blocks only the
old server can read are dropped from what the new server is sent. A move back
to that server gets them back. What a server loses: nothing, for Anthropic-origin
thinking, whose text is empty (encrypted); the readable reasoning text, when the
old server was GLM or Ollama. Claude Code notices nothing: its own transcript is unchanged, and it keeps naming the model it
asked for.
A conversation carried to a new session id is one Cortex did not see begin, so it
goes where Claude Code sends it, and one routed to another server moves back; see
Known issues. That happens in three ways: Claude Code's
continued-in hand-off copies a live conversation to a new id; claude daemon
starts background jobs with --session-id <new> --fork-session --resume <parent>;
and /clear with background subagents running moves their traffic to the new id,
where it goes where Claude Code sends it until the new conversation's opening turn
pins the session.
Nothing fails over. When a session's server is down its requests fail, with a 502 from the proxy when the server cannot be reached; route the agent to another server, and start a new session there.
If the server's model has a smaller output limit or context window than the model
the agent asked for, the router lowers the request's output limit (unless that would
not leave it above an extended-thinking budget) and, near the window, drops the oldest
turns, keeping the task and the recent turns. Turns are dropped from Anthropic messages
and OpenAI chat-completions requests only; a responses-format request has only its
output limit lowered. The agent's own view of the conversation is not changed. See the inference-router's Limits rule in
docs/plugin-catalog.md.
/model may list a server's own names. If Claude Code asks the gateway for its
model list, that request is routed like the rest, so the picker can show, say,
glm-5.3. A name chosen there goes to the server as it is, like any other.
Tested live on 2026-10-05 with Claude Code 2.1.286 on macOS 26.6.2 (arm64), Cortex built
from main at 1fd69b75, through a LiteLLM gateway serving Claude Opus 5.5, Claude
Sonnet 5 and Claude Haiku 4.5. This was exercised:
enable --yes,statusanddisable --yesagainst a scratchHOME:enablewrote the seven keys,statuslisted them, anddisableleft the file as it started. DeletingCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICby hand in between madestatusreport 6 of 7, anddisablestill cleaned up;- a working session through Cortex, recorded under the
claude-codeagent and Claude Code's own session id, with its transcript title; - all 125 successful responses in that session parsed, each with its model, its input, cache-read, cache-write, output and reasoning token counts, and a cost: 21 from the gateway's reported figure, 104 at the bundled rates;
- 29 subagent requests tagged as subagents and filed in their parent's session;
tool-prunerewriting the requests, and the session reporting the cost avoided.
Not exercised: Linux; install.sh --claude-code; Claude Code talking to
api.anthropic.com directly rather than through a gateway; Amazon Bedrock and Google
Vertex AI.
Routing through inference-router was tested live on 2026-10-09 with Claude Code
2.1.295 (claude --bare -p, an API key and no ANTHROPIC_BASE_URL), through a scratch
Cortex: its requests to api.anthropic.com were captured and sent to the server. On a
GLM server claude-opus-5-5 was refused once and sent again with the server's main
model, then sent with it from the start; on a server that serves Claude's names it went
as Claude Code asked. A file was written and read back on each.
- Nonessential traffic is switched off.
enablesetsCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1, which Claude Code documents as turning off its auto-updater, the/bugcommand, error reporting and telemetry. To keep them, delete that key from theenvblock afterenable.statusthen reports 6 of 7 keys set,disablestill works, and the nextenableadds the key back (#1205). disablediscards hand-edits. If you change one of the seven keys by hand afterenable,disablestill puts back the value from beforeenable, without a warning (#1289).- A conversation moved to a new session id leaves its server. Cortex keeps a
conversation on its server by its session id's pin, and Claude Code's
continued-in hand-off and
claude daemon's forked background jobs carry a conversation to a new id, which no pin follows. Cortex then sees a conversation it did not see begin and sends it where Claude Code sends it, so one routed to another server moves back to the serverANTHROPIC_BASE_URLnames, mid-conversation. - A continued conversation can show twice. Claude Code sometimes moves a live conversation to a new session id. agentop then shows two rows with the same title, and the older row keeps the copied title. Cortex does not yet read Claude Code's record of the move.