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. - Editor integration (ACP) —
--input-format acpmakes Klaudia the agent behind Zed,acp.nvimor the JetBrains plugin. - Isolated sub-agents — children that write get their own git checkout, so two of them can work at once.
- Effort —
--effortand/effortset how hard the model works per response, on both the Anthropic and OpenAI-compatible backends. - Background work that reports back — a job or sub-agent that finishes wakes the model, so a long build needs no polling.
/rewind— back to before any earlier prompt, files and conversation together.- Plus OS/container Bash sandboxing, local web search & browsing, MCP, and skills.
CGO_ENABLED=0 go install github.com/greenthread-ai/klaudia/cmd/klaudia@latestThis installs the klaudia binary into $(go env GOPATH)/bin (commonly
~/go/bin) — make sure that's on your PATH. @latest is the newest tagged
release; @main tracks HEAD. @latest routes through proxy.golang.org and
can lag a fresh tag by a few minutes — to force a refresh, prefix the command
with GOPROXY=direct.
Or take a prebuilt binary: each release on the GitHub releases page ships
klaudia_<version>_<os>_<arch>.tar.gz for macOS and Linux on amd64 and arm64
(no Windows build — it compiles, but nobody has run it), with
checksums.txt to verify against. klaudia --version prints the version and
the commit the binary was built from.
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. A project config arrives
with the repository, so it may only tighten yours: plan mode, a sandbox
where you have not chosen one, no network, a narrower egress list. Your
provider, baseURL and keys, bypass mode, a looser sandbox, extra writable
directories and browser binary or profile are yours alone; a project setting
any of them is ignored with a warning. 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
Klaudia can reuse an existing Claude Code login: from the macOS Keychain, or
~/.claude/.credentials.json on Linux. 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 # puts the build on your PATH
go test ./...
KLAUDIA_E2E=1 go test ./internal/tui/e2e/... # the real binary in a pty, against a mock modelThe result is one self-contained binary (Linux + macOS). Prefer go install
to go build: a go build binary lands in the working directory, and running
klaudia afterwards runs whatever was installed last (klaudia --version
prints the commit it was built from).
The e2e suite needs no credential and runs in CI. Two more rigs go further
(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, input history (↑/↓, and Ctrl+R to
search it), and Esc to interrupt a turn. Type /help for the commands by group, and /help <command> for one in full.
Input history is kept per project, the last 200 prompts, in
~/.klaudia/sessions/<project>/prompt-history.ndjson: beside the transcripts
and outside the repository, so it cannot be committed by accident. A !
command is remembered as the line you typed, never its output; a prompt holding
a paste chip, or over 8 KiB, stays in the session's history only. Resuming a
session also puts its prompts back under ↑. In a multi-line prompt ↑ and ↓ move
between lines and browse history from the first and last line. Ctrl+R searches
backwards as you type: Ctrl+R again for an older match, Enter to put the
match in the box to edit, Esc to cancel. Ctrl+G opens the draft in
$VISUAL or $EDITOR (falling back to vi or nano) and puts what you save back
in the box.
Tab completes a command's argument too: themes, modes, job names for /logs,
/restart and /stopjob, result numbers for /last, pinned files for
/unpin, approval ids for /trust revoke, and model ids for /model from the
list /model last fetched (Tab never fetches it).
A line that starts with / but is not a command is sent as a message when its
first word is a path (/etc/nginx/nginx.conf fails to parse) or is followed by
more text. A lone unknown /word is reported with the nearest commands. Start a
line with // to always send it as a message, minus one slash.
Typing @ opens a ranked file menu: ↑/↓ pick, Tab inserts and cycles,
Enter takes the highlighted file and closes the menu (it does not send),
Esc dismisses. Matching is fuzzy, so @session.ts finds
src/auth/session.ts, and files Klaudia has just read or written rank first.
A reference starting with /, ./ or ~/ completes from the filesystem
instead of the project, and a path containing spaces is inserted quoted
(@"My Notes.md") so it stays one reference.
URLs and paths to files that exist are hyperlinks: Cmd+click (iTerm2, Ghostty,
WezTerm, VS Code) or Ctrl+click (kitty, Windows Terminal, GNOME/VTE) opens
them. The terminal still owns the mouse — selection, scrolling and search are
untouched — Klaudia only tells it where a click should go. That fixes two
things terminal URL detection gets wrong: a long URL wrapped across rows opens
in full rather than as its first fragment, and internal/tui/tui.go:3303 opens
the file even though the terminal does not know Klaudia's working directory.
In a Markdown link only the printed URL is clickable, never the label, so what
you click always shows where it goes.
Links are on when the terminal is known to support them (also inside tmux 3.4+
when the outer terminal is detectable). KLAUDIA_HYPERLINKS=0 or =1 overrides
detection, as does [links] enabled in the config. A file link targets
file:// by default, which the OS opens without a line number; to land on the
line in your editor:
[links]
file = "vscode://file/{path}:{line}:{col}" # or cursor://file/{path}:{line}Return sends. For a new line, press Shift+Return — or end the line with
\ and press Return, which works in every terminal with no setup at all, as
in Claude Code. (Option+Return on macOS, Alt+Enter on Linux and Ctrl+J
work too.) To swap Return's meaning:
[input]
enter = "newline" # Return inserts a newline; Ctrl+Return sendsShift+Return and Ctrl+Return need a terminal that can tell them from
Return. Plain, they all send the same byte (CR, 0x0D). Klaudia asks the
terminal for the Kitty keyboard protocol and for xterm's modifyOtherKeys, and
terminals that implement either (Ghostty, kitty, foot, xterm and others) then
report the chords as themselves; /help names Shift+Return once the terminal
has said it can. Where neither is available — Apple Terminal — they arrive as
Return, which is why Option+Return, Ctrl+J and the trailing \ stay.
If your terminal does not report them, you can tell it 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 |
Option+Return needs Settings → Profiles → Keyboard → Use Option as Meta key, which also stops Option typing é, © — and on a UK keyboard, # (Option+3). If that is a dealbreaker, end the line with \ instead |
| 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 |
— |
A trailing \ then Return works in every terminal, needing no configuration
at all — Klaudia turns it into a newline instead of sending (only at the end of
the input, so a \ in a path or regex is left alone). Ctrl+J, the LF byte,
also works everywhere in both modes; that 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.
Each tool call is one line, written when it finishes and saying what happened
(✓ Read main.go · 35 lines, ✓ Bash go test ./... · 7.2s), with an edit's
-/+ hunk or a command's output under it; while it runs, the status line
names it. The status bar tracks model · mode · tokens, and context once it
is half way to auto-compact (amber, with what is left, from 80%). 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. While the
model works through a plan its step list sits above the prompt (Plan once
approved, Steps before) — Ctrl+T collapses it to one line — and
disappears once every step is done. /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. The picker
(/model, /theme, /mode, /mcp) is a dropdown: ↑↓ to move, Enter to
confirm, type to filter, Esc to cancel, and 1-9 still pick directly until
you start filtering. /model <alias|id> still sets one directly (opus, sonnet, haiku, fable, or any
full ID). The aliases are Anthropic's names and expand only on the Anthropic
provider; another provider receives the string as written, with a warning if
it is an alias. Either way Klaudia records that model's real context window and
output limit, which is what auto-compaction (and the status bar's ctx N%) is sized from and what
the next turn's max_tokens asks for — a model switch moves both.
/theme switches the colour theme (Markdown + chrome) for the session; set a
durable default with theme = "nord" in .klaudia/config.toml (klaudia |
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. A job that exits on its own wakes the model with its
result, so it can end its turn and be told. 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?"
# Piped stdin is the prompt, or is appended to one given as an argument
echo "What files are in this directory?" | ./klaudia -p
git diff | ./klaudia -p "Review this change"
# 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 SDK integrations and for an editor without ACP support (no terminal needed):
klaudia --input-format stream-json --verbosePermission asks, the model's questions (AskUserQuestion) and plan approvals
(ExitPlanMode) all arrive as control_request lines and are answered on the
matching request_id. A peer that has not implemented a subtype should answer
with an error: that reads as "cancelled" and leaves the model where it was,
rather than an answer the user never gave being attributed to them. A request
nobody answers is denied (or, for a question or plan, fails) after
--ask-timeout (default 10 minutes; 0 waits forever), so a client that does
not speak the control protocol sees a finished turn, not a hung process.
The first line is {"type":"system","subtype":"init","schema_version":1,…}
with the session id, model, working directory, permission mode and tools. The
events that follow are agent.Event serialised as is — type is one of
assistant, tool_use, tool_progress, tool_result, usage,
compaction, notice, permission_mode, plan or steer, and each turn
ends with a result line. A background job that exits on its own writes
{"type":"system","subtype":"job_exit","job_id":…,"exit_code":…} when it
happens, between turns or not (and job_output when a watched job prints a
matching line, agent_done when a background sub-agent finishes); the model gets the report with its next turn
(mid-turn if one is running), and a headless -p run hands it over mid-run.
schema_version is agent.EventSchemaVersion:
new event types and optional fields keep it (ignore what you do not know);
renaming or removing a field, or changing what one means, bumps it. One-shot
-p --output-format stream-json writes Claude Code's envelopes instead, and
opens with the same kind of system/init line Claude Code does.
klaudia --input-format acpKlaudia serves the Agent Client Protocol v1
over stdio, so Zed, Neovim's acp.nvim and the JetBrains plugin can drive it as
their coding agent — the editor's own UI for messages, tool calls, diffs,
checklists and permission prompts, Klaudia doing the work. In Zed
(~/.config/zed/settings.json):
{
"agent_servers": {
"Klaudia": {
"type": "custom",
"command": "klaudia",
"args": ["--input-format", "acp"]
}
}
}Sessions, modes and commands map onto what Klaudia already has: ACP session ids
are transcript ids (so --resume and the editor's thread list agree), the
mode picker is /mode and is per thread, TodoWrite becomes a native plan,
Edit and Write carry diffs, and .klaudia/skills appear in the command
palette. fs/read_text_file is used when the editor offers it, so the model
reads your unsaved buffer rather than stale text on disk.
Writing through the client, terminal/* and session/delete are declined on
purpose — the reasoning, and the rest of the surface, is in
docs/acp.md.
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 the embedding
protocols (--input-format stream-json and acp) stay stateless unless you
pass --continue or -r <id>. An ACP client has session/list and
session/load for the same job.
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 been compacted, resume seeds from the last summary plus everything
said after 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. Running jobs
are listed under the prompt with their age and port.
The model is told too. A job that exits on its own reports to the model with
the end of its output — at its next step if it is mid-turn, or as a turn of
its own if Klaudia is idle — so it can start a 40-minute provisioning run, end
its turn and be woken by it (a job stopped on request is not reported). Started
with watch, a regex, a job also reports output lines that match as they
appear, so "start the server and tell me if it errors" needs no polling;
reports are batched (the first at once, then at most every 30s). BashOutput
can wait up to 600 seconds for an exit or a filter match in one call rather
than poll. This works in the TUI, headless -p and --loop runs, and over
stream-json. Logs rotate at 64 MiB, and a read returns at most the newest 4 MiB.
Sub-agents can run in the background too: the model sets run_in_background
on the Agent tool for independent work it does not need before its next step,
and carries on. Every running sub-agent, foreground or background, is listed
under the prompt (⏺ main, one ◯ row each) with its latest step, how long
it has run and the tokens it has used. A background agent survives Esc (which ends the turn, not the research), and when it
finishes its result goes to the model — straight away if Klaudia is idle, or
at its next step if it is mid-turn. /stopjob agent_N stops one and passes on
what it had. A headless -p run does not end while a background agent it
started is still working: it waits, and gives the result to the model as one
more turn. Over stream-json the result reaches the model with its next turn
(or the running one) and the peer gets a system/agent_done line. ACP keeps
sub-agents in the foreground.
A sub-agent can also be a fork (fork on the Agent tool): it starts from
a copy of the conversation so far, so it knows what has been read and decided
without being told, works on its own task, and hands back only its answer —
the main thread is untouched. It costs the whole context again, so the model
is told to keep it for tasks that depend on the discussion.
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, and /diff shows the diff in git's
colours, in $PAGER when it is long, naming untracked files that git diff
leaves out. /commit stages only its own work (or what you staged), lists
what it left out, and with no message drafts one from exactly what will be
committed, in the style of the repository's recent subjects — shown first,
and committing still needs a y. /undo
restores its last change, never touching a file you also edited, and
/rewind goes back to before any earlier prompt this session — files and
conversation together, with the prompt returned to edit — or, as its second
option, summarises everything before that prompt and keeps it and what
followed word for word, which frees the context the early conversation was
using without touching a file.
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.
Sub-agents that can write get a checkout of their own. Two children editing
one tree is not a race in the usual sense — nothing errors, and every step
succeeds: one writes a file, another reads a half-written version and reasons
about it, a third rewrites the first one's edit. So a sub-agent holding Write,
Edit, NotebookEdit or Bash runs in its own git worktree under
~/.klaudia/worktrees/, seeded with your uncommitted work rather than HEAD,
and its changes are applied back to your tree with git apply when it finishes
— files only, never your index. Explore and Plan keep sharing the tree:
they run in plan mode, so they cannot write (under bypassPermissions they get
a checkout like any writer). Anything that could not be applied because the
file moved underneath is named to you and to the model, and the checkout is
kept so you still have that version. Ignored files are not copied, so a child
that needs an install step before it can test will pay for it — turn the whole
thing off with [subagents] worktree = false if that is the wrong trade for
your project.
/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". To limit where data can go, list the hosts a
project needs in [sandbox] egress; everything else is refused by an
in-process proxy — enforced under the macOS OS sandbox, advisory elsewhere
(see docs/trust.md).
| Flag | Mode | Behavior |
|---|---|---|
| (default) | autonomous |
Finish the task; ask before changing this machine |
--permission-mode plan |
plan |
Read-only research: commands that only inspect run, writes are refused, and a command Klaudia cannot classify asks |
--permission-mode bypassPermissions |
bypassPermissions |
Allow everything, including host changes — the one way past the gate |
/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 removed. There are no allow/deny
rules, no --allowedTools/--disallowedTools, no /allow or /deny, no
default/acceptEdits/dontAsk modes, and no "always" answer on a prompt;
[permissions] allow/deny and [trust] in .klaudia/config.toml are no
longer read. /trust grants by what an operation does rather than by
matching command text, and an approval is session-scoped.
Running both models at once was the problem: a rule in a project config demoted the next session to a mode that asked before every edit, so "always" — the answer offered to stop a prompt — was what produced more of them.
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" — the API dialect spoken at baseURL,
# not the vendor: vLLM, llama.cpp and LiteLLM all speak "openai".
provider = "openai"
model = "openai/gpt-5.5"
# The endpoint, in whichever dialect `provider` selects. Include /v1 for
# OpenAI-compatible hosts. Omit it to use the provider's default (Anthropic
# production); it is required under provider = "openai".
# An Anthropic-dialect proxy or gateway goes here too, with provider unset.
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: headers sent on every request, each read from the NAMED env var,
# for an endpoint gated by headers (a Cloudflare Access service token) instead
# of, or as well as, a key. With these set, no key is required. User config
# only: a project's .klaudia/config.toml cannot set it.
# extraHeadersEnv = { "CF-Access-Client-Id" = "CF_ID", "CF-Access-Client-Secret" = "CF_SECRET" }
# Optional: set the model's context window in tokens so autocompaction kicks
# in before the provider overflows. Klaudia resolves it for you — this key, then
# the model's own figure from GET /v1/models, then a built-in table — and falls
# back to 200000 when nothing can size the model. Set it when your endpoint
# neither reports a window nor serves a model Klaudia knows (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. Resolved the same way:
# this key, then the model's own maximum from GET /v1/models, then a built-in
# table (128000 on the 1M-context Claude models, 64000 on the 200k ones), and
# 8192 only when nothing can size the model. That 8192 floor is low enough to
# truncate a large Write mid-call, so it is worth setting if your endpoint
# reports no limit. If this is set above what the model allows, the provider
# says so and Klaudia retakes the turn at the limit it named.
# maxTokens = 32000
# Optional: idle compaction. When the conversation is at 85% or more of the
# compaction point, the TUI compacts it after four idle minutes — before the
# prompt cache expires, so the summary is cheap — instead of making the next
# turn stop for it. Skipped while you are typing. On by default.
# idleCompact = false
# Optional: what the Return key does at the prompt. "send" (default) submits
# and Shift+Return, Option+Return / Alt+Enter (or a trailing \) insert a
# newline; "newline" swaps them, and Ctrl+Return sends. See "Return, and
# multi-line input" above for which terminals can send Shift/Ctrl+Return.
# [input]
# enter = "newline"
# Optional: clickable links. URLs and paths to existing files are OSC 8
# hyperlinks (Cmd/Ctrl+click) in terminals known to support them; enabled
# forces it either way, as does KLAUDIA_HYPERLINKS=0/1. A path links to
# file:// by default — set file to open your editor at the line instead.
# [links]
# enabled = true
# file = "vscode://file/{path}:{line}:{col}" # or cursor://file/{path}:{line}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.
baseURL and provider are an endpoint/dialect pair, and both apply to either
backend: point baseURL at an Anthropic-compatible proxy with provider
unset, or at an OpenAI-compatible host with provider = "openai". Klaudia does
not infer the dialect from the URL — a wrong guess would surface as a protocol
mismatch from the server rather than as something naming the cause.
KLAUDIA_CUSTOM_ENDPOINT overrides baseURL for one run, whichever provider
is selected.
In order, first hit wins:
ANTHROPIC_API_KEY(orANTHROPIC_AUTH_TOKEN);- a key stored with
klaudia --login, which prompts without echo (or reads one piped on stdin) and writescredentials.jsonunder~/.klaudia, 0600; - an existing Claude Code login, from the macOS Keychain or
~/.claude/.credentials.jsonon Linux (Klaudia refreshes an expired token and writes it back, atomically and keeping Claude Code's other fields).
For an OpenAI-compatible provider, the key comes from apiKey / apiKeyEnv
in ~/.klaudia/config.toml (a project config cannot set it).
streamIdleTimeout(config) — 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.maxRetries(config) — retries for connection errors, 429s and 5xx, with backoff that honoursRetry-After(default5;0disables).fallbackModels(config) /--fallback-model— models to continue on when the model is overloaded (529) or the provider keeps failing with a 5xx after those retries, e.g.fallbackModels = ["sonnet", "haiku"]. The run carries on with the next one, effort and output cap refitted to it, and a notice names the switch. Sub-agents get the same chain. A 4xx or a 429 quota never falls back: another model would fail the same way.- 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).
--restricted runs without this machine's state, for evals and CI: the
process gets an empty config root, so user config, the global AGENTS.md and
.mcp.json, skills, hook and MCP approvals, session history and job logs are
all absent (and nothing is written back); ~/.claude/skills and
~/.claude/CLAUDE.md are skipped too; and memory and hooks are off. The
repository's own files still apply, since they are what is being run. How to
reach the model is kept: the provider, baseURL, key and model from your user
config.toml are carried over, as is anything in the environment.
Set the standard OpenTelemetry variables and Klaudia exports traces over
OTLP/HTTP — OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 is enough for
a local collector or Jaeger; headers, timeouts and OTEL_SERVICE_NAME come
from the same variables, and OTEL_SDK_DISABLED=true turns it off. Unset,
nothing is exported and the spans cost nothing.
Spans follow the GenAI semantic conventions (pinned to 1.37.0): an
invoke_agent span per turn, a chat <model> span per model request with
token usage (cache included) and finish reason, and an execute_tool <name>
span per tool call, with a sub-agent's spans nested under the Agent call that
started it. MCP tool calls carry the trace on as traceparent in the request's
_meta, so a server that traces joins it. The core (pkg/agent) imports only
the OTel API: an embedder installs whatever provider it already uses.
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.
Effort is how many tokens the model spends on a response — thinking, tool calls and text together. On the current Claude models thinking is always on and effort is the control for how deep it goes.
klaudia --effort xhigh # one run
export KLAUDIA_EFFORT=medium # your shell# .klaudia/config.toml
effort = "xhigh" # low | medium | high | xhigh | max
[subagents]
effort = "low" # unset = inherit the session's levelPrecedence is --effort → KLAUDIA_EFFORT → config; /effort <level> beats
all three for the running session, and /effort off restores the provider's
default. /effort with no argument lists the levels and marks the live one.
maxEffort = "high" caps every one of those, [subagents] effort too, and
/effort refuses a level above it.
Unset is the default and sends nothing, so you get the provider's own default
(high on most Claude models) — unless maxEffort is set, in which case an
unset effort is sent as the cap on models known to take effort. The level reaches both backends from one field —
output_config.effort on Anthropic, reasoning_effort on OpenAI-compatible
endpoints — and all five values pass through unchanged on both, so the setting
means the same thing whichever you are on.
Two things worth knowing, both of which Klaudia tells you about when they
apply. Changing effort mid-session invalidates the conversation's prompt cache,
so the next turn re-reads the whole prefix at full rate. And max_tokens
bounds thinking and text together, so xhigh or max against a small output
cap truncates mid-thought rather than erroring — /doctor warns when that is
the setup.
Full detail, including the per-model ceilings and why the cache-preserving beta is not used: docs/effort.md.
.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 or cannot run (bwrap needs unprivileged user namespaces); setfailIfUnavailable = trueto refuse to start instead.container— run inside docker/podman (runtime,image,mountCwd,readOnly,network).
sandbox.egress limits where commands can connect, in any mode:
[sandbox]
egress = ["github.com", "*.npmjs.org"]Every command is pointed at an in-process proxy that forwards to those hosts
and refuses the rest with a 403 naming the host and the setting. It is
enforced under the macOS OS sandbox (the profile allows outbound traffic to
the proxy only); in local mode and under bubblewrap it is advisory — a
program that ignores the proxy variables is not stopped — and in a container or
with network = "none" it is not applied, with a warning. /doctor shows
which, and the hosts refused. See
docs/trust.md.
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
if it isn't found). Tunable via .klaudia/config.toml →
browser (engine, headless, chromePath, userDataDir, headedFallback,
…) or KLAUDIA_* env vars. When a search or a fetch comes back with a
bot-challenge page instead of the content (Cloudflare, DataDome, a captcha),
Klaudia says so and relaunches a headed Chrome with a persistent profile
(~/.klaudia/browser/…) so you can clear it: the window closes as soon as the
page comes through, and the cookie you earned means the next headless fetch of
that site usually needs no window at all. Set headedFallback = false (or
KLAUDIA_BROWSER_HEADED_FALLBACK=0) to never open one — the challenge page is
then returned as-is, labelled as a challenge. 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. Servers start in parallel, each
with 60 seconds to answer; a tool call gets ten minutes
(KLAUDIA_MCP_TOOL_TIMEOUT, or "timeout" in seconds on the server).
A server is stdio (command +
args) or HTTP (url, with type:"sse" for the legacy SSE transport, and
headers sent on every request — typically Authorization). A url with no
type tries streamable HTTP first and falls back to SSE. // and /* */
comments are allowed; a file that still doesn't parse is reported — naming the
file, since all three share a base name — and skipped, while the other files
still load:
Servers from the project's files arrived with the clone, so — like project
hooks — they wait for your approval: your first message shows each
server's command or URL (or approve them sooner from /mcp, which lists them as
waiting), and the answer is remembered in
~/.klaudia/mcp-approvals.json until the set changes. The global file's
servers start at once. A headless run starts project servers only with
--allow-host-changes (see docs/hooks.md).
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.
Klaudia speaks protocol 2026-07-28, which lets a server ask you for
something mid-call — a token it has no other way to obtain, a branch name, a
yes/no before something destructive. The question arrives at the same prompt the
model's own AskUserQuestion uses, labelled with the server that asked. Each
field in the server's schema is one question; booleans and enums (titled ones
too) become choices, a multi-select is answered as a comma-separated list, a
declared default is shown and can be accepted as is, and anything else is
answered by picking the free-text row and typing. You can always skip an
optional field or cancel the whole form, and cancelling tells the server you
cancelled rather than handing it a guess.
Two deliberate limits. Only form elicitation is supported, and only form is advertised: the spec's other mode hands the client a URL to open out of band, which a terminal cannot do without reaching outside the project, and a server told "yes" for a link that was only printed into scrollback is worse off than one that was told no. And headless runs advertise nothing, because there is nobody to ask — a server that sees no elicitation capability takes its own non-interactive path instead of waiting for an answer that is never coming.
The legacy HTTP+SSE transport (type:"sse") still works, but the spec has
deprecated it in favour of streamable HTTP and servers drop it on their own
schedule. /doctor warns when a configured server is still on it, because the
symptom otherwise is a connect error that says nothing about the one word that
fixes it.
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. Those sub-agents run in plan mode,
and plan mode allows exactly these: an MCP tool marked read-only is callable
there, any other is refused — in the main session's plan mode too.
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: read-only
sub-agents and plan mode stop seeing its tools as read-only, and the main agent
keeps calling them without asking, as it does every MCP tool outside plan mode.
This is not a claim that calling them is safe.
${VAR} and ${VAR:-default} are expanded from your environment in
command, args, env, url and headers, as Claude Code does, so a token
for an HTTP server can be "Authorization": "Bearer ${GITHUB_TOKEN}" rather
than a literal in the file. A variable that is unset with no default fails
that server's connect, naming it. For a project's servers the approval card
shows the templates as written and says which variables each server reads and
where a remote one sends them; changing a template asks again. A stdio server
also inherits Klaudia's environment, so it needs no env entry to see a
variable you exported. .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.
A stdio server's stderr is where it logs. In headless (-p) and embedding
runs each line is forwarded to Klaudia's stderr as mcp[<name>]: …;
interactive runs do not, because the TUI owns the terminal. Set
KLAUDIA_MCP_STDERR=<dir> to also append each server's stderr to
<dir>/<name>.log, in any mode. A server that fails to start has the tail of its
stderr appended to its connect error.
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 still matters: MCP tools are deferred behind
ToolSearch, but every tool the model has discovered rides on each request
from then on, and a smaller toolset makes discovery itself more accurate.
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). A server that doesn't report within 10s (still starting or indexing) is returned as an error, not as a clean file.Definition/References/Implementation— jump to a symbol's definition, find its uses, or find the concrete implementations of an interface.Hover— the type, signature, and docs a server shows on hover.DocumentSymbols— an outline of the functions, types, and methods in a file.Rename— a preview of the edits a symbol rename would make across the workspace (it returns the edits; it does not apply them).WorkspaceSymbol— find a symbol by name anywhere in the project, when the agent knows what it's called but not which file declares it. Given afile, that file's language server answers; without one, every language whose project file (go.mod,Cargo.toml,package.json, …) is at the workspace root is searched.
Location, symbol, and hover results include the source line's text alongside
its file:line, so the agent sees the code without a follow-up read. A server
that lacks a capability is reported as "not supported" rather than failing.
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
skills/synced/<id>/review/ # a container of skill directories, which is how
# Claude's synced skills arrive
Hidden directories are skipped, so deleted skills parked in
~/.claude/skills/.trash/ stay deleted. A directory is only reported as a
broken skill if it holds Markdown but no SKILL.md; a directory that merely
contains other skill directories is a container, not a mistake.
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.
When Klaudia needs you — a permission prompt, a question, a plan to approve, or a turn that ran 20s or more finishing — and your terminal is not focused, it says so: a desktop notification on terminals known to show one (iTerm2, WezTerm, Ghostty), the bell everywhere else and inside tmux. Klaudia asks the terminal for focus reports, so nothing fires while you are looking; a terminal that never reports focus is treated as unfocused.
[tui]
notify = "auto" # default; or a list of bell, osc9, osc777; or all; or offThe same moments run the Notification hook, for a phone or a chat:
[[hooks]]
event = "Notification"
command = "jq -r .message | xargs -I{} ntfy publish klaudia {}"A hook is a shell command run at a fixed point in a turn — the escape hatch for things Klaudia should not have an opinion about:
# .klaudia/config.toml — or ~/.klaudia/config.toml
[[hooks]]
event = "PostToolUse" # run the project's formatter after every write
matcher = "Edit|Write"
command = "gofmt -w $(jq -r '.tool_input.file_path') 2>/dev/null"
[[hooks]]
event = "PreToolUse" # refuse edits to generated files
matcher = "Edit|Write"
command = '''
case "$(jq -r .tool_input.file_path)" in
*.pb.go) echo "generated file — edit the source" >&2; exit 2 ;;
esac
'''
[[hooks]]
event = "UserPromptSubmit" # stdout is added to the conversation
command = "echo \"Branch: $(git branch --show-current)\""
timeout = "5s"Five events, deliberately: SessionStart, UserPromptSubmit, PreToolUse,
PostToolUse, and Notification (when Klaudia needs you; see
Notifications). The hook is handed a JSON object on stdin using Claude Code's
field names (tool_name, tool_input, tool_response, prompt, …) so
existing hook scripts work unedited. Exit 2 blocks and its stderr is the
reason the model is given; any other non-zero status is a malfunction, reported
to you and never to the model. stdout becomes context.
A hook is not a security boundary. PreToolUse runs after the host gate
and permission check have allowed a call, so a hook can narrow what Klaudia will
do and never widen it. And because a project's .klaudia/config.toml arrives
with a clone, hooks from a repository are confirmed once — you are shown
every command, the decision is remembered in ~/.klaudia/hooks.json, and
editing the set asks again. Your own ~/.klaudia/config.toml hooks run without
prompting. /doctor lists what is attached and warns about a set still waiting
on approval.
Full semantics: docs/hooks.md.
/theme recolors the whole UI — banner, prompts, menus, type-ahead, and
Markdown rendering, not just code blocks. Built in: klaudia, dracula,
gruvbox, tokyo-night, nord, light, catppuccin. klaudia — sky blue
and bronze on near-black — is what you get until you choose otherwise. Persist
a default in config (project .klaudia overrides ~/.klaudia):
theme = "nord"/plan makes the session read-only: edits, writes and MCP tools not marked
read-only are refused, commands that only inspect (git log, git diff,
ls, sed -n, …) run, and a command Klaudia cannot classify asks you. The
model researches, then presents a plan, and you pick:
- Yes — run it (
y): plan mode ends and the model works the plan. - Yes — and keep going until every step is done: a plan run. If the model stops with steps left it is sent back to them, every step done is followed by one verification turn, and the run ends with a single summary.
- No — keep planning (
n): say what should change; it goes to the model.
The approved plan is saved to ~/.klaudia/plans/<session-id>.md: the plan as
approved, then a checklist of its steps. Approval starts that checklist from
the plan's own steps (a new plan replaces the previous one's list), and the
model keeps it current with TodoWrite as work goes; where the two differ,
the checklist is what the work follows. The live panel above the prompt shows
it — titled Plan once a plan is approved and Steps for a working list
that is not — and Ctrl+T collapses it. --resume reloads it. /plan run [N] resumes a plan with steps left
(up to N turns, default 10), and /plan stop — or Esc — halts it. /plan off
leaves plan mode without a plan. Questions the model asks (AskUserQuestion)
use the same picker: ↑↓ or a digit, Enter to confirm, and the last row to
answer in your own words — typing alone never answers.
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 [--max-iterations N]runs the same loop without the TUI (each iteration with a fresh context). It needs a spec in the cwd and a mode that does not ask — autonomous, the default, or bypassPermissions — plus--allow-host-changesif the work changes this machine, and also stops if it stalls (no new commits for a few iterations).
Klaudia reads both, at three levels, in increasing precedence — generic first at each level, so an agent-specific file reads as the refinement:
~/.claude/CLAUDE.md ~/.klaudia/AGENTS.md <git root>/AGENTS.md <git root>/CLAUDE.md ./AGENTS.md ./CLAUDE.md
AGENTS.md is the cross-agent standard; CLAUDE.md is the
Claude-family one. Both are read because a repo may carry either or both, and
ignoring one silently drops instructions the user wrote for exactly this
purpose. ~/.claude is included for the same reason skills are read from
there: that is where the ecosystem puts things.
If you support both with a symlink (ln -s AGENTS.md CLAUDE.md) or a copy, the
contents are sent once, not twice — identity is established by resolved path
and by content hash. The prompt's section header names the files it actually
used, so asking Klaudia to "write that down" puts it in the right one.
- 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: the note's frontmatterdescription, else its first line after any frontmatter), kept in sync automatically. Only the index is recalled into the prompt — capped at 200 lines / 25 KB, with a line saying how much was left out — 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);removeforgets one session note (byquery) or a detail note (byname). - Project knowledge —
.klaudia/KNOWLEDGE.md(curated, durable lessons) is injected into the system prompt when present.
Every KLAUDIA_* and ANTHROPIC_* variable Klaudia reads. Retries and the
stream idle timeout are config.toml keys (maxRetries, streamIdleTimeout),
not variables. For the [browser] settings the config
key wins over the variable. Boolean variables take 1/true/yes/on or
0/false/no/off; anything else is ignored.
| Variable | Default | Purpose |
|---|---|---|
ANTHROPIC_API_KEY |
unset | Anthropic API key. Wins over every other Anthropic credential. |
ANTHROPIC_AUTH_TOKEN |
unset | Bearer token for the Anthropic API, used when ANTHROPIC_API_KEY is unset; wins over the --login store and a Claude Code sign-in. |
KLAUDIA_CUSTOM_ENDPOINT |
the provider's | Base URL for the model endpoint (a proxy or gateway); wins over baseURL in config.toml. |
KLAUDIA_CONFIG_DIR |
~/.klaudia |
Where Klaudia's own user files live: config.toml, sessions, tool-output spills, job logs, caches, the Chrome profile and the global .mcp.json. |
KLAUDIA_EFFORT |
unset | Default effort (low, medium, high, xhigh, max); --effort beats it, and it beats effort in config.toml. |
KLAUDIA_HYPERLINKS |
detected | 0/false turns clickable links off, anything else on, whatever the terminal detection or links.enabled says. FORCE_HYPERLINK is honoured the same way. |
KLAUDIA_LOG |
unset (discarded) | File to append the standard log package's output to while the TUI runs. |
KLAUDIA_BROWSER_LOG |
unset | File to append chromedp's unfiltered browser/protocol log to. |
KLAUDIA_MCP_STDERR |
unset | Directory to append each stdio MCP server's stderr to, as <name>.log. |
KLAUDIA_MCP_TOOL_TIMEOUT |
600 |
Seconds one MCP tool call may take; "timeout" on a server in .mcp.json beats it. |
KLAUDIA_WEB_SEARCH_ENGINE |
ddg |
BrowserSearch engine: ddg or google. Config: browser.searchEngine. |
KLAUDIA_CHROME_PATH |
discovered | Chrome/Chromium executable to launch. Config: browser.chromePath. |
KLAUDIA_CHROME_REMOTE_URL |
unset (launch Chrome) | DevTools endpoint of a running Chrome to attach to instead. Config: browser.remoteUrl. |
KLAUDIA_CHROME_USER_DATA_DIR |
<config dir>/browser/… |
Chrome profile directory. Config: browser.userDataDir. |
KLAUDIA_BROWSER_HEADLESS |
true |
Run the launched Chrome headless. Config: browser.headless. |
KLAUDIA_BROWSER_HEADED_FALLBACK |
true |
On a search bot-challenge page, relaunch a visible Chrome so you can solve it once. Config: browser.headedFallback. |
Variables you name yourself (apiKeyEnv, extraHeadersEnv, ${VAR} in
.mcp.json) are not listed, nor are the ones only tests read.
cmd/klaudia/envdocs_test.go keeps this table honest: it fails when non-test
code names a KLAUDIA_* or ANTHROPIC_* variable that has no row here, or a
row names one that no code reads.
| Package | Responsibility |
|---|---|
pkg/agent |
the agentic loop + sub-agent spawning |
pkg/api |
provider abstraction (Anthropic client + OpenAI-compatible shim), model sizing |
anthropicauth |
Klaudia's credential discovery (env, --login store, Claude Code's login) and Anthropic request shaping |
tools |
local tool implementations |
browser |
lazy headless-Chrome engine + web search |
lsp |
language-server client for code intelligence (Diagnostics/Definition/References) |
pkg/permission |
the three permission modes (a leaf package) |
trust |
zones, command/tool classification, session-scoped grants |
hostgate |
the host gate (agent.Gate), its approval card, the headless approver |
session |
JSONL transcripts, resume, persisted summaries |
pkg/compaction |
micro + auto context compaction |
mcp |
Model Context Protocol client (2026-07-28) + form elicitation |
subagent |
built-in sub-agent types |
worktree |
per-sub-agent git checkouts: seed, adopt, conflict reporting |
skill |
user-defined skills |
hooks |
lifecycle hooks: events, project-hook trust, execution |
memory |
auto-memory store |
goal |
standing goals, goal specs, and the Ralph loop |
doctor |
/doctor environment diagnostics |
sandbox |
local / OS-confined / container Bash execution |
egress |
the [sandbox] egress allowlisting proxy |
streamjson |
bidirectional stream-json frontend |
acp |
Agent Client Protocol v1 agent (editor-driven sessions) |
tui |
Bubble Tea terminal UI |
cli |
command entry, flags, wiring |
native |
pure-Go search / bash-parsing / PDF |
pkg/plan, goal |
the session plan and its artefact; goal specs and the loop that also drives plan runs |
pkg/schema |
JSON Schema for tool inputs |
pkg/tool |
the tool contract: interface, registry, per-call context, results, output spill |
prompt, config, fuzzy, version |
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/hooks.md — lifecycle hooks, and why project hooks are confirmed
- docs/jobs.md — the job model, logs, and its limits
- docs/working-tree.md — change ownership,
/commit,/undo, and the sub-agent checkouts - docs/acp.md — the ACP frontend: what is served, and what is declined
- docs/effort.md — effort levels, model support, and the two costs it is easy not to notice
- docs/releasing.md — tagging, the build matrix, and why
--versionreports a commit rather than a number - 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)
- docs/plan-mode.md — plan mode's design record: the mutation gate, the plan store, plan runs
- docs/roadmap.md — recommendations and the extraction plan (forward-looking, not a record)
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, sessions, plans, hooks approvals, user skills and the global
AGENTS.mdlive under~/.klaudia(moved together byKLAUDIA_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), per-sub-agent git checkouts, an ACP frontend for editors, and an autonomy model that stops at the host boundary rather than at each action.
docs/roadmap.md — what comes next and in what order, where
agentic tooling is heading, the extraction of the agent loop into pkg/ as an
embeddable Go library, and the TUI audit.
- 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", "headers": { "Authorization": "Bearer …" } } } }