Coding agents: Claude Code and Codex¶
Keryx does the talking; a coding agent on your machine does the work. Two are supported, and a task runs on whichever one it was handed to for its whole life:
- Claude Code (
claude), driven through the Claude Agent SDK. The default. - Codex (
codex), driven through OpenAI'sopenai-codexPython SDK, which runs thecodexCLI it bundles as an app-server.
Each agent is an optional extra of the same name — claude, codex, or all — and each
SDK carries its own CLI, so there is nothing else to install. They are large: a complete
environment measured about 760 MB, 510 MB with Codex alone, 410 MB with Claude alone and
160 MB with neither (dev tools included in all four). The Codex SDK is pinned to one exact
version, because Keryx reads its messages field by field.
The one-line installer (install.sh) installs both. Elsewhere:
| Command | Installs |
|---|---|
uv sync (in a clone) |
both agents — the default agents group is keryx[all] |
uv sync --no-group agents --extra codex |
Codex only (--extra claude: Claude only) |
uv sync --no-group agents |
neither: keryx serve --demo only |
uv sync --extra codex |
both still: the default group comes along |
pip install 'keryx-voice[all]', [codex], [claude] |
what it names; plain keryx-voice installs neither |
Python packaging has no default extras (PEP 771 is a draft, and uv 0.11 has no
default-extras), which is why the default is a dependency group. One consequence: uv run
syncs the environment back to the defaults before it runs, so on a narrowed checkout give it
the same flags (uv run --no-group agents --extra codex keryx …) or set UV_NO_SYNC=1 —
a clone's service unit runs uv run, and would otherwise put every agent back.
An agent that is not installed says so in doctor and keryx setup, with the command that
installs it; it is never offered to the voice model; and keryx serve refuses to start with
it as AGENT_BACKEND. keryx serve --demo, which answers every task with a sample reply,
needs neither.
AGENT_BACKEND picks the one that does the work when you do not say, and AGENTS_ENABLED
lists every one a task may be sent to. With more than one enabled and signed in, you can
name it out loud — "have Codex look at the build" — and a model name picks its agent too
("use opus" is Claude, "use terra" is Codex). A follow-up always goes back to the agent that
started the task: a session belongs to the agent that issued it.
keryx setup is the way in: its "Coding agents" section shows what is installed and
signed in, asks which agents and which is the default, runs the sign-in each one is missing
(and leaves one that can already run alone), and runs one real task through each as a smoke
test. From the command line: keryx auth login claude|codex, keryx config set
AGENT_BACKEND …, and keryx auth status --smoke. keryx doctor checks the same
things on every run.
Signing in¶
Both agents take the same three tiers, in the same order — the first one set wins:
| Tier | Claude | Codex |
|---|---|---|
| API key (pay per token) | ANTHROPIC_API_KEY |
CODEX_API_KEY |
| Headless subscription token | CLAUDE_CODE_OAUTH_TOKEN, from claude setup-token |
CODEX_ACCESS_TOKEN (see below) |
| Stored subscription login | claude, then /login |
codex login, or codex login --device-auth with no browser |
The credential reaches the agent in its environment and nowhere else — never on its command
line, never in a log. OPENAI_API_KEY, the voice model's key, is never handed to Codex:
that would quietly move a ChatGPT-plan user onto per-token billing. Set CODEX_API_KEY if
that is what you want.
Codex's own process is handed a copy of Keryx's environment, so Keryx overrides every credential variable the chosen tier does not use with an empty value, which Codex treats as unset. Beyond that, each Codex tier goes in its own way, because of what the app-server the SDK runs actually reads (checked against 0.157.1):
CODEX_API_KEYis ignored in the app-server's environment. So Keryx logs in with the key once — on stdin, never on a command line — into a Codex home of its own (~/.local/share/keryx/codex, owner-only, with yourconfig.toml,AGENTS.mdandskillslinked in, but nothooks.json: your hooks are your own automation), and logs in again only when the key changes. Your own~/.codexlogin is never touched.CODEX_ACCESS_TOKENis read from the environment, so that is all Keryx does with it: nothing is stored. It is not a ChatGPT token but an OpenAI agent identity token; a bogus one fails cleanly, but no real one has been run yet.- The stored login is your own
~/.codex, exactly as thecodexCLI uses it.keryx auth login codexruns the SDK's bundledcodex login, which shares that home with anycodexyou have on PATH.
What each agent can do here¶
Every ✅ was checked against the real CLI while the feature was built — the Slack and Gmail rows as far as the mechanism: an MCP server handed to Codex with its secrets passed by name, proved with a stand-in server rather than Slack or Google themselves. Anything that was not checked is marked, and listed under "possible".
| Capability | claude | codex |
|---|---|---|
| Dispatch by voice, and name the agent out loud | ✅ | ✅ |
| Follow-ups resume the same session | ✅ | ✅ thread_resume |
| A follow-up reaches a task while it is still running | re-runs after the turn | ✅ into the running turn |
| Cancel stops the work | ✅ | ✅ turn/interrupt, then the app-server is stopped |
Progress lines in DATA_DIR/tasks/<id>.log |
✅ | ✅ |
SPOKEN_SUMMARY: / RESTART_REQUIRED: |
✅ | ✅ |
| Project working directory and briefs | ✅ | ✅ |
| The per-call memory update | ✅ | ✅ when it is the default |
| API key and stored subscription login | ✅ | ✅ |
| Headless subscription token | ✅ | 🟡 read from the environment; no real token run yet |
| Choosing the model by name | ✅ opus, sonnet, fable, haiku | ✅ astra, sol, luna, terra |
Wall-clock cap (SUBAGENT_TIMEOUT_S) |
✅ | ✅ |
| Turn cap and dollar cap | ✅ SUBAGENT_MAX_TURNS, SUBAGENT_MAX_BUDGET_USD |
— |
Tokens recorded on the task (keryx tasks show, per project in keryx tasks usage) |
✅ | ✅ |
| Dollar cost recorded on the task | ✅ | — a plan call has no price |
Slack, through the server the send_to_slack plugin names (mcp_server) |
✅ | ✅ handed over from ~/.claude.json |
| Gmail and Calendar | ✅ claude.ai connectors | ✅ with GOOGLE_WORKSPACE_MCP=true |
| Its own instructions file | ~/.claude/CLAUDE.md |
~/.codex/AGENTS.md |
| Its own skills, listed to the voice model | SKILLS_DIR |
~/.codex/skills |
Nothing to install beyond Keryx (extra claude / codex) |
✅ | ✅ bundled CLI (~350 MB) |
doctor, keryx setup, keryx auth, --demo |
✅ | ✅ |
| Approval bridge for your on-screen sessions | ✅ | — |
Two of those are worth knowing before you switch:
- Gmail and Calendar. Claude gets them from its claude.ai connectors, which live on the
Anthropic account. Codex has none, so with Codex enabled, connect Google for agents in
keryx setup(orkeryx auth login google-workspace);doctorwarns until you have. - The approval bridge (
keryx approvals) is for Claude Code sessions on your own screen that stop and ask you something. It has nothing to do with which agent Keryx dispatches to, and it stays Claude Code only.
Both agents run with approvals and the sandbox off (bypassPermissions for Claude, the
full-access sandbox with approvals never asked for Codex). The phone PIN gates the dispatch,
the same way whichever agent runs it: see SECURITY.md.
Two more that are true of both, and worth knowing:
- A follow-up to Claude while it works waits for the turn to end, then resumes the session with it; Codex takes it in the turn it is running. The difference is Claude's SDK, not a choice: see "Possible later".
- Cancel and the wall-clock cap stop the agent, then its process. For Codex that kills
the commands it was running — checked with one that ignores SIGTERM, SIGHUP and SIGINT.
A command that detached itself (
setsid, a daemon) survives, as it would have undercodex exec: Keryx stops the app-server, not a process group it never had.
Possible later¶
- A dollar figure and a dollar cap for Codex. It reports tokens per turn, so an API-key user's cost could be priced from a table and capped. A ChatGPT-plan run has no price.
- A turn cap for Codex. There is no flag for one; the wall-clock cap stands in.
- The approval bridge for Codex sessions on your screen. Codex has a hooks file, so this is plausible; nothing is built.
CODEX_ACCESS_TOKEN, verified. The plumbing exists (above); it needs a run with a real agent identity token before it moves up.- Codex's own budget. The app-server can end a turn with
sessionBudgetExceeded; that is already reported as an ordinary failure, but nothing sets such a budget yet. - A follow-up into a running Claude turn. A
query()sent while Claude works folds into the turn at a tool boundary, but one sent after its final text starts a second turn that the reader never sees, so it would be lost. The way in to try: start the CLI withextra_args={"replay-user-messages": None}and see whether it echoes a queued message when it takes it; if so, the adapter can read on past a result while a steer is unacknowledged.
Not easily possible¶
- claude.ai connectors for Codex. They belong to the Anthropic account. Codex uses
workspace-mcpinstead. - Handing a task from one agent to the other. Session ids are not portable, so a follow-up always stays with the agent that started the task.
- A spend figure for subscription use. Neither subscription exposes what one call
cost, so
check_billingcovers API keys only, for both agents. - A dollar cap on subscription runs, for the same reason.
Adding a third agent¶
Every agent runs through one session (keryx/agents/session.py), which already does the
progress lines, the SPOKEN_SUMMARY: and RESTART_REQUIRED: reading, the usage, the
redaction and the error handling. A new agent is:
- one module under
src/keryx/agents/holding an adapter — a class whoseturn(prompt)yieldsText,ToolCall,FileEdit,SessionId,Noticeand a finalDone, plussteer,interruptandclose— and anAdapterRunnersubclass whosecontext(task)builds itsAgentContextand whoseconnect(context, resume)starts its client and returns the adapter; - one
BackendSpecentry inkeryx/agents/registry.py::BACKENDS: its runner, its spoken model names, where its credentials come from (AuthSource), its install hint, instructions file, skills directory and login commands. The router, the task manager, the voice tools,doctorandkeryx setupread that table and nothing else; - its name in
AgentNameinconfig/settings.py, its settings inSettings(docs/configuration.mdis regenerated from it), a column in the table above (a test checks), and tests — the sharedScriptedAdapterintests/agents/fakes.pycovers the session, so its own tests are the translation from its client's messages to those events.