cxagent 0.9.24
dotnet tool install --global cxagent --version 0.9.24
dotnet new tool-manifest
dotnet tool install --local cxagent --version 0.9.24
#tool dotnet:?package=cxagent&version=0.9.24
nuke :add-package cxagent --version 0.9.24
CXAgent
<div align="center">
<img src="docs/logo.svg" alt="" width="72" height="72">
</div>
nickprotop.github.io/cxagent — install instructions, the plugin marketplace, and docs.
A terminal AI coding agent built on SharpConsoleUI.
Give it a goal in plain language. It reads your files, works out what to change, and changes them — in one context, with the real bytes in front of it. Anything outside your working folder asks first.
Bring your own model: Ollama, any OpenAI-compatible endpoint, or Anthropic.
<div align="center"> <img src="docs/images/cxagent.png" alt="cxagent analysing a repository, with a sub-agent's report and the live session panel" width="900"> <br> <sub>A worker agent explored the repo and reported back — 214,073 of the session's tokens spent inside the child, and 4% of the parent's context used.</sub> </div>
See it working →
One session closing three features in another repo, with the mistakes left in — the full walkthrough
is on the site; docs/screenshots/ has the raw captures if you'd rather read
them here.
Quick Start
Option 1: One-line install (Linux/macOS, no .NET required)
curl -fsSL https://raw.githubusercontent.com/nickprotop/cxagent/master/install.sh | bash
cxagent
Windows (PowerShell)
irm https://raw.githubusercontent.com/nickprotop/cxagent/master/install.ps1 | iex
Option 2: As a .NET tool (requires the .NET 10 SDK or runtime)
dotnet tool install -g cxagent
cxagent
Updates with dotnet tool update -g cxagent. Unlike the binaries above this one is
framework-dependent — it uses the .NET you already have rather than carrying its own.
Option 3: Build from source (requires .NET 10 SDK)
git clone https://github.com/nickprotop/cxagent.git
cd cxagent
./build-and-install.sh
You do not need to write any configuration to start. On first run — with no config file, or none that names a working provider — a setup wizard asks for a provider and model and writes the file for you. It is never stored in the repository.
It lands in ~/.config/cxagent/config.json on Linux ($XDG_CONFIG_HOME when set),
%APPDATA%\cxagent\config.json on Windows, and
~/Library/Application Support/cxagent/config.json on macOS. The same directory holds your session
history and logs.
Building your own?
Everything under the terminal ships as CxAgent.Core — sessions,
agents, tools, sub-agent delegation, permissions and MCP, with no UI dependency. cxagent is one
consumer of it; a Spectre.Console front end is another, in a
hundred lines.
dotnet add package CxAgent.Core
What it does
Run it in the folder you want to work in, and type what you want:
add an overflow guard to EstimateOutputLength in HexEncoder.cs
It reads the file, finds the method, and edits it in place — matching the surrounding indentation and style, because it is looking at the actual text rather than reconstructing it from memory.
Tools
| Tool | Purpose |
|---|---|
read_file |
Read a file, or a line window of it (offset/limit) |
write_file |
Write a whole file |
replace_in_file |
Replace an exact passage, leaving the rest untouched |
glob |
Find files by path pattern, e.g. **/*.cs |
grep |
Search file contents, literal or regex |
run_shell |
Run a command, or start one in the background (background) and be told when it exits |
http_request |
Call an HTTP endpoint |
web_fetch |
Read a web page as text, markup stripped |
skill |
Load a skill's instructions on demand |
todowrite |
Keep a task list across a long job |
ask_user |
Ask the user questions — options with descriptions, several per call |
agent |
Delegate a job to a sub-agent (fan-out mode) |
agent_send |
Ask a sub-agent you already spawned for more — it still has its own context |
agent_list |
See which sub-agents this session has spawned |
job_list |
Background commands running now, across every agent in the session, and where each one's output is going |
job_kill |
Stop one by pid — your own, or any of them if you are the session agent |
A long command does not have to hold the turn. run_shell with background: true starts the
command and returns at once with its pid and the path its output is going to — no stdout, because
nothing has been printed yet — and the agent is told separately when it exits, with the exit code. A
command that is still running at its timeout_seconds (120 by default) is handed over the same way
rather than killed, so the work is not thrown away and re-run; shellDetachOnTimeout: false in
config kills it at the deadline instead. Sixteen background commands may run at once, and past that
background: true is refused rather than queued — the refusal names job_list and job_kill, since
ending one is what frees a slot.
F7 shows this session's background commands in the right-hand column, and Enter on one follows its output in a tab. /jobs lists every background command running, and /jobs kill <pid> stops one — the pid
completes in the palette, so nothing has to be retyped from a row you just read. The model has the
same reach through job_list and job_kill, with one restriction a user does not have: the session
agent may stop any job, but a sub-agent may stop only its own, and the refusal names who owns it.
There is no permission prompt on a kill — the command was already approved when it was backgrounded.
A detached command is killed when cxagent exits, and one left behind by a crash is killed by the next
launch.
A long output arrives as its tail. Above 8,192 characters run_shell returns the END of the
stream, plus stdout_spill — a file in the job's log directory holding everything the call captured,
head included — and stdout_bytes, the true size. The end is where a command's answer is: a failing
build's error and its summary are last, while a head would show the compiler banner. A command that
was then handed over at its deadline keeps printing to background.out in the same directory, so
reading all of one takes both files.
run_shell also takes env, a set of environment variables added to the command's own.
A sub-agent is kept, not thrown away. When one finishes, its context — everything it read, ran
and concluded — stays for the life of the session, and agent_send asks it more on that same
conversation. That is far cheaper than spawning a second agent to cover the ground the first already
covered, which would have to redo the work to get back to where the first one already was. The three
tools are offered only to an agent that can spawn, so a sub-agent cannot reach a sibling.
One name per tool, and no aliases. Several of these have been renamed — glob was
list_files, grep was search_files, agent was spawn_agent then task, ask_user was
question. Each old name was accepted for a short window afterwards so a conversation resumed across
the change would not fail on a name it had seen in its own history. Those windows are closed: a call
under an old name gets no such tool, with the current names listed beside it.
todowrite keeps its unusual spelling deliberately — it is what Claude Code calls it, and models
distilled from it recognise the exact string.
Sessions
One window holds several conversations. /sessions new [folder] opens another in its own tab —
on the same project or a different one. Each is a whole session end to end: its own history, model,
working directory, plugins and permissions.
That last part is the one worth stating plainly. A permission answered in one session has not been answered for the others, a plugin unwired in one keeps running in the rest, and a turn stopped in one leaves the others alone. Two conversations in one window are two conversations, not one conversation with two views.
F6 focuses the tab strip, ← and → change tabs, F6 returns to the composer. A tab whose
session is waiting on a permission prompt says so, so a session that needs you cannot sit quietly
behind the one you are reading.
/btw <question> asks a side question without adding it to the conversation. A side agent that
starts from what the main agent knows answers in its own block, under this session's permissions, and
can only read. Enter keeps the side thread going; Esc stops an answer, then leaves. See
COMMANDS.md.
Permissions
Reading and writing inside the working folder is free, and so are commands that can only look —
ls, cat, grep, and cd into the folder before one. Anything that can write, and anything
outside the folder, stops and asks with Allow once, Always allow, or Deny.
"Always" grants the command's NAME, not the exact string. Approving git status stores
git status*, so the next git status --short runs — while git push still asks, because a
subcommand is part of what names a command. The prompt shows the rule it would write before you
grant it.
Grants are remembered per folder, and a folder is identified by more than its path: delete a folder and recreate it, and its old grants do not apply to the new one.
auto lets a model decide some of what would otherwise ask. In a trusted folder, a shell command
the static check refused is no longer a guaranteed prompt — it goes to the classifier first, and an
allow runs it silently. This is only for commands already confined: every path in the command
(including a cd target) has to resolve inside the working folder, the command has to be fully
parseable with no $(...) or backticks, and neither an egress verb like curl or scp nor a
recursive delete like rm -rf is ever eligible —
those stay outside what a verdict can silence, no matter what the model says about them. The
classifier can also deny an action outright, with a reason you're shown. Every failure —
timeout, transport error, an answer it can't parse — means ask, same as if there were no classifier.
Auto-decided rows are marked auto-approved / auto-denied so you can tell a model's call from a
stored rule or a silent boundary pass.
Sub-agents
Fan-out is the default. The agent can delegate a job to a sub-agent: a second agent with its own context, its own conversation, and its own compaction. It runs, returns one message, and stops.
What that buys is room. A search that reads thirty files to answer one question fills a context with material nobody needs afterwards — delegated, the parent keeps the conclusion and not the file dumps. Measured on a real repo: 23k characters in the parent instead of 213k, for the same answer.
A sub-agent's work is not shown in the transcript. It gets one row, showing live turns, context occupancy and elapsed time; expand it to see what the child is doing, or its report once it has finished.
/mode agent single turns delegation off for a session, /mode agent fan-out turns it back on, and
--mode single starts that way. Single mode's prompt is what shipped before sub-agents existed —
turning it off really does turn it off.
The axis is named because there is more than one, and /mode on its own reports every axis rather
than guessing which one you meant.
/mode edits decides when a file write happens without asking. accept-edits — the default —
keeps writes inside the working directory silent and asks everywhere else; always-ask prompts for
every one. Shift+Tab cycles it from the composer. It names what cxagent already did rather than
granting anything new, and it cannot widen past your trust decision: on a folder you did not trust,
accept-edits still asks for everything, and the listing says so. Writes to .git/, .vscode/,
.claude/ and .idea/ keep asking regardless — a hook that runs on your next git command is not
what anyone means by "accept edits".
Named types say how a delegated job should be done. Five ship with cxagent and need no
configuration — including a planner that writes a plan to ./plans/ and reports what it decided,
and a builder that implements one and refuses to start without it. Their briefings live in the
program rather than in your config file, because a briefing is the contract a type keeps with the code
around it: cxagent names the file a planner must write, checks afterwards whether it is there, and the
builder refuses work that arrives without one. Config still chooses where a type runs (provider) and what
it may spend (maxTurns), and any name cxagent does not ship is entirely yours, briefing and all.
See CONFIG.md for the agents block, and COMMANDS.md for /mode and
/stats.
Honest about the limits, because they are the sort you would otherwise find out the hard way:
- Delegation depends on your model, and what we measured is one model. On a local
qwen3.6-35b-a3b, it delegates readily when asked — say "use a sub-agent to…" and it will — and rarely on its own judgement, usually doing the work inline instead. A stronger model may well choose to delegate unprompted; we have not measured one. Treat the guidance as a starting point rather than a property of the tool. - Sub-agents in one message run concurrently, and the parent waits for all of them before it answers. No child outlives the turn that started it — the message format requires every tool call to be answered in the same turn, so a background agent has nowhere to put its result.
- A sub-agent cannot spawn its own — not a rule it is asked to follow, a tool it is never given.
- A briefing is a request, not a permission. "Never edit files" in a type's briefing asks the agent not to; it does not remove the tool. Permissions are the mechanism that constrains an agent, and they apply to sub-agents exactly as they apply to the main one.
Skills
Instructions the model loads when it needs them, instead of carrying them on every turn.
Put a SKILL.md in .cxagent/skills/<name>/ with a description saying when it applies. Only the
name and description ride in the prompt; the body is fetched by a tool when the model decides a task
matches — so twenty skills cost a few hundred characters instead of sixty thousand.
---
name: double-entry-posting
description: Use when adding or fixing ledger posting or balance logic in this
repo. Covers house rules that are not obvious from the code.
---
Every posting must sum to exactly zero. Reject unbalanced transactions with
ERR-UNBALANCED in the message.
/skills lists what was found — and, more usefully, every SKILL.md that was skipped and why,
because a file with broken frontmatter is otherwise invisible: you wrote it, nothing happened, and
there was no error anywhere. The session panel shows which skills are currently in force.
Already have .claude/skills/? ln -s .claude/skills .cxagent/skills — the prose loads, and
cxagent's own permission gate still governs every tool call, so no allowed-tools grant comes with
it.
Same caveat as delegation: whether a model reaches for a skill is a property of the model. On a
local qwen3.6-35b-a3b it does — it announced "this is a double-entry bookkeeping task, so let me
load that skill first" and loaded it unprompted — but only after the prompt was changed to say
explicitly that reading the file directly is not the same thing. Before that, the model found the
SKILL.md with the file-listing tool and read it: the instructions arrived, and nothing else in
the session knew a skill was in force.
See CONFIG.md for where they live and how shadowing works.
Plugins
Tools from a DLL you drop in a folder, without rebuilding cxagent.
Two ship: csharp-lsp — go-to-definition, find-references and diagnostics for C#, backed by a language server, crossing project boundaries as grep cannot — and calculator, which evaluates a whole arithmetic expression exactly in one call.
Neither is bundled with the installer. Both arrive through the plugin manager (F2), which downloads, verifies and unpacks into your plugins folder and stops there. cxagent tells you a plugin is present and leaves it alone:
plugin 'csharp-lsp' found in ~/.config/cxagent/plugins (3 tool(s)), no auto load.
Turn it on for one session, with no file to edit:
/plugin load csharp-lsp.dll
or keep it, in config.json:
"plugins": { "csharp-lsp": { "file": "csharp-lsp.dll" } }
Either way it asks once whether to trust the binary, showing a hash of its contents — installing a
plugin is not approving it, and nothing in config can pre-approve one. It needs a C# language
server on PATH: dotnet tool install -g csharp-ls.
Its lookups run unasked inside the folder you started in, and ask before reading a file outside it — a plugin decides that per call, from the arguments, rather than being on or off for every call.
Browse plugins at https://nickprotop.github.io/cxagent/plugins.html. Writing one starts at https://nickprotop.github.io/cxagent/writing-a-plugin.html; the full contract is in cxagent.Core/docs/plugins.md.
Configuration
~/.config/cxagent/config.json:
{
"providers": {
"local": { "kind": "ollama", "model": "qwen3:32b", "baseUrl": "http://localhost:11434" }
},
"defaultProvider": "local"
}
Provider kinds: ollama, openai-compatible (requires baseUrl), anthropic.
Set contextWindow on a provider when you know it — it is the denominator for the occupancy
readout and the trigger for compaction. Left unset, cxagent asks the endpoint at startup.
Optional blocks: agents for sub-agent types, mcp for MCP servers, plugins for tools loaded from
disk, orchestrator for caps.
CONFIG.md is the full reference — every block, where the file lives on each OS, what
else cxagent keeps in that directory, and how AGENTS.md / CXAGENT.md / CLAUDE.md are resolved.
config.sample.json documents every key inline, including what each one's
absence means.
ROADMAP.md is what is next.
Command line
cxagent # start in the current folder
cxagent --resume [<id>] # continue the most recent session here, or one by id
cxagent --sessions [all] # list earlier sessions and exit
cxagent --model <instance> # open on a specific entry from `providers`
cxagent --mode <single|fan-out> # start in that mode
cxagent --config-dir <dir> # use this directory instead of the usual one
cxagent --theme <name> # start in a named theme
cxagent --mock # a fake provider, to try the UI
cxagent --version
cxagent --help # the options, also shown when an argument is wrong
--config-dir points cxagent at a different config directory — the one holding config.json,
the session database and the logs. The path is used AS the directory, with no cxagent folder
appended, unlike XDG_CONFIG_HOME. It is the quickest way to try something against a clean setup
without touching your own:
cxagent --config-dir /tmp/scratch
--theme starts in a named theme. Names are case-insensitive, and one that matches nothing falls
back to cxagent's own rather than refusing to start — a colour scheme is not worth failing over.
--help lists what is installed. A theme key in config.json does the same thing for every run;
the argument wins when both are present.
Keys
| Key | Action |
|---|---|
Enter |
Send. During a running turn, queues instead — several queued messages go as one prompt |
Esc |
Stop the running turn. Anything queued goes back into the composer rather than being lost. In btw mode: stop the aside's answer, then leave btw mode |
F1 |
Help |
F3 |
Session panel (show / hide / automatic) |
F4 |
Put the cursor back in the composer |
F6 |
Focus the tab strip — then ← → to change session, F6 back to the composer |
F7 |
Jobs panel — background jobs in the right-hand column; ↑ ↓ and Enter open one. F7 again hides it |
F9 |
Theme |
Shift+Tab |
Cycle the edit mode — see COMMANDS.md |
Ctrl+Q |
Quit |
Commands are typed in the composer — see COMMANDS.md.
What this thing actually does to your machine
Read this once. It is short, and every line of it is something the software genuinely does.
It edits your files. Inside the working folder it writes without asking — that is the point of
it, and it is why you should run it in a git repository with your work committed. git diff is the
review step. There is no undo inside cxagent.
It runs shell commands. Anything outside the working folder, and every shell command, asks first — Allow once, Always allow, or Deny. "Always" is remembered per folder. Read what you are approving: the command is shown in full, and "always allow" means the next one like it will not ask. A model can propose a command that deletes things, and if you approve it, it runs.
In auto mode, a model can run a shell command without asking you first. This is bounded — the
folder has to be trusted, the command has to resolve entirely inside it, and things like $(...)
substitution, curl/scp-style egress and rm -rf-style recursive deletes are never eligible,
whatever the model decides — but the decision to skip the prompt is still made by a language model,
and language models are wrong sometimes. That trade is the point of the mode: fewer interruptions on the commands most likely to be
fine, at the cost of trusting a model's judgment on the ones it approves without you seeing them
first. If you want every shell command to ask, don't use auto.
It spends your money. Every turn is a request to whichever provider you configured, and a
sub-agent is a whole second run of turns. A single delegated search can cost several hundred thousand
tokens. The session panel shows the running total and the parent/worker split, /stats shows what
past sessions cost and which tools fill the context, and orchestrator.maxTurns stops a request
after a set number of turns. A turn cap bounds iterations, not spend — one turn that reads a
large file costs more than ten that do not, so it is a backstop against a runaway, not a budget. If you point cxagent at a paid API, you are paying for what
it does, including work that turns out to be wrong.
A measured example, on a local model where the cost was only electricity: one evening of six sessions came to roughly two million input tokens, because every turn re-sends the whole conversation and one sub-agent made ninety-five web requests before its turn cap stopped it. On a metered API that is real money for a result that was partly wrong.
It talks to whatever you configure. Your prompts, your file contents and your shell output go to your chosen model provider, and to any MCP server you have enabled. What they do with it is between you and them.
A plugin is code you chose to run. A plugin is a DLL loaded into cxagent's own process, so it can do anything cxagent can — it is not sandboxed, and no permission gate constrains what it does internally. What cxagent can enforce is the decision to load it at all: it asks once, showing a hash of the plugin's whole contents, and asks again if a single byte of it changes. Nothing in config can pre-approve one. Past that, a plugin gates its own operations, or does not; trusting a plugin is trusting its author, exactly as installing any other software is.
A sub-agent is an agent. It inherits its parent's tools and the same permission gate. A type's briefing — "never edit files" — is a request written into its prompt, not a sandbox. Models do not reliably follow instructions they are given. Permissions and tool selection are the mechanisms that constrain an agent; prose is not.
A tool selection narrows what an agent is offered, and a withheld tool is refused if called by name rather than merely hidden — so it constrains rather than suggests. It is not a security boundary on its own: it decides what this deployment offers, while the permission gate decides what any offered tool may do.
Nobody is responsible for the results but you. cxagent is provided as-is under the MIT licence, with no warranty of any kind. The authors are not liable for lost work, deleted files, broken builds, leaked secrets, provider bills, or anything an agent does with the access you granted it. Review the diff. Keep backups. Do not run it against anything you cannot afford to have changed.
Uninstall
However you installed it:
# the install script (Linux/macOS)
curl -fsSL https://raw.githubusercontent.com/nickprotop/cxagent/master/uninstall.sh | bash
# the .NET tool
dotnet tool uninstall -g cxagent
irm https://raw.githubusercontent.com/nickprotop/cxagent/master/uninstall.ps1 | iex
Your configuration and session history are left alone — they live in
$XDG_CONFIG_HOME/cxagent (%APPDATA%\cxagent on Windows, ~/Library/Application Support/cxagent
on macOS). Delete that directory to remove them too.
The cx family
cxfiles · cxpost · cxlog · cxnet · cxgpu · cxshell
Star it
If cxagent is useful to you, a star helps others find it.
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.9.24 | 0 | 10/14/2026 |
| 0.9.23 | 0 | 10/14/2026 |
| 0.9.22 | 109 | 10/9/2026 |
| 0.9.21 | 133 | 9/13/2026 |
| 0.9.19 | 165 | 9/13/2026 |
| 0.9.18 | 107 | 9/12/2026 |
| 0.9.17 | 117 | 9/10/2026 |
| 0.9.16 | 113 | 9/8/2026 |
| 0.9.15 | 118 | 9/5/2026 |
| 0.9.14 | 125 | 9/4/2026 |
| 0.9.13 | 108 | 9/4/2026 |
| 0.9.12 | 121 | 9/3/2026 |
| 0.9.11 | 108 | 9/3/2026 |
| 0.9.10 | 110 | 9/1/2026 |
| 0.9.9 | 109 | 9/1/2026 |
| 0.9.8 | 113 | 8/29/2026 |
| 0.9.7 | 123 | 8/28/2026 |
| 0.9.6 | 109 | 8/28/2026 |
| 0.9.5 | 121 | 8/25/2026 |
| 0.9.4 | 105 | 8/25/2026 |