Skip to content
 
 

Repository files navigation

Klaudia

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's terminal UI

Klaudia keeps full parity with the reference and then builds past it — the extras we lean on day to day:

Getting started

Install

go install github.com/greenthread-ai/klaudia/cmd/klaudia@main

This 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.

Create a config (optional but recommended)

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.

Authentication

Pick one of these paths:

Anthropic API key

export ANTHROPIC_API_KEY="sk-ant-..."
klaudia

Existing 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
klaudia

OpenAI-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
klaudia

Once the TUI starts, type /doctor to verify auth and environment status.

Build

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.

Usage

Interactive (TUI)

./klaudia

A 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, and multi-line input

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 sends

Ctrl+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.

Headless (one-shot)

# 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-messages

Embedding (stream-json over stdin)

A 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

Resuming

./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.

Long-running commands, logs, and your shell

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.

Your changes and Klaudia's

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.

Autonomy and the host boundary

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.

Model & provider

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.

Auth

  • ANTHROPIC_API_KEY (or ANTHROPIC_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 / apiKeyEnv in .klaudia/config.toml.

Streaming & reliability

  • KLAUDIA_STREAM_IDLE_TIMEOUT — seconds a streamed model turn may go without any event before it's treated as a stalled connection (default 120). 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 to 0 to 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 contextWindow so autocompaction triggers earlier, and /compact).

Logs & diagnostics

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.Discard while the program runs, as a backstop for the next dependency that reaches for log.Printf.

Two env vars recover the raw output when you're debugging:

  • KLAUDIA_LOG — file path for anything the process writes via the standard log package (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.

Sandboxing the Bash tool

.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); network configurable. Falls back to local with a warning if the tool is absent.
  • container — run inside docker/podman (runtime, image, mountCwd, readOnly, network).

Web search & browsing

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.

MCP

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:

{ "mcpServers": {
  "local":  { "command": "my-server", "args": ["--stdio"] },
  "remote": { "type": "http", "url": "https://mcp.example.com/v1" }
} }

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.

Code intelligence (LSP)

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

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. $ARGUMENTS

Loaded 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.

Themes

/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"

Goals & autonomous iteration

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 clear removes it.
  • Goal spec + Ralph loop — for bigger objectives:
    • /goal (no args) enters goal-setting: it loads an existing spec (./PRD.md or ./.klaudia/GOAL.md) or, if none, helps you draft one (objective, an acceptance-criteria checklist, and a verification command). /goal again 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 dedicated klaudia/goal-<slug> branch, stops when the model reports <goal-complete/> or after N iterations (default 10, cap 50), and is interruptible any time with Esc or /goal stop. The status bar shows goal 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 ## Progress tracker 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).

Memory & project knowledge

  • Auto-memory — the Memory tool stores and recalls notes. .klaudia/MEMORY.md is the index (session bullets); longer notes live as .klaudia/memory/*.md detail files. The index keeps a ## Linked memory section 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. Memory search 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.

Internal package layout

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

Documentation

Background

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.

Deliberate divergences from the reference

  • 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.

Roadmap

  • 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.md via scoped Memory writes; evaluate embeddings for recall.
  • Extended tooling: project-specific analyzers and custom Go MCP servers.

Non-goals

  • 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages