A locally-buildable, extensible coding agent — a single static Go binary (Linux +
macOS), no runtime dependencies. Klaudia began as a cleanroom of Claude Code
(v2.1.66) and was ported to Go; the JavaScript reference is retired (preserved on
the js-reference branch). See Background for the story and
docs/parity.md for the feature map.
Klaudia keeps full parity with the reference and then builds past it — the extras we lean on day to day:
- Code intelligence (LSP) — real diagnostics and go-to-definition from language servers you already have installed.
- Memory & project knowledge — a
MEMORY.mdindex that links out to detail notes, recalled into every session. - Themes — the whole UI (banner, prompts, menus, Markdown) recolors, persisted in config.
- Standing goals —
/goalpins an objective that's re-stated to the model every turn. - Plus OS/container Bash sandboxing, local web search & browsing, MCP, and skills.
go install github.com/greenthread-ai/klaudia/cmd/klaudia@mainThis installs the klaudia binary into $(go env GOPATH)/bin (commonly
~/go/bin) — make sure that's on your PATH. We track main while a release
tag isn't published yet; @main always resolves to the current HEAD, whereas
@latest (the usual Go default) routes through proxy.golang.org and can lag
behind new commits on an untagged module. To force a refresh:
GOPROXY=direct go install github.com/greenthread-ai/klaudia/cmd/klaudia@latest.
Prefer to build from a checkout? See Build.
Klaudia reads ~/.klaudia/config.toml automatically for every run; a project
./.klaudia/config.toml overlays it when present. Generate a commented starter:
klaudia --create-config=global # ~/.klaudia/config.toml (your default)
# or:
klaudia --create-config=local # ./.klaudia/config.toml (project override)Both commands refuse to overwrite an existing config (so you can't accidentally clobber settings); delete the file first if you want a fresh starter.
Pick one of these paths:
Anthropic API key
export ANTHROPIC_API_KEY="sk-ant-..."
klaudiaExisting Claude Code login on macOS
Klaudia can reuse an existing Claude Code OAuth session from the macOS Keychain. Sign in with Claude Code first, then run Klaudia:
claude
klaudiaOpenAI-compatible provider
Edit the config you just generated and set the provider block:
# ~/.klaudia/config.toml (comments are supported)
provider = "openai"
model = "openai/gpt-5.5"
baseURL = "https://api.example.com/v1"
# apiKeyEnv is the NAME of the environment variable that holds your key —
# pick any name you like and export a variable of that name (see below).
# Prefer this over apiKey = "sk-..." so the key stays out of the file.
apiKeyEnv = "MY_API_KEY"Then export the variable you named in apiKeyEnv and run:
export MY_API_KEY="sk-..." # same name as apiKeyEnv above
klaudiaOnce the TUI starts, type /doctor to verify auth and environment status.
Pure Go, no CGO, no system libraries:
CGO_ENABLED=0 go install ./cmd/klaudia # or: go build -o klaudia ./cmd/klaudia
go test ./internal/...The result is one self-contained binary (Linux + macOS).
Two end-to-end rigs go beyond the unit tests (both need a working credential):
scripts/smoke.sh drives the real agent loop across modes, the client-side
tools and the resume path on haiku; scripts/torture.sh runs the spec's
agent-loop torture test — one task needing 20+ file inspections, edits, a dev
server, log inspection, SSH to a container, a wrong turn and a recovery — and
scores the transcript against the spec's checklist.
./klaudiaA Bubble Tea terminal UI: streamed Markdown answers, / slash commands with
type-ahead, fuzzy @path file completion (Tab, Tab again to cycle), input
history (↑/↓), and Esc to interrupt a turn. Type /help for the full list.
Return sends; Ctrl+J and Alt+Return insert a newline. To swap them:
[input]
enter = "newline" # Return inserts a newline; Alt+Return or Ctrl+J sendsCtrl+Return is not something Klaudia can offer on its own. A terminal sends
the same byte for Return and Ctrl+Return (CR, 0x0D) — Ctrl+J is simply the
LF byte, which is why it is the traditional newline chord. Terminals that
implement the Kitty keyboard protocol can distinguish the two, but Bubble Tea v1
does not parse those sequences, and enabling the protocol would break every
other Ctrl binding.
What does work is telling your terminal to send ESC CR for the chord, which
arrives as Alt+Return:
| Terminal | Setting | Caveat |
|---|---|---|
| Ghostty | keybind = ctrl+enter=text:\x1b\r |
— |
| kitty | map ctrl+enter send_text all \x1b\r |
— |
| WezTerm | { key="Enter", mods="CTRL", action=wezterm.action.SendString("\x1b\r") } |
— |
| iTerm2 | Settings → Keys → Key Bindings → ⌃↩ → Send Escape Sequence → \r |
— |
| Apple Terminal | Cannot remap Return at all |
Use Option+Return, with Settings → Profiles → Keyboard → Use Option as Meta key. That option also stops Option from typing é, © and friends |
| Windows Terminal | { "keys": "ctrl+enter", "command": { "action": "sendInput", "input": "\u001b\r" } } |
Alt+Return is the fullscreen toggle by default and never reaches the app, so remap it or use Ctrl+J |
| tmux / screen | nothing to do — ESC CR passes through |
— |
Ctrl+J works in every terminal, in both modes, needing no configuration at
all: it is the LF byte. That is the escape hatch, and it is why
enter = "newline" cannot leave you unable to send.
What this changed: Alt+Return used to send, because the Return handler
ignored the Alt modifier. It now inserts a newline (or sends, in newline
mode). Two consequences worth knowing: if you were using Option+Return to
send on macOS, that now adds a line; and pressing Esc immediately followed by
Return can be read as Alt+Return, since that is the same byte sequence —
one of the reasons a terminal cannot simply invent a Ctrl+Return key.
Klaudia renders inline, not full-screen. Finished output is printed into
your terminal's real scrollback and only the input and status bar are redrawn in
place, so scrolling, drag-to-select, your terminal's own search and tmux copy
mode all keep working — and the conversation is still there after you quit.
Copying is meant to be exact: rendered code blocks carry no margin, no padding
and no expanded tabs, so a snippet pastes as the source it came from. /copy
puts the last answer, a code block, or a tool result on the system clipboard via
OSC 52, which works over SSH and inside tmux.
Ctrl+C does the smallest useful thing first — interrupt a running turn, cancel
a prompt, or clear the line — and only quits when pressed twice in a row.
Tool calls show their key input (e.g. ⚙ Bash go test ./...) and a -/+
preview for edits; a status bar tracks model · mode · turns · tokens. Long
output is kept in full: the preview tells you its number and /last <n> opens
it in $PAGER, where searching and copying are your pager's job. /search,
/outline and /errors index the session, and /open <path:line> sends a
reference copied from a stack trace straight to $EDITOR.
You can queue a follow-up while the model is working: type and press Enter to
queue it (it's sent when the current turn finishes); press Enter again to
interrupt and send it now, or ↑ to edit it.
/model with no argument asks the provider which models it serves and offers
them as a picker — Anthropic and OpenAI-compatible endpoints both answer at
GET /v1/models — so you don't have to remember an exact model ID. /model <alias|id> still sets one directly (opus, sonnet, haiku, fable, or any
full ID). Picking from the list also records that model's real context window,
which is what the status bar's ctx N% measures against.
/theme switches the colour theme (Markdown + chrome) for the session; set a
durable default with theme = "nord" in .klaudia/config.toml (dracula |
gruvbox | tokyo-night | nord | light | catppuccin). NO_COLOR is honoured.
Long-running commands run detached as managed jobs: Bash with
run_in_background returns a shell id, BashOutput reads new output
incrementally and KillShell stops it — so the agent can launch a dev server or
watcher and keep working. Jobs get a name, a port and a log file; see
Long-running commands, logs, and your shell.
# Print the final result and exit
./klaudia -p "What files are in this directory?"
# Unattended, including changes to this machine
./klaudia -p "Install and configure nginx" --allow-host-changes
# Stream events as JSON (tool calls, results) as they happen
./klaudia -p "Explain the build" --output-format stream-json --verbose
# Partial message deltas, JS-compatible (only with --print + stream-json)
./klaudia -p "…" --output-format stream-json --verbose --include-partial-messagesA persistent agent driven by newline-delimited JSON over stdin/stdout — the channel for editor/SDK integrations (no terminal needed):
./klaudia --input-format stream-json --verbose./klaudia # auto-resume the most recent session here
./klaudia --new-session # start fresh instead of auto-resuming
./klaudia --continue # explicitly resume the most recent session here
./klaudia -r <session-id> # resume a specific session
./klaudia -r <session-id> --full # replay the whole transcript (not the summary)Auto-resume is an interactive convenience: headless (-p) and embedding
(--input-format stream-json) runs stay stateless unless you pass
--continue or -r <id>.
Sessions are JSONL transcripts under ~/.klaudia/sessions/<encoded-cwd>/
(override the base with KLAUDIA_CONFIG_DIR). Klaudia still reads legacy
transcripts from ~/.klaudia/projects/<encoded-cwd>/ during migration. When a
session has a persisted compaction summary, resume seeds from it (token-saving)
unless --full.
npm run dev becomes a job rather than a blocked turn: it keeps running,
gets a name and a log file, and Klaudia carries on. /jobs lists what is up and
on what port, /logs <job> opens the log in your $PAGER, /logs -f tails it
into real scrollback, /restart replaces the process in place rather than
starting a second copy, and a crash is reported when it happens.
Commands run in their own process group, so stopping one stops what it started —
and they inherit your PATH, ssh agent and git credential helpers. Klaudia does
not allocate a PTY, so vim, less, top and git commit with no -m are
refused immediately with the flag that would have worked, rather than hanging.
You can type while Klaudia works and it will read your message before its next
step, not after the turn; /stop asks it to finish the current step and report.
A leading ! runs a command yourself, and its output becomes context:
> work out why the auth test is failing
$ git diff
> keep the test change but revert the API change
Details, including what deliberately does not work: docs/jobs.md.
Klaudia knows which working-tree changes are yours, which are its own, and which
are both. /changes shows the split, /commit stages only its own work and
lists what it left out, and /undo restores its last change — never touching a
file you also edited.
Undo stores prior contents as git blobs (git hash-object -w). It does not
touch your index, does not create a stash, and shows the exact git cat-file
commands it would run before doing anything. Full detail:
docs/working-tree.md.
/context shows what Klaudia has actually read rather than a token percentage,
and /pin <path> keeps a file in context every turn so it survives compaction.
Headless runs exit with codes an automation can branch on — notably 4 for "needed a host change and had no way to ask".
The terminal-UX specs this was built against, and the places the implementation deliberately went a different way, are recorded in docs/ux-spec.md.
Klaudia finishes the task without asking per action, and stops before changing
the machine it runs on. Work in the project — editing, building, testing, git,
dev servers, and the destructive parts like rm -rf ./dist — is autonomous, as
is work on a remote host the task calls for. Changing this machine (packages,
services, /etc, shell rc files, users, firewall) needs your agreement, and
Klaudia asks for the whole operation at once rather than one command at a time:
This changes your machine
Install nginx and configure it as a development proxy
why: the task asks for the app to run behind a local proxy
paths: /etc/nginx services: nginx packages: nginx
approving covers every step inside that scope, for this session only
Approvals are session-scoped and never written to disk. /trust shows what is
live and revokes it.
Most gate hits never reach you. An incidental 2>/dev/null or a scratch file in
/tmp is stopped, Klaudia takes another route, and the attempt is drawn quietly
as ⊘ changes this machine: writes /dev/null — trying another way rather than as
a failure. You are asked only when the work genuinely cannot proceed otherwise,
and then (s)omething else sits beside yes and no — declining usually means
"not like that" rather than "give up", so it keeps the turn alive and lets you
redirect. Anything blocked and never approved is named in the completion block
under Not done — needs your agreement, so giving up quietly is not an option
available to it.
This is a guardrail against well-intentioned mistakes, not a security
boundary. It reads command lines and tool inputs; it does not watch what
programs do, so a command that builds its own target or a package's install
script can change things without being seen. For enforcement the kernel applies,
set [sandbox] mode = "os".
| Flag | Mode | Behavior |
|---|---|---|
| (default) | autonomous |
Finish the task; ask before changing this machine |
--permission-mode plan |
plan |
Read-only; mutations and network blocked |
--dangerously-skip-permissions |
bypassPermissions |
Allow everything, including host changes |
/mode switches interactively; /trust shows and revokes approvals. Headless
runs do project and remote work but refuse host changes unless you pass
--allow-host-changes.
The per-command model it replaced is deprecated, not removed: legacy modes
(default, acceptEdits, dontAsk), --allowedTools 'Bash(go test:*)' and
the /allow and /deny commands are still honoured so existing setups keep
working, but they create nothing new and /allow//deny are no longer listed
in /help. /trust grants by what an operation does rather than by matching
command text, and shows any surviving rules alongside its own. A config that
already has permission rules starts in observe mode until you run
/trust upgrade.
Full detail, including the zone table and what is deliberately not protected: docs/trust.md.
Klaudia defaults to the Anthropic Messages API. A project or user
.klaudia/config.toml selects the provider and model:
# "anthropic" (default) | "openai"
provider = "openai"
model = "openai/gpt-5.5"
# OpenAI-compatible endpoint.
baseURL = "https://api.example.com/v1"
# apiKeyEnv names the env var holding the key (you then `export MY_API_KEY=...`).
# Or set apiKey = "sk-..." inline — but the env form keeps secrets out of files.
apiKeyEnv = "MY_API_KEY"
# Optional: set the model's context window in tokens so autocompaction kicks
# in before the provider overflows. Defaults to 200000 (Anthropic-sized); set
# this when running against smaller-context models (e.g. 128000 for many
# OpenAI-compatible hosts) — otherwise long sessions can hit
# "max_tokens must be at least 1, got -N" or "context length exceeded" 400s.
# contextWindow = 128000
# Optional: cap the tokens a single turn may generate. Defaults to the model's
# real maximum (128000 on the 1M-context Claude models, 64000 on the 200k ones,
# 8192 for models Klaudia doesn't recognise — including most OpenAI-compatible
# ones). Set this when your provider's limit differs from that fallback.
# maxTokens = 32000
# Optional: what the Return key does at the prompt. "send" (default) submits
# and ctrl+j / alt+Return insert a newline; "newline" swaps them. See
# "Return, and multi-line input" above — ctrl+Return is not a value, because
# terminals cannot send one.
# [input]
# enter = "newline"Create a commented starter config with ./klaudia --create-config=global for
~/.klaudia/config.toml, or ./klaudia --create-config=local for
./.klaudia/config.toml.
--model haiku|sonnet|opus (or a full model ID) overrides per-run. The
OpenAI-compatible provider translates the Anthropic message shape to Chat
Completions (including image tool-results → image_url).
~/.klaudia/config.toml is the user default; a project ./.klaudia/config.toml
overlays it (project wins). Settings merge per field.
ANTHROPIC_API_KEY(orANTHROPIC_AUTH_TOKEN), or- an existing Claude Code OAuth session in the macOS Keychain (Klaudia refreshes expired tokens and writes them back), or
- a provider key via
apiKey/apiKeyEnvin.klaudia/config.toml.
KLAUDIA_STREAM_IDLE_TIMEOUT— seconds a streamed model turn may go without any event before it's treated as a stalled connection (default120). On a stall Klaudia transparently retries the turn if nothing has been emitted yet, otherwise it fails the turn with a clear timeout instead of hanging forever. Set to0to disable the watchdog.- Long-context credits (429) — if the API returns "Usage credits are
required for long context requests", that's a billing/entitlement gate, not a
transient throttle: retries won't help. Add usage credits, or reduce context
(lower
contextWindowso autocompaction triggers earlier, and/compact).
The TUI paints inline and coordinates every write it makes; anything else writing to the terminal lands mid-repaint and tears the frame (a stray library log once spliced the input box's border into the status line). Klaudia therefore keeps other writers off the terminal without going deaf:
- Chrome's CDP chatter is split by cause. chromedp logs any DOM/Page event newer
than its own type switch as an error — structural, since that switch trails
the protocol, and noisy enough that an ad-carrying page emits one per update —
so that class is dropped. Everything else it reports is kept and attached to
the error of the next browser operation that fails, as
(chrome: …). - The standard logger is pointed at
io.Discardwhile the program runs, as a backstop for the next dependency that reaches forlog.Printf.
Two env vars recover the raw output when you're debugging:
KLAUDIA_LOG— file path for anything the process writes via the standardlogpackage (Klaudia's own and its dependencies').KLAUDIA_BROWSER_LOG— file path for chromedp's browser/protocol log, unfiltered, including the dropped events.
Neither is on by default, and a path that can't be opened is dropped rather than
reported — the noise is the thing being prevented. /doctor remains the way to
check auth, tools and environment.
.klaudia/config.toml → sandbox.mode:
local(default) — run on the host, unconfined.os— host confinement:sandbox-exec(macOS) /bubblewrap(Linux). Reads are unrestricted; writes limited to cwd + temp (+writeRoots);networkconfigurable. Falls back to local with a warning if the tool is absent.container— run inside docker/podman (runtime,image,mountCwd,readOnly,network).
Built-in, permission-gated tools backed by a lazily-launched headless Chrome (nothing spawns until a web tool runs; the browser is closed at session end):
BrowserSearch— DuckDuckGo (default) or Google; returns titles/URLs/snippets.BrowserFetch/BrowserNavigate/BrowserSnapshot— render a page and return Markdown.
On a Claude model these are the fallback: the built-in Anthropic web_search /
web_fetch server tools are preferred (they return cited results). The
Chrome-backed tools above are what non-Claude providers use, and what you get
when you explicitly ask Klaudia to drive the browser.
Requires a Chrome/Chromium install (auto-discovered; set KLAUDIA_CHROME_PATH
on Linux/Windows if not on PATH). Tunable via .klaudia/config.toml →
browser (engine, headless, chromePath, userDataDir, headedFallback,
…) or KLAUDIA_* env vars. When a search hits a bot-challenge page, Klaudia can
relaunch a headed Chrome with a persistent profile (~/.klaudia/browser/…)
so you can solve it once. Anthropic's server-side web_search/web_fetch betas
remain available when using the Anthropic provider.
Model Context Protocol servers from .mcp.json, read from three scopes in
increasing precedence: global ~/.klaudia/.mcp.json (honours
KLAUDIA_CONFIG_DIR), then the project's .mcp.json, then
.klaudia/.mcp.json. Per server name, the narrower scope wins — a project can
point a globally configured server at a different binary without disturbing it
elsewhere. Put personal servers you want everywhere in the global file, and
servers belonging to a repo in the project's. A server is stdio (command +
args) or HTTP (url, with type:"sse" for the legacy SSE transport).
// and /* */ comments are allowed; a file that still doesn't parse is
reported — naming the file, since all three share a base name — rather than
silently loading nothing:
Edits to any of those files apply to the running session: servers are
added, dropped or restarted in place, and a server whose config didn't change
keeps its session rather than being interrupted. Installing a server no longer
means restarting to use it. A config that doesn't parse leaves the running
servers alone, so a half-typed file can't take working tools away; the reload
is otherwise silent, so check /mcp if a server doesn't appear.
Their tools appear as mcp__<server>__<tool>, auto-deferred behind ToolSearch.
In the TUI, /mcp lists servers and reconnects/disconnects them.
The read-only sub-agents get read-only MCP tools. Fanning out across a wiki,
an issue tracker and a chat archive is what Explore and Plan are for, and it
is also the work whose bulk should never reach the main thread — a sub-agent
spends its own context and hands back a summary. A tool qualifies by declaring
the protocol's readOnlyHint; a tool that says nothing is treated as a write, so
delete_branch never arrives via this route.
readOnlyHint is a claim a server makes about itself, and nothing verifies it.
Per server, readOnly overrides that claim in either direction:
{ "mcpServers": {
// unset: trust each tool's readOnlyHint
"gitea": { "command": "gitea-mcp", "args": ["-t","stdio","-r"], "readOnly": true },
"sketchy": { "type": "http", "url": "https://third-party.example/mcp", "readOnly": false }
} }true for a server that annotates nothing — including one you launched in its
own read-only mode, where you know something the protocol wasn't told. false
to decline to take a server's word, without giving up the server: the main agent
keeps it and still asks before every call. This decides which tools a read-only
sub-agent is handed; it is not a claim that calling them is safe.
There is no ${VAR} expansion — a value is used exactly as written — but the
server subprocess inherits Klaudia's environment, so export credentials in your
shell rather than writing them into the file. .mcp.json.example is a working
starting point; copy it and edit. .mcp.json itself is gitignored because a
credential in it would be a literal in a committed file — git add -f it if you
want a secret-free team config in the repo.
Worth pairing with the readOnly guidance above: -r and -S on the server
command narrow what exists at all, and readOnly decides who is handed it.
-S issue,pull_request,actions matters more than it looks, because every tool's
schema is sent on every request — loading 54 tools to use four is a permanent
context tax.
Klaudia talks to language servers you already have installed to give the agent real code intelligence:
Diagnostics— compiler/linter errors for a file (the edit → check → fix loop).Definition/References— jump to a symbol's definition or find its uses.
Servers are detected, never downloaded — looked up on $PATH and in the
usual toolchain locations (so gopls in ~/go/bin, rust-analyzer in
~/.cargo/bin, global-npm bins, etc. are found even when not on PATH).
Recognised today: gopls (Go), rust-analyzer (Rust),
typescript-language-server (TS/JS), pyright-langserver (Python), clangd
(C/C++). They're launched lazily on first use and shut down at session end.
/doctor lists which servers it found. Turn one off with:
[lsp]
disabled = ["python"]Skills are read from four directories, in increasing precedence — so a project skill overrides an installed one of the same name:
~/.claude/skills/ ~/.klaudia/skills/ .claude/skills/ .klaudia/skills/
.claude is included because that is where the ecosystem's skill installers
put things (anthropics/skills and friends), for the same reason Klaudia reads
~/.claude/CLAUDE.md. Either layout works in any of them:
skills/review.md # one file per skill
skills/review/SKILL.md # one directory per skill, for skills that ship
# templates, licences or scripts alongside
They become a Skill tool the model can invoke and /<name> commands in the
TUI. Body supports $ARGUMENTS. name defaults to the file's — or the
directory's — name.
---
name: review
description: Structured review of the current diff
---
Review the staged changes carefully. $ARGUMENTSLoaded skills are listed in the startup banner. A skill's name and description are in every request; its instructions load only when the skill is invoked (skill bodies are large, so this is deliberate) — a model saying "registered but not loaded" is reporting correct behaviour.
If a skill doesn't appear at all, run /doctor. With no skills loaded the
Skill tool is not registered at all, so asking the model whether it has skills
gets an honest "I have no such tool" — which is indistinguishable from the
feature being missing. /doctor reports what loaded, from which scope, and
names the directories when nothing did. A skill directory without a SKILL.md
warns at startup rather than being skipped in silence.
/theme recolors the whole UI — banner, prompts, menus, type-ahead, and
Markdown rendering, not just code blocks. Built in: dracula, gruvbox,
tokyo-night, nord, light, catppuccin. Persist a default in config
(project .klaudia overrides ~/.klaudia):
theme = "nord"Two complementary modes for working toward an objective:
- Standing goal —
/goal <text>pins an objective re-stated to the model at the start of every turn so it doesn't drift;/goal clearremoves it. - Goal spec + Ralph loop — for bigger objectives:
/goal(no args) enters goal-setting: it loads an existing spec (./PRD.mdor./.klaudia/GOAL.md) or, if none, helps you draft one (objective, an acceptance-criteria checklist, and a verification command)./goalagain finishes./goal run [N]then runs an autonomous loop against the spec: each iteration re-reads the spec, makes the next valuable change, verifies, and commits — progress accumulating in files and git, not the context window (the Ralph pattern). It runs on a dedicatedklaudia/goal-<slug>branch, stops when the model reports<goal-complete/>or afterNiterations (default 10, cap 50), and is interruptible any time withEscor/goal stop. The status bar showsgoal K/N. On an incomplete stop it runs a final wrap-up turn that records an end-of-run summary in the spec (what's done, what remains, the next step) so a re-run resumes cleanly. When it stops, it prints where the work landed and how to review/merge the branch — the loop never touches your starting branch.- Completion is gated on the spec, not the model's word. Before each run,
the spec's
## Progresstracker is scanned: if the body describes phases that the tracker doesn't list, the first iteration is a stub-fix turn that repairs the tracker. After every claimed<goal-complete/>the loop runs two checks — a mechanical count of remaining- [ ]items, and a one-shot verification turn that re-reads the spec from disk and cross-references it against git/build/tests — and only honours completion if both agree. - Headless/scriptable:
klaudia --loop --dangerously-skip-permissions [--max-iterations N]runs the same loop without the TUI (each iteration with a fresh context). It needs a spec in the cwd and bypass permissions (no human to approve), and also stops if it stalls (no new commits for a few iterations).
- Auto-memory — the
Memorytool stores and recalls notes..klaudia/MEMORY.mdis the index (session bullets); longer notes live as.klaudia/memory/*.mddetail files. The index keeps a## Linked memorysection pointing at those files (name + one-line hook), kept in sync automatically. Only the index is recalled into the prompt — cheap as memory grows — and the model opens a detail note on demand.Memorysearch spans both the index and the detail notes (a hit is tagged with its filename). - Project knowledge —
.klaudia/KNOWLEDGE.md(curated, durable lessons) is injected into the system prompt when present.
| Package | Responsibility |
|---|---|
agent |
the agentic loop + sub-agent spawning |
api |
provider abstraction (Anthropic client + OpenAI-compatible shim) |
tools |
local tool implementations |
browser |
lazy headless-Chrome engine + web search |
lsp |
language-server client for code intelligence (Diagnostics/Definition/References) |
permission |
the three permission modes + the deprecated allow/deny rules (a leaf package) |
trust |
zones, command/tool classification, session-scoped grants |
session |
JSONL transcripts, resume, persisted summaries |
compaction |
micro + auto context compaction |
mcp |
Model Context Protocol client |
subagent |
built-in sub-agent types |
skill |
user-defined skills |
memory |
auto-memory store |
goal |
standing goals, goal specs, and the Ralph loop |
doctor |
/doctor environment diagnostics |
sandbox |
local / OS-confined / container Bash execution |
streamjson |
bidirectional stream-json frontend |
tui |
Bubble Tea terminal UI |
cli |
command entry, flags, wiring |
native |
pure-Go search / bash-parsing / PDF |
prompt, schema, config, version, tasks |
supporting packages |
- CHANGELOG.md — what changed, and why it was done that way
- docs/ux-spec.md — the terminal-UX specs, and where the implementation deliberately departs from them
- docs/trust.md — the host boundary, zones, and what is not protected
- docs/jobs.md — the job model, logs, and its limits
- docs/working-tree.md — change ownership,
/commit,/undo - docs/parity.md — JS→Go feature map and divergences
- docs/compaction.md — context-window management
- docs/memory-architecture.md — index→detail memory store
- docs/server-side-tools.md — Anthropic server-side tool schemas (reference)
Klaudia is a locally-buildable, extensible agentic coding tool for our team's workflow: one static Go binary with a self-contained tooling layer and room to grow tools, providers, and UI.
It began as a cleanroom extraction of Claude Code (@anthropic-ai/claude-code
v2.1.66) — prettified JavaScript split into src/sections/*.js — which served as
the golden reference for differential testing during a full port to Go. The port
is complete and is the product; the JavaScript reference (and the Go sidecar
tools that preceded the pure-Go native packages) is retired to the
js-reference branch — git checkout js-reference to consult it.
Builds are pure Go (CGO_ENABLED=0): the search / bash-parsing / PDF layers are
pure-Go too, so there are no wasm blobs, vendored binaries, or required system
tools.
- Bubble Tea TUI (not React + Ink).
- A multi-provider abstraction (the reference was Anthropic-only): Anthropic Messages API + an OpenAI-compatible shim.
- Local web search/browse via headless Chrome, for providers that have no server
tools of their own (the reference was Anthropic-only, and used its server-side
web_search/web_fetch— which Klaudia still prefers on Claude models, because their results come back cited). - Config and sessions live under
~/.klaudia(KLAUDIA_CONFIG_DIR), not~/.claude. - New capabilities with no reference analogue: language-server code intelligence
(Diagnostics/Definition/References), OS/container Bash sandboxing, persisted
resume summaries, project
KNOWLEDGE.md, an index→detail memory store, standing goals (/goal), chrome-wide themes, managed background jobs with logs, working-tree change ownership (/changes,/undo), and an autonomy model that stops at the host boundary rather than at each action.
- Web: more robust search-result parsing; optional custom MCP auth headers.
- Provider breadth: image tool-results and richer translation across more OpenAI-compatible backends.
- Knowledge: let the agent curate
KNOWLEDGE.mdvia scoped Memory writes; evaluate embeddings for recall. - Extended tooling: project-specific analyzers and custom Go MCP servers.
- Reimplementing the Anthropic SDK or the Claude API.
- Cloud-provider SDK auth (Bedrock / Vertex / Foundry).
- A general-purpose fork — this targets our team's workflow.
{ "mcpServers": { "local": { "command": "my-server", "args": ["--stdio"] }, "remote": { "type": "http", "url": "https://mcp.example.com/v1" } } }