Huginn
A self-hosted, model-agnostic coding agent — Muninn's brother raven ("thought" to Muninn's "memory"). Built spine-first from first principles, then refined against the best ideas from the open-source coding-agent community (aider, SWE-agent, opencode).
Philosophy
Models are swappable batteries; the harness is the durable, owned, learnable layer — the edit tool, context strategy, verify loop, planning, self-review, memory, and safety gating. Point Huginn at any OpenAI-compatible endpoint (local llama.cpp on the GPU server, or a frontier provider with an API key) and every model improvement is a free upgrade. See ROADMAP for the full CP0→CP10 plan and what shipped.
What it can do
Agent loop with native tool-calling; anchored ("hashline") edits that don't require
the model to retype code; a verify loop (run_command); a tree-sitter repo map; LSP
code-intelligence (diagnostics/hover/goto/references/rename); phased planning with
per-phase git commits; a self-review critic that auto-fixes blocking findings, now
backed by a static-analysis loop (clippy / ruff / eslint / go vet diagnostics
folded in as deterministic findings); a repro-test-first check that re-runs a bug
fix's own reported test before honoring "done"; cross-session memory plus a
procedural-skill loop that learns reusable approaches from clean jobs; a
command-safety policy (classify → ask/allowlist/judge); session recovery
(transient-retry + convergent context-window compaction); a multi-agent delegate
tool for subagent fan-out; and a scaffold_project tool that bootstraps a project for
checkpoint-by-checkpoint building. It also keeps a run-trace ledger and an L4
self-improvement loop that proposes harness-config changes from its own scorecard.
Reachable four ways: TUI, one-shot, REPL, and a headless NATS worker
(delegation from Muninn).
Benchmarks
SWE-bench Verified: 68.2% resolved (341/500), scored with the official
swebench harness — driving a local, quantized Qwen3.6-27B (INT4) on a
self-hosted OpenAI-compatible endpoint, no frontier API, ~$0/instance.
That is a frontier-adjacent number on commodity hardware: it sits at or above where GPT-4-class agents scored on this exact benchmark a few months prior, reached here on a single quantized open model served from a home GPU box.
Controlled harness A/B — Huginn vs. the reference harness. To isolate the harness from the model, I ran mini-SWE-agent (the SWE-bench team's own reference minimal harness) on the same Qwen3.6-27B, the same 500 Verified instances, graded by the same official harness:
| Harness (same model, n=500) | Resolved | |
|---|---|---|
| Huginn (this project) | 341/500 (68.2%) | — |
| mini-SWE-agent (reference) | 351/500 (70.2%) | |
| Paired McNemar test | χ²=1.09, n.s. | statistically tied |
A solo-built agent — written in a few days and still WIP — landing in a statistical dead heat with the field's reference harness, on the reference team's own benchmark, is the headline result. On a strong model the harness is not the differentiator; the model dominates and a sound harness is enough to extract its full capability — and a sound one is reachable fast.
The harnesses are complementary. They resolve different instances — Huginn-only 32, mini-only 42 — so their union is 383/500 = 76.6%. There is real signal in which problems each harness solves, not just how many.
The harness still moves the needle measurably. On a frozen, gold-verified 39-instance subset, the same model climbs on harness levers alone — baseline 15/39 → +self-review gate 17/39 → +larger iteration budget 20/39 (38.5% → 51.3%) — evidence the layer is real, even if it doesn't beat a well-built peer harness on a capable model.
Install
Option A — drop-in static binary (Linux x86-64)
A fully static (musl) binary with no runtime dependencies — copy it to any Linux box and run:
scp dist/huginn-x86_64-linux-musl you@host:~/.local/bin/huginn
chmod +x ~/.local/bin/huginn
Build it yourself with scripts/build-static.sh (needs musl-tools +
rustup target add x86_64-unknown-linux-musl).
Option B — build from source (any OS/arch)
cargo build --release
install -m755 target/release/huginn ~/.local/bin/huginn # optional
Rust + a C toolchain (for the tree-sitter grammars). macOS, Windows, and ARM build the same way; the TUI (ratatui/crossterm) is cross-platform.
Configure
The only hard requirement is a model endpoint. Set it in a config file or via env vars.
cp config.example.toml config.toml # then edit [model].base_url / model / api_key
Config precedence: --config <path> → ./config.toml →
~/.config/huginn/config.toml → built-in defaults (which point at the GPU server).
Env overrides (handy for a different box): HUGINN_BASE_URL, HUGINN_MODEL,
HUGINN_API_KEY. For example, against a local llama.cpp:
HUGINN_BASE_URL=http://localhost:8080/v1 HUGINN_MODEL=qwen huginn --tui
Resilience and multiple endpoints (optional). The [model] block can carry
extra interchangeable hosts that serve the same model, round-robined per request
([[model.peers]] — e.g. Qwen on both the GPU server and .78); a dead one is skipped. A
separate [model_fallback] block is a different host the agent fails over to when
the primary is unreachable (transport error / 5xx / 429 / stall) — failover, not
routing. Both are opt-in; with neither set, behavior is unchanged. Named
[models.<name>] tables form a registry the TUI's /model command switches between
live (see TUI keys).
Optional tools (everything degrades gracefully if missing)
| Tool on PATH | Enables | If missing |
|---|---|---|
git |
per-task checkpoints (/undo), self-review diff |
edits still work; no checkpoint/review |
rust-analyzer, pyright, typescript-language-server, gopls |
LSP tools for that language | that language's LSP tools report "not installed" |
rustfmt / prettier / black / gofmt |
format-on-edit | edits aren't auto-formatted |
Run
The workspace defaults to the current directory — cd into a repo and go (or
pass --workspace <path>). File tools are sandboxed to that root.
# Full-screen TUI cockpit (recommended for interactive work):
cd ~/code/myrepo && huginn --tui
# One-shot:
huginn --workspace ~/code/myrepo "add a doc comment to main.rs"
# Interactive line-REPL:
huginn --workspace ~/code/myrepo
# Headless NATS worker (delegation target for Muninn):
huginn --worker
The TUI is a full cockpit: markdown-rendered answers (fenced code blocks,
lists, tables), live token streaming (with the model's "thinking" preamble
shown dim-italic and the committed answer bright), a live plan panel that shows
phases tick off as the agent works, inline diffs of every edit/write (red −/
green +), a background resource/LLM-status bar (CPU/RAM/GPU/VRAM + whether the
model endpoint is up and which model it's serving, plus a live context-fill gauge
— green/amber/red by how full the window is — and live tok/s while streaming), a
toggleable tool panel (F2) with recent tool calls and timing, and full
input-line editing with history.
TUI keys
Type + Enter to send · Shift+Enter newline · ←/→ Home/End edit the line ·
↑/↓ command history · PgUp/PgDn, Ctrl+↑/Ctrl+↓, or the mouse wheel scroll ·
F2 tool panel · /plan /undo · /compact (summarize old history) /clear (reset
to a fresh context) · /model [name] (list configured models, or switch the active
one live between turns) · /add-model (interactive wizard that registers a new model
endpoint and appends it to the config). When a command needs approval, a modal appears
— ←/→ then Enter, or y/n/a (approve-all this session). Long input soft-wraps
as you type.
Stopping vs. quitting. Esc interrupts the running turn (it abandons the
in-flight model/tool call immediately; when idle it clears the input line) — it does
not quit. Ctrl-C/Ctrl-D also interrupt while working, and quit when idle.
The clean way to close is /exit (or /quit), which also reaps any servers the
agent left running in the background.
Steering mid-run. The input stays live while the agent works — type a message
and Enter and it's folded into the agent's context at its next model step (not
queued for after), so you can redirect it without stopping: "focus on the parser
bug first", "don't touch the tests", "stop and just show me the diff". Steering
messages show as ↳ steering: in the transcript.
Command-safety policy ([agent] command_policy)
run_command executes shell as you — fine in a trusted session, dangerous
unattended. So every command is first classified deterministically, then a
configured policy decides its fate. The classifier (policy.rs) reads the
whole line — each &&/||/|/; segment is judged independently, so a chained
cargo build && rm -rf / is caught:
| Class | Meaning |
|---|---|
| Safe | Every segment leads with an allowlisted read-only/build/VCS-inspection tool (cargo, git status, ls, pytest, gcc, …), with destructive subcommands filtered out (git reset/checkout, cargo publish, pip install, …). |
| Dangerous | Any segment matches a known-destructive pattern — rm -rf, dd, mkfs, sudo, git push --force, curl … | sh, fork bombs, writes to /dev or /etc, … |
| Unknown | Neither clearly safe nor clearly dangerous (a custom script, docker run, mv). |
The policy then governs what runs:
| Policy | Safe | Unknown / Dangerous |
|---|---|---|
ask (default) |
runs | interactive approval prompt (denied if headless) |
allowlist |
runs | denied — fully non-interactive safe mode |
judge |
runs | an LLM safety judge decides ALLOW/BLOCK (no human in the loop) |
auto |
runs | runs — trusted/sandboxed environments only |
judge is what makes Huginn safe to drive headlessly: it mirrors Muninn's
fast-reject-then-judge pipeline, and the judge is fail-safe — anything that
isn't a clear ALLOW is treated as a block, and the prompt biases toward blocking
when uncertain (a false block just re-routes the agent; a false allow is
irreversible). Worker mode forces judge regardless of config.
File-write confinement. Independently of the command policy, every file tool
resolves paths against the workspace root and refuses any write or read that
escapes it (ctx.resolve() — ../../etc/passwd is rejected). Edits are also
syntax-guarded: an edit that would turn a parsing file into a non-parsing one is
refused.
Known limitations (this is a heuristic permission layer, not a sandbox — see ROADMAP):
- File writes aren't policy-gated. They're confined to the workspace root, but within it any path can be written — there's no per-path write allowlist (e.g. to keep an agent from dirtying a test fixture or build output during an eval).
- No network isolation. The classifier inspects command text, not behaviour; a command can still open sockets. Real egress control belongs at the container / namespace level, not in Huginn.
MCP servers (optional)
Huginn is an MCP client: list Model Context Protocol servers in config and their
tools join the agent's native toolset (filesystem, git, databases, browsers, your
own services). Both transports are supported — stdio (spawn a child process,
the common npx case) and http (incl. FastMCP streamable-http). Tools are
trust-by-config (configuring a server opts its tools in; each call still shows in
the transcript and log), native tools win on a name clash, and a server that fails
to start is logged and skipped. None configured → zero overhead. See the
[[mcp_servers]] examples in config.example.toml.
[[mcp_servers]]
name = "filesystem"
kind = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
Self-improvement loop (the L4 scorecard)
Every finished job appends one compact RunRecord to a global ledger (runs.jsonl
in the state dir) — outcome, why the loop stopped, iterations, tool-failure count,
and which quality levers were on. huginn --runs prints the analysis. With
failure_mining on, a non-clean run also gets a one-word reason tag (a cheap-model
pass: went-in-circles, forgot-to-validate, task-too-hard, …) so failures group
by real cause, not just the stop symptom.
huginn --improve reads that scorecard and prints ranked, concrete config changes:
climbers (e.g. raise the iteration budget when runs hit it, lower temperature on
high tool-failure rates) and ablations (flip an on-by-default lever off purely to
measure what it's worth). An addressability filter suppresses climbers when mining
says the failures aren't harness-fixable, so it doesn't churn config on noise.
--improve-apply writes the top proposal as a candidate config file (the live,
hand-edited config is never rewritten) and opens an experiment; the apply→decide arm
stays inert until a real SWE-bench score is fed back via --improve-resolve <id> --baseline PASS/TOTAL --candidate PASS/TOTAL, which keeps the candidate only if it
resolves strictly more instances — otherwise it reverts. A change is kept only when a
number says it helped. See DESIGN-multiagent.
Hermes feed (optional)
The worker also emits its events as coabai-agent-protocol Envelopes on a stable NATS
subject (hermes.huginn.events, overridable via HUGINN_HERMES_SUBJECT) for the
Hermes fleet console. Purely additive — the per-job thin-JSON feed Muninn's Matrix
relay parses is untouched, and publishing is best-effort (no subscriber → dropped, so
it's harmless when Hermes isn't running).
Delegation from Muninn (optional)
huginn --worker consumes coding jobs off a NATS JetStream work-queue and posts
results back — so "hey Muninn, fix X in dlejos" from your phone reaches Huginn on the
box with the repos. Entirely opt-in; see the [worker] block in
config.example.toml. Not needed for any local use.