An open source, hackable agentic coding harness, built in Rust and powered by Oxen.ai.
All you need is an Oxen.ai API key
git clone https://github.com/gschoeni/oxen-harness && cd oxen-harness
export OXEN_API_KEY=...
cargo run -p harness-cliThat's it — you're on the trail.
No API key or internet handy? Run an open-weight model fully offline instead (see Running models locally):
cargo run -p harness-cli -- --local qwen3-0.6boxen-harness runs an agent loop against any model exposed through the Oxen.ai OpenAI-compatible chat completions API, with first-class tool calling for editing code, running commands, and more. Every turn is persisted so you can later export your sessions and fine-tune a model on your own coding traces.
- Hackable & open source (Apache-2.0). A small, readable Rust workspace you can fork and extend.
- Extend it at three levels, no forking required. Teach the agent reusable workflows with skills (a markdown file — no code), connect your own HTTP endpoints as custom tools from the desktop app's Settings, or add a built-in tool in Rust (
AGENTS.mdhas a start-to-finish recipe). - Bring your own model. Anything with a chat completions endpoint and tool calling — default is
claude-opus-4-8via Oxen.ai, or run models locally with llama.cpp (--local). - Your data, exportable. Full conversation + tool-call history in SQLite, with a JSONL exporter for fine-tuning.
- Two front ends. A
claude-style interactive CLI and a cross-platform Tauri desktop app.
For the layering, the lifecycle of a turn, and how to extend the harness, see
ARCHITECTURE.md. At a glance, it's a single Cargo workspace
of focused crates:
| Crate | Responsibility |
|---|---|
harness-core |
Shared domain types (messages, roles) and defaults |
harness-config |
Shared config plumbing: ~/.oxen-harness paths, versioned JSON files, secrets in .env |
harness-llm |
Oxen.ai chat completions client: tool calling + SSE streaming, lightweight auth |
harness-tools |
The TypedTool trait + built-in tools: read/write/edit files, glob find, regex search, sandboxed shell, git, web search, web page fetch, interactive questions, canvas documents, plans, skills, and user-defined HTTP tools |
harness-compress |
Reversible context compression for stale tool output before it goes on the wire |
harness-store |
SQLite history (verbatim) + JSONL export for fine-tuning |
harness-local |
Local models: config-file GGUF catalog, downloads + disk tracking, offline-first resolution, llama-server launcher |
harness-theme |
Configurable, shareable themes (palette + voice + style): built-ins, TOML/JSON load/save, partial overrides |
harness-oxen |
Version control for config, project data, and shareable traces, via the oxen CLI |
harness-agent |
The agent loop, wiring the LLM, tools, and store together |
harness-loop |
Goal-driven, self-verifying loops: gates, journal, stop conditions |
harness-runtime |
Front-end-agnostic runtime services shared by the CLI and desktop app |
harness-cli |
The oxen-harness interactive REPL binary |
A cross-platform Tauri desktop app lives in app/ (a separate project, excluded from this workspace) and reuses harness-agent. It needs the Tauri CLI, which is not bundled with cargo — install it once, then run from app/:
cargo install tauri-cli --version "^2" --locked # provides `cargo tauri`
cd app
OXEN_API_KEY=sk-... cargo tauri devSee app/README.md for the npm-based alternative (npm install && npm run dev) and platform webview prerequisites.
| Setting | Source | Default |
|---|---|---|
| API key | OXEN_API_KEY env, or ~/.config/oxen/auth_config.toml ($OXEN_CONFIG_DIR to override) |
— (required) |
| Base URL | --base-url flag, OXEN_BASE_URL env, or --host/OXEN_HOST (expanded) |
https://hub.oxen.ai/api/ai |
| Model | --model flag |
claude-opus-4-8 |
| Resume | --resume <SESSION_ID> flag (id printed on the death screen) |
new session |
| Web search | BRAVE_API_KEY env (or BRAVE_SEARCH_API_KEY), or ~/.oxen-harness/.env |
always offered; key enables results |
| Local model | --local <MODEL_ID> flag (runs llama.cpp instead of a remote endpoint) |
remote Oxen.ai |
| Theme | /theme in the REPL or oxen-harness theme use <name> (persists to ~/.oxen-harness/config.toml) |
Oregon Trail |
To use a local or self-hosted Oxen server, override the base URL — by host or full URL:
# Convenience: just the host[:port] (http is used for local hosts, /api/ai appended)
oxen-harness --host localhost:3001
# Explicit full base URL (any scheme/path)
oxen-harness --base-url http://localhost:3001/api/aiPrecedence: --base-url > --host > OXEN_BASE_URL > OXEN_HOST > default. The
API key is looked up by the resolved host (e.g. localhost:3001), so OXEN_API_KEY
or an oxen CLI login for that host works automatically. The desktop app honors
the same env vars.
The web_search tool uses the Brave Search API
and is always available. Set BRAVE_API_KEY (or BRAVE_SEARCH_API_KEY) in the
environment or ~/.oxen-harness/.env — or just paste a key when prompted after
a failed search; both front ends turn the missing-key error into an inline
prompt so you can enable it mid-conversation and retry.
The web_fetch tool reads a single page into context — the "read" half that
pairs with web_search's "find": the agent searches, then fetches a promising
URL for its full text. It does an HTTP GET (defaulting a missing scheme to
https://), and when the server returns HTML it converts the page to clean
Markdown (htmd — dropping <script>,
<style>, <nav>, and <head>), so the model gets prose instead of markup.
Markdown/plain-text responses pass through untouched. Results are capped
(default 100K chars, overridable per call via max_chars) so one fetch can't
dominate the window, and the compression layer shrinks them further. No API key
required. It fetches localhost/private addresses too, so the agent can pull in
docs from a dev server or internal wiki.
When a decision is genuinely ambiguous, the agent can interview you with the
ask_user_question tool instead of guessing — 1–4 questions, each with 2–4
options, mirroring Claude Code's AskUserQuestion. The CLI renders an
interactive picker (with a row for typing your own answer), the desktop app a
question card. Piped/non-interactive sessions skip the prompt and the agent
proceeds with sensible defaults.
Instead of a remote endpoint, oxen-harness can run open-weight models on your
own machine via llama.cpp's
llama-server, which speaks the same OpenAI-compatible API — so the agent (and
all its tools) work identically, fully offline, with no API key.
1. Install llama-server (one time):
brew install llama.cpp # macOS
# Linux/Windows: a release from https://github.com/ggml-org/llama.cpp/releases
# Or point at any build: export LLAMA_SERVER=/path/to/llama-serverUse a current build for newer formats. The desktop setup manages a pinned
Apple Silicon build (b10002); if Homebrew is already installed, run
brew upgrade llama.cpp before using Bonsai 27B.
2. Browse and download a model. The catalog lives in configuration files,
not code: Qwen3 at Q4_K_M (0.6B through 32B and 30B-A3B), plus
Bonsai 27B at its
native 1-bit and ternary formats. Your own additions live in
~/.oxen-harness/local-models.json:
oxen-harness models list # catalog + everything downloaded, sizes, disk used
oxen-harness models pull qwen3-8b # download with a live progress bar
oxen-harness models pull bonsai-27b # 27B reasoning/tool calling in ~3.8 GB
oxen-harness models remove qwen3-8b # reclaim the disk (also clears stale .part files)To add your own model (or override a built-in by reusing its id), drop an
entry in ~/.oxen-harness/local-models.json — only id, repo, and file
are required:
{
"schema_version": 1,
"models": [
{ "id": "my-model", "repo": "owner/My-Model-GGUF", "file": "My-Model-7B-Q4_K_M.gguf" }
]
}An entry names one exact GGUF by default. If—and only if—the repository ships
the standard sibling filenames (Q8_0, Q6_K, …, Q3_K_M), add
"derive_quants": true to offer that ladder. This makes a new native format a
safe JSON-only addition instead of guessing download URLs. The Bonsai entries
use the standalone language weights for text, reasoning, and native tool calls;
optional vision projectors and speculative-decoding drafters are not presented
as standalone models.
bonsai-27b is the broadly supported stock-llama.cpp choice.
ternary-bonsai-27b uses the higher-quality mainline Q2_g64 file and currently
targets CPU or Metal; CUDA users should use the 1-bit entry or point
LLAMA_SERVER at PrismML's fork. See the
official demo compatibility table.
3. Ride it. --local starts llama-server for the session (auto-downloading
the model first if needed) and points the agent at it:
oxen-harness --local bonsai-27bAnything already downloaded — a catalog model at any quant, a model installed
from the desktop app's Hugging Face search, or a GGUF you dropped into the
models directory yourself — starts fully offline: --local <id> resolves
weights on disk first and only reaches for the network when there's nothing to
serve. Weights live in ~/.oxen-harness/models/. Match the model to your
hardware (roughly: 0.6B–4B on a CPU/small GPU, 8B–14B on an 8–12 GB machine;
the 1-bit Bonsai 27B also fits there despite its parameter count; conventional
32B or 30B-A3B Q4 builds want a 24 GB card). The desktop app exposes the same catalog under
🐂 Local models.
The whole personality of the harness is a theme: a palette (named semantic colors), a voice (the prompt, spinner phrases, per-tool verbs, banner art), and a style (the desktop app's typography and framing). Both front ends render from the active theme. Five ship built in — Oregon Trail (default, 8-bit pixel), Synthwave, Midnight, New York Times, and Cupertino — and they look genuinely different, not just recolored.
Themes are a single self-contained TOML file (also readable as JSON), so they're
trivial to export, import, and share. Files can be partial — override just a
few colors or phrases and the rest inherits the default. They live in
~/.oxen-harness/themes/.
oxen-harness theme list
oxen-harness theme use Synthwave
oxen-harness theme export Synthwave ./synthwave.toml # share this file
oxen-harness theme import ./a-friends-theme.tomlInside the REPL, /theme opens an interactive picker. To vibe-code a
brand-new theme, run /theme new (optionally with a description): a short
interview asks for the mood, color inspiration, and voice, then the model
designs a complete theme, saves it, and activates it live. The desktop app has
the same controls under 🎨 Theme.
The REPL borrows its structure from modern coding CLIs (a welcome panel, an in-place status spinner, transparent tool lines) and its voice from the 1980s Oregon Trail game — because oxen pull the wagons on the trail, and Oxen.ai powers this one. While the model thinks or a tool runs, you'll see trail-flavored status lines animate in place ("Fording the river…", "Yoking the oxen…"), tools show what they're doing, and errors are reported as the classic "You have died of dysentery."
🐂 trail ❯ add a test for the parser
✶ Caulking the wagon to float across… (3s)
◆ Reading the trail guide read_file(src/parser.rs)
└─ 142 lines forded.
The menu mirrors the game's title screen (/help):
You may:
1. Travel the trail — just type what you want done
2. Learn about the trail — /help
3. See the Oregon Top Ten — /export [path] (save the journey as JSONL)
4. Trade your oxen — /model [name]
5. Change your colors — /theme (select, create, import, export)
6. Pack the wagon — /queue add <msg> … then /queue run
7. Set the wagon rolling — /loop run [name] (work until the gate is green)
8. Inspect the wagon — /code-review [branch] (find → verify → report)
9. Make camp / End — /exit (or Ctrl-D)
While the agent is thinking, streaming, or running tools, the composer stays
live — keep typing and press Enter to stack messages (the prompt shows the
depth: [2 queued] ❯), and they drain automatically, in order, as each turn
finishes. Stacked messages render as a navigable list above the composer:
↑ steps into it, Enter/e edits a message inline, and d removes
it. The desktop app has the same live queue above its composer.
Piped / non-interactive sessions fall back to explicit commands: /queue add <msg>, /queue to list, /queue edit|up|down|rm <n>, and /queue run.
A prompt hands the agent an instruction. A loop hands it a job, a way to
know when the job is done, and a rule for when to give up. Each pass runs
DISCOVER → QUESTION → PLAN → EXECUTE → VERIFY → ITERATE, with a journal fed
into the next pass (and saved for resuming) and hard stop conditions (an
iteration cap plus an optional token budget).
The heart of a loop is VERIFY — a gate that can actually fail the work, so the agent makes real progress instead of agreeing with itself on repeat:
- Command — a shell command in the workspace; exit 0 = pass. The
strongest, most objective gate (e.g.
cargo test). - Rubric — a separate, strict checker scores the work 1–10 against your criteria and passes only if every score clears a threshold — for when "done" can't be reduced to an exit code.
oxen-harness loop run default # keep working until fmt + clippy + tests are green
oxen-harness loop run --goal "make every test in crates/parser pass" --max-iterations 6
oxen-harness loop new # short interview → saved TOML you can shareThe same commands work inside the REPL as /loop …. Loops live as shareable
TOML under ~/.oxen-harness/loops/; default, green-tests, and
clean-clippy ship built in.
/code-review runs a configurable multi-step review pipeline over your
changes — the working diff by default, or PR-style against a base branch:
🐂 trail ❯ /code-review # staged + unstaged + untracked
🐂 trail ❯ /code-review main # everything vs. the merge base with main
🐂 trail ❯ /code-review steps # show the configured pipeline
The default pipeline borrows the shape the strongest production reviewers (Claude Code, Codex) converged on, with every reviewer running on a fresh, isolated agent so nothing anchors the next step:
- find — three recall-biased reviewers in parallel, each with a narrow lens: a line-by-line diff scan, a removed-code audit (what invariant did each deleted line enforce, and where is it re-established?), and a callers trace (which call sites does the change break?). All are told explicitly not to self-censor: every candidate with a nameable failure scenario goes through.
- verify — an adversarial pass that tries to refute each candidate against the actual source, returning CONFIRMED / PLAUSIBLE / REFUTED with quoted evidence. Only CONFIRMED and PLAUSIBLE survive.
- report — dedups, ranks most-severe first (P0–P3), caps the list, and
emits structured findings:
file:line, a one-line title, and the concrete failure scenario.
While the finders run, each one is a live lane — a themed status line with what it's reading, its token spend, and its clock. Press 1-9 to watch any lane's output stream, esc for the overview, ctrl-c to stop. Any step can be split into parallel agents (or back) in Settings → Code review.
The findings then land in the conversation as a settled exchange, so the
natural next message — fix 1 and 3 — hands them straight to the agent to
repair. Every step's prompt is editable: the pipeline lives in
~/.oxen-harness/code-review.json (add, remove, reorder, or rewrite steps),
with a full editor in the desktop app under Settings → Code review. The
desktop composer has the same pipeline behind its Review button.
The fleet isn't just for reviews. The model itself can fan work out from any
turn — a chat in the CLI or desktop app, a loop pass — with the
spawn_agents tool: up to six parallel agents, each with its own prompt and a
fresh context plus the full tool set (except spawn_agents itself — one level
deep, no fork bombs). Ask for it naturally:
🐂 trail ❯ search the codebase for every place we touch the filesystem —
split the work across a few parallel agents
Each subagent appears as a live lane while it works. In the CLI's interactive composer, alt+1-9 switches which lane you're watching (alt+0 for the overview); in the desktop app the same lanes render as a panel above the composer — click a lane to watch that agent's output stream, click again to collapse. Results come back labeled by agent, one section each, and the model carries on with them in hand. Stopping the turn stops the fleet.
Every session is saved to ~/.oxen-harness/history.sqlite. When you quit, the
tombstone screen engraves your session id and the command to pick the trail
back up:
Your trail journal was saved. Resume this expedition with:
oxen-harness --resume 8f3c…
Resuming restores the full transcript (so the model keeps its memory) along with
that session's working directory and model; override either with --workspace
or --model.
Three concepts cover everything the agent can be taught:
- Tools are what the agent can do — read a file, run a command, search the web, call your API. Every tool's name, description, and schema are sent to the model on every request.
- Skills are what the agent knows how to do — a workflow, a house style,
a procedure, written as markdown. The model sees only each skill's name and
one-line description, and pulls the full instructions into the conversation
on demand via the built-in
skilltool. - Rules are what the agent must not do — a pattern to watch for and a correction to send when it appears. Rules cost nothing until they match, and an interrupting rule stops the model mid-sentence so the correction lands before the work does.
In the desktop app these are one group in Settings — Teaching the agent — and each page opens with the trio so it's clear which of the three you're looking at.
| You want the agent to… | Add a… | How |
|---|---|---|
| Follow your release-notes format, review checklist, deploy runbook | Skill (markdown, no code) | Settings → Skills, or drop a SKILL.md folder |
Stop reaching for .unwrap(), --force, or a generated directory |
Rule (a regex + a sentence) | Settings → Rules, or /rules add |
| Call your internal API or webhook | Custom tool (no code) | Settings → Tools → New tool (HTTP POST) |
| Do something new on the machine (parse a format, drive a CLI…) | Built-in tool (Rust) | The recipe in AGENTS.md |
A skill is a folder holding a SKILL.md (the same shape as Claude Code skills):
---
name: release-notes
description: Writes release notes from the git log in our house style.
---
1. Run `git log --oneline` since the last tag.
2. Group changes into Added / Fixed / Changed.
3. One crisp line per change — no commit hashes, no filler.Add one from the desktop app (Settings → Skills → New skill), drop a folder
in ~/.oxen-harness/skills/<name>/ for a global skill, or commit one to a repo
at .oxen-harness/skills/<name>/ so everyone who opens that project gets it.
The description is the trigger — write it like "does X, use when Y" — and tools
are referenced by their backticked names ("read it with read_file"), which
the desktop editor autocompletes and lints. This repo ships
add-a-tool, a skill that teaches
the agent to extend itself with new tools.
Some corrections don't belong in the system prompt. "Never use .unwrap()
here" is worth saying only when the model is about to write .unwrap(); the
rest of the time it's paying rent on every request for a situation that hasn't
come up. A rule inverts that: it sits dormant, watching the reply as it
streams, and speaks only when its pattern appears.
What happens when one matches. The rule's message is injected as a
<system-reminder> — the same channel the built-in correctives use — and the
model tries again. An interrupt: true rule (the default) also abandons the
reply in flight, so a bad edit is stopped while it's being written rather than
after it's applied. interrupt: false lets the reply finish and corrects
before the next step, which is right for style notes that aren't worth
discarding work over.
Where to watch. scope: ["tool"] matches the arguments the model writes
into a tool call — the earliest place a bad edit_file or run_shell becomes
visible, and usually what you want. ["text"] matches its prose. Omit scope
to watch both.
How often. "once" (the default) fires a rule once per session: a reminder
the model has already seen and worked around is noise. "after:5" lets it
return after five more rounds, which suits rules the model tends to drift back
into.
Writing good rules. A rule is a regular expression, so it catches text, not intent — which makes narrow, literal patterns the ones that work:
- Match something unambiguous:
\.unwrap\(\),generated/,push\s+--force,TODO\(nobody\). - Say what to do instead, not just what's wrong. "Return a Result" beats "don't unwrap" — the model has to act on it.
- Prefer
["tool"]scope for anything about changes. A rule watching prose fires when the model merely discusses the thing. - Expect false positives and choose
interruptaccordingly. A pattern that fires on a legitimate mention costs a round trip each time it does. - Patterns use Rust's
regexsyntax: no lookahead, no backreferences. The editor tells you if a pattern won't compile — a rule that can't compile is skipped, so it protects nothing while looking like it does.
Where to start. Neither front end ships rules turned on — a rule that
interrupts is the kind of thing you want to have chosen — but both offer the
same short library, each entry saying what it catches and why:
Settings → Rules lists them with an Add button, and /rules suggest prints
them with /rules suggest <name> to take one.
Or write it by talking to the model. Regexes are the part of this people
bounce off, so Settings → Rules → New rule opens a conversation and the
terminal has /rules draft <what you want>:
/rules draft don't remove files from outside the project without asking
It's a back-and-forth, not a one-shot. The model's sentence streams as it
writes ("Watching for rm with a path that leaves the project root"), the rule
it produces appears as a card showing what it catches and what it deliberately
ignores, and the fields below fill in. Then you keep going — "also catch mv",
"make it stricter", "just remind, don't interrupt" — and each message
revises the rule on the table rather than starting over. The form and the
tester stay visible throughout, so the conversation is about something you can
see and edit yourself.
Reopening a rule picks the conversation back up: what you asked for is in the
thread, the rule you saved is the card under it, and the tester holds a line
this rule is actually about — so a follow-up revises what's on disk instead of
writing a stranger. That's what prompt and sample in the file are for; both
are optional, and a rule written by hand still resumes from the rule itself.
Every draft is checked before you see it: the model must supply an example its pattern catches and a near-miss it shouldn't, and the harness runs both through the real engine. A pattern that doesn't compile, misses its own example, or is broad enough to catch the near-miss is retried once — visibly, in the thread — and reported rather than saved if it fails again.
Trying one before you trust it. Both front ends run candidate patterns through the agent's own regex engine, not the editor's, so what you see is what will fire:
- Desktop — Settings → Rules. Type what the model might write and the match highlights in place, with the exact reminder the model would receive and what happens to the reply.
- Terminal —
/ruleslists what's in force,/rules addwalks through writing one (and offers to test it),/rules test <name>runs a saved rule against text you paste (pre-filled with the sample it was written against),/rules on|off <name>and/rules rm <name>do the rest. Edits apply to the running session immediately.
Project rules travel with the repository and override a rule of yours with the same name, so a team can standardise on "don't touch the generated client" without anyone configuring anything.
This is a small, layered Rust workspace — you can hold the whole thing in your
head in an afternoon. CONTRIBUTING.md is the front door;
from there, ARCHITECTURE.md covers the layering and the
lifecycle of a turn, AGENTS.md is the contributor protocol — the
verification loop, project conventions, a codebase reading order, and the
add-a-built-in-tool recipe — and DOCUMENT-MAP.md indexes
every file.
Powered by Oxen.ai 🐂
