README
¶
◆ cortex
A terminal coding agent built on the axon runtime.
Streaming chat · tool dispatch · append-only sessions · secondary-LLM context pruning — all from your terminal.
Documentation · Install · Quick start · Architecture · Playground · GitHub
The full documentation site is at atakang7.github.io/cortex. This README renders that same documentation in markdown for reading on GitHub, in your editor, or offline.
Documentation map
cortex/
├── getting-started/
│ ├── install — go install · pre-built binaries · build from source
│ └── quick-start — env vars · interactive & non-interactive · first config
├── guides/
│ ├── configuration — the three-layer cascade · fields · known providers
│ ├── interactive — the transcript · Ctrl-C · the banner
│ ├── non-interactive — stdout/stderr split · iteration limits · piping
│ ├── commands — /new · /undo · /cd · /session · /help · /quit
│ └── keyboard — every key the terminal binds
├── concepts/
│ ├── architecture — axon owns the loop · cortex owns the product · two boundary rules
│ ├── pruner — the secondary model · what it sees · when to skip it
│ ├── trace-log — the third event sink · both models tapped · why it exists
│ └── mcp-servers — MCP protocol · auto-discovery · multiple servers
├── playground/
│ ├── overview — run.sh & trace.sh · the run directory · the six views
│ ├── running-tasks — step by step · what a run keeps · comparing runs
│ ├── reading-traces — timeline · prompt · system · tools · usage · errors
│ └── tasks — ledger · report · pipeline · what the playground found
└── reference/
├── cli — --config · --prompt · --verify · --version · exit codes
├── config-file — every field · pruner · mcp_servers · env overrides
└── api-keys — resolution order · providers.json · the leading $
Install
Get the cortex binary on your machine in one of three ways.
go install
If you have a Go toolchain, this is the fastest path:
go install github.com/atakang7/cortex/cmd/cortex@latest
The binary lands in your $GOPATH/bin — make sure that's on your $PATH.
Pre-built binaries
Every release ships binaries for linux and darwin on amd64 and arm64. Download the archive for your platform, extract it, and put the cortex binary somewhere on your $PATH.
curl -L https://github.com/atakang7/cortex/releases/latest/download/cortex-linux-amd64.tar.gz \
| tar xz -C /usr/local/bin cortex
Build from source
git clone https://github.com/atakang7/cortex.git
cd cortex
go build -o cortex ./cmd/cortex
Verify
Check that it runs and prints build metadata:
cortex --version
cortex 1.2.3 (abc1234, built 2025-01-01T00:00:00Z)
ℹ️ What's next? cortex needs a model before it can start. Continue to Quick start to configure one.
Quick start
Configure a model and run cortex in under a minute.
The fastest possible start
cortex needs one thing before it can start: a model. Two environment variables are enough:
export LLM_MODEL=gpt-4o
export LLM_API_KEY=sk-...
cortex
Interactive mode
Run cortex with no arguments to start an interactive session in your current directory:
cortex # interactive
cortex --prompt "summarize the TODOs" # one turn, then exit
cortex --config reviewer.yaml # a specific config, no cascade
cortex --version
Non-interactive mode
--prompt writes the assistant's answer to stdout and everything else — tool activity, notices, errors — to stderr, so it composes:
cortex --prompt "list every exported function in internal/ui" > api.txt
See Non-interactive mode for the full picture.
A config file for repeat use
If you don't want to pass env vars every time, write a config file:
# ~/.config/cortex/config.yaml
provider: openrouter
model: deepseek/deepseek-v3.2
See Configuration for the full three-layer cascade.
Onboarding
If cortex starts without a model configured, it prints an onboarding message explaining the three places a model can come from — it does not guess or default.
💡 Tip: The transcript is committed to your terminal's own scrollback, not held in an alternate screen. A finished session stays scrollable and copyable after cortex exits.
Configuration
Three sources, each overriding the last. Layering is field by field.
The cascade
~/.config/cortex/config.yaml— your defaults (honoursXDG_CONFIG_HOME)./cortex.yaml— this project's overrides- the environment —
LLM_PROVIDER,LLM_MODEL,LLM_BASE_URL,LLM_API_KEY
Layering is field by field, so a project config that names only a model keeps the provider from your user config. You only specify what you want to override.
⚠️ Specifying
--configdisables the cascade entirely — only that file is read, plus the environment. Use it when you want a specific, self-contained config.
The full file
name: reviewer # shown in the banner
provider: openrouter # the axon provider name
model: deepseek/deepseek-v3.2 # the model within that provider
system_prompt: ./reviewer.md # a path is read from disk; prose is used as written
pruner: # optional secondary model for context pruning
model: meta-llama/llama-3.1-8b
provider: openrouter # defaults to the main provider if omitted
mcp_servers: # spawned at startup, their tools auto-discovered
github:
command: docker
args: ["run", "-i", "--rm", "ghcr.io/github/github-mcp-server"]
env: ["GITHUB_TOKEN=..."]
Providers and credentials are not in this file
cortex's config only names which provider and model to use. Endpoints and credentials live in axon's config — the shared providers.json the axon-family tools already use, or the LLM_API_KEY / LLM_BASE_URL environment variables. See API keys for the resolution order.
Known providers
provider supplies base_url for openai, openrouter, anthropic, groq, deepseek and ollama. Any other provider must state its own base_url (via axon's config or LLM_BASE_URL). A localhost endpoint (e.g. ollama) needs no API key.
System prompt
Omit system_prompt and cortex uses its built-in coding-agent role text. The tool catalog is appended by axon, so a custom prompt must not enumerate tools — or you pay for those tokens twice on every call. See Architecture for why.
The fastest possible start
export LLM_MODEL=gpt-4o
export LLM_API_KEY=sk-...
cortex
Interactive mode
The terminal UI — a transcript committed to your scrollback, not an alternate screen.
Starting a session
cortex
cortex runs in your current working directory. It reads the repo, streams the model, dispatches tools, and renders everything into your terminal's own scrollback. When cortex exits, the finished session stays scrollable, searchable and copyable.
How the transcript works
The transcript is committed to your terminal's own scrollback rather than held in an alternate screen. Only work still in flight redraws. This is a deliberate choice: a finished session is something you scroll back through, search, and copy from — including after cortex exits.
Interrupting a turn
Press Ctrl-C to interrupt the running turn. cortex treats Ctrl-C in raw mode as a key, not a kill signal — it cancels the turn cleanly and returns you to the prompt.
ℹ️ Why not SIGINT? In raw mode Ctrl-C arrives as a key. cortex binds it to cancelling the turn rather than killing the process. Only signals the terminal cannot deliver as a keystroke (
SIGTERM,SIGHUP) end the program from outside.
The banner
Each session opens with a banner showing the cortex version, the provider and model in use, and the current turn count. The name field from your config appears here too.
Changing the model mid-session
When the model is changed interactively, cortex persists the new provider and model to your user config. The same applies to the pruner. So the next session picks up where you left off.
For the keys and commands available inside a session, see:
Non-interactive mode
Pipe cortex into scripts with --prompt.
The split: stdout for answers, stderr for everything else
--prompt runs a single turn with no TUI, then exits. The assistant's answer goes to stdout; tool activity, notices and errors go to stderr. That separation lets you compose:
cortex --prompt "list every exported function in internal/ui" > api.txt
cortex --prompt "summarize the TODOs" | fold -s | head -40
Objective verification
When a command can judge the result, add --verify. A failing verifier is sent back to the same session for up to --repair-attempts extra turns, then run again.
cortex --prompt "fix the failing report total" --verify "python3 -m pytest -q" --repair-attempts 1
The extra turn only happens on failure, so cheap smoke tasks do not pay repair tokens when the first answer is already correct.
Cancellation
In non-interactive mode, Ctrl-C ends the process — the ordinary expectation for a one-shot command. This is different from interactive mode, where Ctrl-C cancels the turn.
Iteration limits
A --prompt run is bounded by maxIterationsOnce, a cap on how many tool turns the agent may take before it must answer. Interactive mode is unbounded — a human is watching, and Ctrl-C ends a turn that has lost its way.
ℹ️ Why bounded? With no one watching, an agent that loops forever is a bill, not a bug. The cap is deliberately conservative for one-shot use.
Combining with a config
cortex --config reviewer.yaml --prompt "review the last commit"
--config disables the cascade and reads only that file plus the environment. Combine with --prompt for reproducible one-shot runs against a fixed configuration.
Slash commands
Type a slash command at the prompt to control the session.
| Command | What it does |
|---|---|
/new |
Wipe the session and start over. |
/undo |
Revert the last file edit, byte for byte. |
/cd <path> |
Change the working directory. |
/pwd |
Show the working directory. |
/session |
Session file, turn count, pending undos. |
/help |
List the commands. |
/quit |
Exit (/exit, /q also work). |
/undo — byte-for-byte revert
Every file edit is atomic and recorded. /undo reverts the last edit exactly — the previous content is restored, not reconstructed. Repeat /undo to step backwards through the session's edits.
/session — inspect state
Shows the path to the append-only session file, the current turn count, and how many undos are pending. Useful when you want to find the session on disk or understand how far the conversation has gone.
/new — clean slate
Wipes the in-memory session and starts a fresh turn counter. The old session file remains on disk — /new only affects the live conversation, not persisted history.
💡 Aliases:
/quit,/exitand/qall exit.
Keyboard reference
Every key the terminal binds, in one table.
| Key | Action |
|---|---|
enter |
Send the current input. |
ctrl+j / alt+enter |
Insert a newline. |
ctrl+c / esc |
Interrupt the running turn. |
ctrl+c (when idle) |
Clear the input, then exit. |
ctrl+d |
Exit. |
Ctrl-C has two behaviours
Ctrl-C is context-sensitive. While a turn is running, it cancels the turn — the agent stops and you return to the prompt. When idle (no turn in flight), the first Ctrl-C clears the input buffer; a second exits.
Esc interrupts too
Esc is bound to the same interrupt as Ctrl-C, so you can cancel a turn even on a terminal where Ctrl-C is captured by the shell or a multiplexer.
Newlines without sending
Ctrl+J and Alt+Enter both insert a literal newline, so you can write multi-line prompts without accidentally sending them.
ℹ️ Raw mode. cortex runs the terminal in raw mode, so keystrokes are intercepted individually rather than line-buffered. That's why Ctrl-C is a key, not a signal — see Interactive mode.
Architecture
axon owns the loop. cortex owns the product on top. Layers point one direction.
┌─────────────────────────────────────────────────────────┐
│ cortex — the product │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ main.go │ │ config │ │ ui │ │
│ │ flags │ │ resolve │ │ terminal │ │
│ │ wiring │ │ model │ │ transcrip│ │
│ │ dispatch │ │ prompt │ │ commands │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ config & theme are leaves · ui owns the terminal │
└───────────────────────┬─────────────────────────────────┘
│ the only seam
┌───────────────────────┴─────────────────────────────────┐
│ axon — the runtime (↗ docs) │
│ ┌────────┐ ┌─────────┐ ┌────────┐ ┌────────┐ │
│ │ loop │ │ session │ │ pruner │ │ tools │ │
│ │ stream │ │ append │ │ 2nd-LLM│ │ 7 built│ │
│ │ chat │ │ undo │ │ context│ │ + MCP │ │
│ └────────┘ └─────────┘ └────────┘ └────────┘ │
└─────────────────────────────────────────────────────────┘
The two boundary rules
-
internal/configimports nothing else from this module. It is a leaf. If a value there needs a type from a higher layer, it belongs in that layer. -
internal/ui/event.gois the only file that touches both axon and Bubble Tea. axon calls its event handler synchronously on the turn goroutine; Bubble Tea owns the terminal from its own. Nothing else may cross that line.
What main.go wires
main.go is wiring and nothing else. Every decision it makes is delegated: configuration to internal/config, presentation to internal/ui, and the loop itself to axon.
Parse flags → Load config → Tap models → Run loop
⚓ ⚙️ 📡 ▶
interactive resolve model trace build axon Options
non-interactive & pruner wiretap attach handler
version vs axon main+pruner go
Why the system prompt must not list tools
The tool catalog is appended by axon, natively, to every request. If a custom system_prompt also enumerates the tools, the model sees the catalog twice — and you pay for those duplicated tokens on every single call. This is exactly the kind of thing the trace log was built to catch: a ~2,100-token duplication that was invisible in the event stream and obvious in the wire log.
Why the transcript isn't an alternate screen
| alternate screen | committed scrollback |
|---|---|
| Vanishes on exit. The session is gone the moment cortex stops — nothing to scroll, search or copy. | Stays in your terminal's own scrollback. A finished session is scrollable, searchable and copyable — even after cortex exits. Only work in flight redraws. |
📖 Deeper into axon: the loop, session, pruner and tools are all axon's. Read axon's architecture, context management, and the built-in tools for the layer cortex builds on.
The pruner
A cheap secondary model that parks stale context so the main model stays under its window.
The problem
As a session grows, the conversation approaches the model's context window. The naive fix — dropping old messages — loses the thread. The pruner does something smarter: it runs a separate, cheap model that parks stale context by summarising it, so the main model keeps the thread without paying for every old token on every call.
How it's configured
The pruner is optional. When configured, it runs alongside the main model:
pruner:
model: meta-llama/llama-3.1-8b # a cheap, fast model
provider: openrouter # defaults to the main provider if omitted
If pruner.provider is omitted, it falls back to the main provider. If pruner.model is empty, no pruner runs — the main model manages its own window.
What the pruner sees
The pruner's context decisions are as worth auditing as the agent's, and they are invisible in the event stream — the stream only reports how many tokens a pass moved, never why. That's why the pruner model is also tapped by the trace log, so you can read its curator calls and understand what it decided to park and what it kept.
ℹ️ Two models, one trace. Both the main model and the pruner are tapped at the single place they are resolved. See Trace log for how to read what each one sent.
Persisting the pruner choice
When the pruner model is changed interactively, cortex saves the new provider and model to your user config. The next session picks it up automatically.
📖 The pruner is axon's. cortex only names which cheap model to run. axon owns the context windowing, the curator calls and the parking strategy — see axon's context management for how it keeps long sessions stable. cortex taps the pruner model into the trace log so you can audit those decisions.
When not to use a pruner
- For short sessions that never approach the window — the pruner only adds cost.
- When you want the main model to see the full, unwindowed conversation — e.g. careful auditing.
Trace log
A third, unfiltered event sink — wire-level request and reply logging.
┌──────────────────┐
│ axon runtime │
│ stream · tools │
│ pruner │
└────────┬─────────┘
│ every request & reply
┌────────┴──────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ the TUI │ │ plain render │ │ the trace │
│ what user │ │ stdout = ans │ │ the wire │
│ sees │ │ stderr = all │ │ exact req/rsp│
└─────────────┘ └──────────────┘ └──────────────┘
Both the main model and the pruner are tapped at the one place they are resolved — so the trace holds the actual prompts, not just the rendered replies.
Where it lives
The trace log is written to DefaultTracePath(), a path under your cache directory. At startup cortex prints the path to stderr:
cortex: trace: /home/you/.cache/cortex/trace-20250101-120000.jsonl
It never fails the run
A broken cache directory means no trace, not no cortex. If the trace log can't be opened, cortex prints a notice to stderr and continues without it:
cortex: trace log disabled: <error>
Both models are tapped
The pruner's context decisions are as worth auditing as the agent's, and they are invisible in the event stream — the stream only reports how many tokens a pass moved, never why. Tapping both models at the one place they are resolved is what puts those decisions in the trace, so you can read what the pruner decided to park and what it kept.
Run metadata
The trace opens with a Describe block recording the cortex version, the provider and model for both the main agent and the pruner, and the working directory. This anchors every trace to the exact configuration that produced it.
Why it exists
The trace log is the tool you reach for when the agent did something surprising and you want to know why — what prompt the model actually received, what context the pruner had decided to park, what the model said before the UI rendered it. It's prompt-engineering against reality, not against the abstraction.
📖 The event stream is axon's. axon emits structured events describing every reply — the events reference covers them. The trace log adds the wire layer those events are built on top of. See Reading traces for the tool that views them.
MCP servers
Spawn any Model Context Protocol server at startup; its tools are wired in automatically.
What MCP is
The Model Context Protocol is an open standard for exposing tools, resources and prompts to LLMs. An MCP server is a process that speaks the protocol — GitHub's server, a filesystem server, a database server, anything.
Configuring a server
mcp_servers:
github:
command: docker
args: ["run", "-i", "--rm", "ghcr.io/github/github-mcp-server"]
env: ["GITHUB_TOKEN=ghp_..."]
Each entry under mcp_servers names a server and gives the command, arguments and environment to spawn it. cortex starts the server at startup, discovers its tools over the protocol, and wires them into the agent's tool set.
Auto-discovery
You don't enumerate the tools a server provides — cortex asks the server what it offers and adds every tool to the catalog automatically. If the server adds a new tool, it shows up on the next session with no config change.
Environment variables
The env list is passed to the spawned process. Use it for API tokens and any other secrets the server needs. The values are strings; a leading $ does not expand here (unlike api_key), so pass the literal value or interpolate from your shell before writing the config.
⚠️ Security: an MCP server runs with the same filesystem and network access as the cortex process. Only spawn servers you trust.
Multiple servers
mcp_servers:
github:
command: docker
args: ["run", "-i", "--rm", "ghcr.io/github/github-mcp-server"]
env: ["GITHUB_TOKEN=..."]
sqlite:
command: npx
args: ["-y", "@modelcontextprotocol/server-sqlite", "--db-path", "./data.db"]
Tools from all servers are merged into a single catalog. Name collisions are resolved by the server prefix.
📖 MCP is axon's. cortex spawns the servers in config; axon speaks the protocol, discovers the tools, and merges them with its built-in tools into one catalog. The auto-discovery is axon's — see axon's tools reference for the full toolset cortex gets on top of MCP.
Playground
A harness for watching cortex work and finding out why it works the way it does.
Not a benchmark —
benchmarks/polyglotscores cortex. This one shows its reasoning so a bad habit can be traced to the prompt or tool description that caused it.
The commands
./run.sh <task> [label] # run cortex on one task, keep everything
./batch.sh [-n runs] [task ...] # repeat a suite and keep going on failure
./report.sh [runs/<task-label> ...] # summarize pass/fail, cost and behavior
./trace.sh <view> <trace> # read what happened inside one run
What a run keeps
Each run gets its own directory under runs/ holding a pristine copy of the task's seed project, the trace cortex wrote, cortex's own stdout, and verifier output. The seed is never touched, so a run is repeatable and two runs are directly comparable:
trace.jsonl— the wire-level recorddiff.patch— exactly what the agent changedstdout.txt/stderr.txt— the agent's own outputverify_stdout.txt/verify_stderr.txt— final project verificationstatus.txt— git status of the work tree
The trace
Newline-delimited JSON with one discriminator, kind. Most kinds are axon event names. Three are cortex's own:
| kind | what it holds |
|---|---|
run |
which build and which models produced this trace |
wire_request |
the exact messages sent to a model, and to which one |
wire_reply |
what came back, and how long it took |
The wire kinds are the reason this harness exists. Events are emitted downstream of a request — they describe the reply and never the prompt — so the composed system message, the windowed conversation and the pruner's own curator calls are all invisible without them. Prompt engineering against the event stream alone is guesswork.
The views
trace.sh offers six views, each one jq filter over the trace:
./trace.sh timeline runs/report-final/trace.jsonl
./trace.sh system runs/report-final/trace.jsonl # the prompt under test
./trace.sh prompt runs/report-final/trace.jsonl pruner
./trace.sh tools runs/report-final/trace.jsonl # shows a wrong approach fastest
report.sh is the first thing to read after a suite. It flags failed verification, tool errors, runtime errors, no-op prunes, large call counts, and no-diff runs before you dive into individual traces.
Continue to Running tasks and Reading traces for the details.
Running tasks
run.sh drives cortex through one playground task and keeps everything it did.
Usage
./run.sh <task> [label]
What happens, step by step
run.sh ledger iter1copiestasks/ledger/seedintoruns/ledger-iter1/work.- It commits the seed in a fresh per-run git repo.
git diffin the work directory is then exactly what the agent changed, with no other setup. - A
.gitignoreis written so build artefacts (__pycache__/,*.pyc,.pytest_cache/) don't fill the diff with binary noise and bury the one source change that matters. - It builds the cortex in the working tree — not whatever was on
$PATH. The whole point is to test the code you're working on. - It detects the seed project's verifier and passes it to cortex with
--verifyand one repair attempt by default. - It runs cortex non-interactively on
tasks/<task>/TASK.txt, writing stdout and stderr to the run directory. - cortex announces its trace file on stderr;
run.shcopies it into the run directory so the directory is self-contained once the cache is cleaned. - It runs the seed project's verifier (
go test ./...,python -m pytest -q, ornpm test) and records the result. - It stages all changes and writes
diff.patch— staged so files the agent created show up as additions rather than untracked paths the patch never mentions.
The run directory
runs/ledger-iter1/
├── work/ # pristine seed copy + the agent's changes
├── cortex # the freshly-built binary
├── trace.jsonl # the wire-level record
├── diff.patch # exactly what the agent changed
├── stdout.txt # the agent's answer (stdout)
├── stderr.txt # notices, tool activity, errors
├── verify.status # verifier exit status
├── verify_stdout.txt
├── verify_stderr.txt
└── status.txt # git status of the work tree
Batch runs
./batch.sh -n 3 report pipeline
Batch runs keep going after failures and write a markdown summary under runs/. Use this for prompt or tool changes so one unlucky run does not hide the rest of the suite.
Set PLAYGROUND_REPAIR_ATTEMPTS=0 when you want to measure one unaided turn.
💡 Comparing runs: because the seed is never touched and each run is self-contained, two runs are directly comparable. Diff the
diff.patchfiles, or comparetrace.jsonlwith the trace views.
Reading traces
trace.sh reads a cortex trace. Six views, each one jq filter.
./trace.sh <view> <trace.jsonl> [arg]
timeline
One line per thing that happened, with just enough payload to see the shape of a run. Streaming token noise is dropped — it's the same text the assistant_end line carries in full. Great for getting the overall shape of a run at a glance.
./trace.sh timeline runs/report-final/trace.jsonl
prompt
Every message of one model call, exactly as the provider saw it. Defaults to the last main-model request; pass a call number for a specific one, or pruner for the curator's last.
./trace.sh prompt runs/report-final/trace.jsonl # last main call
./trace.sh prompt runs/report-final/trace.jsonl 3 # call #3
./trace.sh prompt runs/report-final/trace.jsonl pruner # the pruner's last call
system
The system message alone — the prompt under test, plus whatever axon appends (including the tool catalog). This is the view for prompt engineering: it shows the composed system prompt the model actually received.
./trace.sh system runs/report-final/trace.jsonl
tools
What the agent actually did, in order, with full arguments and a clipped result. This is the view that shows a wrong approach fastest.
./trace.sh tools runs/report-final/trace.jsonl
usage
Token spend per call and in total. Reported by the provider, not estimated.
./trace.sh usage runs/report-final/trace.jsonl
errors
Every error, tool error and wire error, with timestamps.
./trace.sh errors runs/report-final/trace.jsonl
💡 The pattern worth keeping: when the agent misbehaves, read
toolsfirst to see what it did, thenpromptto see what it was told. The cause is almost always something it was told, and usually somewhere more structural than the role prompt.
Tasks reference
A task is a seed/ project and a TASK.txt written the way a user would write it.
Seeds ship with a passing test suite so a regression is unambiguous. The debugging tasks are the useful ones — feature work rewards a competent model; debugging rewards one that plans, reads before concluding, and knows what it has not checked.
| task | shape | what it tests |
|---|---|---|
ledger |
small CLI, suite passes | a feature across four files, with a backward-compatibility trap in the on-disk format |
report |
four modules, four failing tests | a bug whose symptom and cause are in different files, with a plausible wrong fix at the symptom |
pipeline |
16 modules, 41KB, three failing tests | a bug visible only by comparing two files neither of which is wrong alone; large enough to put the pruner under load |
⚠️
pipelineis the only task big enough to exercise the pruner. Keep it that way — a curator that never runs is a curator nobody is testing.
What the playground found
Each of these was invisible in the event stream and obvious in the wire log:
Duplicated tool catalog (~2,100 tokens/call) — axon restated the whole tool catalog in the system prompt while the same tools were already sent natively. ~2,100 duplicated tokens on every call.
write steered to whole-file rewrites — Rewriting the tool's prose changed nothing; reordering the mode enum in its JSON Schema changed everything. Where prose and schema disagree, the schema wins.
replace_string's not-found error recommended replace_lines — A run took the advice, used line numbers from the same stale read, destroyed a docstring and spent two calls repairing it.
The pruner fired on an empty log — The pruner fired on a context of 10,229 tokens and sent the curator an empty log — no task, no blocks, 223 bytes. ShouldFire counts tokens while the recency window counts blocks, so a few large files early in a task clear the floor while every block is still inside the window. The curator answered {"park":[]}, correctly, having been shown nothing; the context was unchanged, so it re-fired every growth_tokens after that.
💡 The pattern worth keeping: when the agent misbehaves, read
toolsfirst to see what it did, thenpromptto see what it was told. The cause is almost always something it was told, and usually somewhere more structural than the role prompt.
CLI reference
cortex takes a small number of flags. Everything else lives in config.
Flags
| Flag | Default | Description |
|---|---|---|
--config <path> |
— | Path to a config file instead of the usual cascade. Disables the cascade entirely — only that file is read, plus the environment. |
--prompt <text> |
— | Run one prompt non-interactively and exit. The answer goes to stdout; everything else to stderr. Bounded by maxIterationsOnce. |
--verify <command> |
— | After --prompt, run a verifier command in the working directory. If it fails, cortex exits non-zero unless --repair-attempts allows another turn. |
--repair-attempts <n> |
0 |
Number of extra repair turns allowed after --verify fails. The verifier runs again after each repair. |
--version |
— | Print the version, commit and build date, then exit. |
Modes
| Invocation | Mode |
|---|---|
cortex |
Interactive (the TUI) |
cortex --prompt "…" |
Non-interactive (one turn, then exit) |
cortex --prompt "…" --verify "go test ./..." |
Non-interactive with objective verification |
cortex --version |
Version only |
Exit codes
0— normal exit1— an error occurred (printed to stderr ascortex: <error>)
Build metadata
The version, commit and build date are injected at link time by goreleaser. A source build without ldflags reports cortex dev (none, built unknown).
ℹ️ Why so few flags? Configuration belongs in the config file, not on the command line. The only flags are mode switches and the override path.
Config file reference
Every field in a cortex config file, what it means and what it overrides.
Locations
| Source | Path | Notes |
|---|---|---|
| User config | ~/.config/cortex/config.yaml |
Honours XDG_CONFIG_HOME |
| Project config | ./cortex.yaml |
Overrides user config, field by field |
| Environment | LLM_* |
Overrides both |
| Explicit | --config <path> |
Disables the cascade entirely |
Fields
| Field | Type | Description |
|---|---|---|
name |
string | Labels this agent personality (e.g. cortex, reviewer). Shown in the banner. |
provider |
string | The axon provider name (e.g. openai, openrouter). Supplies a default base_url for known providers. |
model |
string | The model within that provider (e.g. gpt-4o, deepseek/deepseek-v3.2). |
system_prompt |
string | The agent's role text. A path is read from disk; prose is used as written. Omit to use the built-in coding-agent prompt. Must not enumerate tools — axon appends the catalog. |
pruner |
object | The secondary model for context pruning. See below. |
mcp_servers |
map | MCP subprocesses to spawn at startup. See below. |
pruner
| Field | Type | Description |
|---|---|---|
pruner.provider |
string | Provider for the pruner model. Defaults to the main provider if omitted. |
pruner.model |
string | The cheap, fast model used for context pruning. Empty means no pruner. |
mcp_servers
Each key is a server name. Each value has:
| Field | Type | Description |
|---|---|---|
command |
string | The executable to spawn. |
args |
[]string | Arguments passed to the command. |
env |
[]string | Environment variables for the spawned process. Literal values — no $ expansion. |
Environment overrides
Only model selection is overridable by the environment. Credentials and endpoints are axon's concern.
| Variable | Overrides |
|---|---|
LLM_PROVIDER |
provider |
LLM_MODEL |
model |
LLM_BASE_URL |
The provider's default base_url |
LLM_API_KEY |
API key (see API keys) |
⚠️ Providers and credentials are not in this file. cortex does not define endpoints or carry credentials — those live in axon's
providers.jsonor theLLM_*environment. cortex only names which provider and model to use.
API keys
Where the key comes from. Checked in order; the first one that answers wins.
api_keyin axon'sproviders.json— a leading$reads that environment variable at startup, so you never commit the key itself.- The
LLM_API_KEYenvironment variable. ~/.config/agent/providers.json, matched on theprovidername — the shared file the axon-family tools already use. Override its path withAXON_PROVIDERS_PATH.
The third exists so a key has one home on disk. If you already keep credentials in providers.json, omit the key from your cortex config entirely and cortex will find it:
{ "providers": [
{ "name": "openrouter", "base_url": "https://openrouter.ai/api", "api_key": "sk-or-..." }
] }
cortex reads only name and api_key from that file; the model lists and routing options in it belong to other tools and are ignored.
Local endpoints need no key
A localhost endpoint (e.g. ollama running locally) needs no API key. Point LLM_BASE_URL at it and leave the key unset.
The leading $
Wherever a config accepts an api_key value, a leading $ reads the rest as an environment variable name and substitutes its value at startup. This keeps the literal key out of your config file:
{ "providers": [
{ "name": "openai", "api_key": "$OPENAI_API_KEY" }
] }
ℹ️ Why three sources? The cascade lets you keep a default provider in your user config, override only the model per project, and keep the actual credential in one shared file. No copying keys between configs.
atakang7.github.io/cortex · MIT · built on axon
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
cortex
command
Command cortex is a terminal coding agent built on the axon runtime.
|
Command cortex is a terminal coding agent built on the axon runtime. |
|
internal
|
|
|
config
Package config resolves the one thing cortex must decide before it can start: which model to talk to, with what role text, and which MCP servers to spawn.
|
Package config resolves the one thing cortex must decide before it can start: which model to talk to, with what role text, and which MCP servers to spawn. |