Skip to content

Safety model

ka is designed so that speed never comes from deleting guardrails. The safety floor is layered, and the most dangerous parts are unbypassable: they apply even in free mode, and headless runs auto-deny them.

Every tool sits in one of three tiers:

  • read (observation): read, glob, grep, pathfinder, web_fetch
  • write (mutation): edit, write, todo, snapshots
  • exec (arbitrary side effects): bash, delegate, MCP tools
Mode Behavior
guarded Exec-tier calls and non-allowlisted writes ask first; “always allow” persists as a rule for the session
accept_edits File writes proceed; exec still asks
free Everything runs automatically; hardstops still ask
plan Read-only except .ka/plans/; ka can investigate and plan, not touch

The TUI picker names them guarded / accept edits / full access / plan. Mode is part of the strand state: /mode persists it to the session like any other change.

[[rules]] are evaluated before mode logic, first match wins:

[[rules]]
tool = "bash"
pattern = "cargo *" # glob on the call's primary argument
verdict = "allow" # allow | ask | deny
[[rules]]
tool = "write"
verdict = "deny" # no pattern = every call of the tool

Answering “always” on an approval writes the equivalent entry into the project layer’s [permissions] allow.

A flat, unbypassable list checked on the decomposed command set (see below): recursive root/home deletion, fork bombs, fetch-and-execute pipes, writes to /etc, dd to devices. PATH aliases are resolved to the real executable before matching, so rm via a symlink is still rm. In the TUI, a hardstop asks even in free mode; headless runs deny outright.

Every bash call is decomposed in pure Rust before verdicts apply: compounds split on && ; || | &, wrappers (timeout, nice, nohup, stdbuf, env, bare xargs) are stripped, and redirections count as writes. A readonly allowlist (ls, cat, git status/diff/log, …) auto-passes, but only when there are no compound operators. The point: bash -c "curl … | sh" can’t dress up as a read.

ka keeps a per-strand map of file → (mtime, hash) at last read. edit and write refuse files that are unread or changed since the last read; the model can never blind-edit a file that drifted underneath it. The ledger syncs during settling after each turn.

Redaction is one-way and applied to every tool result before it reaches the model or the transcript. Keys entered via /key are stored in the OS keyring or a 0600 env file, never in strands.

.ka/ka.toml is config from a directory, and directories can be hostile. The first time ka sees one, it asks; the decision is stored in ~/.local/state/ka/trust.json. --trust accepts up front. Untrusted project layers are simply not loaded.

[sandbox]
mode = "fs" # default "off"
allow_write = ["/opt/cache"] # extra writable dirs

With mode = "fs", bash children can read everything but write only the cwd, /tmp, XDG state/cache, and any allow_write entries, enforced by bubblewrap, firejail, or in-kernel landlock (whichever is available). The sandbox fails closed: if no mechanism can be applied, the run is refused.

Sandbox expansion: when a sandboxed command deterministically needs more than the policy allows (a redirection outside the write allowlist, a network-touching program under a network-denying backend, env assignments an env-clearing backend would strip), ka computes the exact missing grants before running and asks once: allow (this run), always (write paths persist to [sandbox] allow_write; network and env grants are session-scoped), or deny (the sandbox stays unchanged; the command fails on its own). Grants come from command analysis, never from failure output, and writes beneath broad directories (/, $HOME, /etc) are never offered. Nothing expands without an explicit ask.

[guards]
spend_usd = 2.0 # ask before the session's total cost crosses $2.00
context_pct = 90 # ask when the context meter first passes 90%

Each guard latches once per session; answering stop aborts the current turn cleanly.

One grammar decides what the model can use: set at startup (flags, [features]) or flipped mid-session (/features, /sandbox). Every change is recorded on the strand and restored on resume, exactly like model and mode.

agents | skills | mcp | hooks | web | lsp | debug # whole features
mcp:<server> · skill:<name> · tool:<hand> · agent:<name> # single items

Disabling is fail-closed: the capability vanishes from the model’s tool list and a stray call is rejected naming the toggle, so it cannot be reached even by guessing the name. Enabling restores it: an MCP server disabled at startup spawns on the spot, and so do the LSP and debug tiers when configured. Toggles land between turns: sent mid-turn, the hide applies to the next model round-trip inside that turn, and the side effects (spawn, strand record, inventory) settle with it. Disabling a tier never weakens the permission tiers; it only removes capabilities. Precedence: flags → resumed strand → [features] → defaults.

Terminal window
ka --disable agents,mcp:github # no subagents, one server off
ka --disable tool:bash # a session that cannot run commands
ka --enable mcp:github # re-enable over a [features] disable

In the TUI, /features opens the panel (↑↓ choose, Enter toggles, s saves the current set as your user-layer default), /features on|off <spec> toggles directly, and /sandbox off|fs switches the bash sandbox live: the fs policy is recomputed from [sandbox] allow_write, so unpersisted session grants drop on off→fs. The status bar shows ⊘ N while N specs are disabled.

ka --safe-mode (or KA_SAFEMODE=1) disables all customizations (AGENTS.md, MEMORY.md, skills, agents, commands, hooks, MCP, LSP), keeping built-ins, config, and auth. It outranks every feature toggle: nothing it disables can be re-enabled until restart. If something misbehaves, safe mode answers whether the culprit is your extensions or ka itself.