Modes & permissions¶
How kin decides whether a tool call runs, asks, or is refused — and how you steer that with modes and per-session grants.
Permission is a property of the tool, not just the mode. Every tool declares a risk kind; the active mode maps each kind to a decision. Anything that is not an edit or a command is non-destructive and never prompts.
The two modes¶
There are two cyclable modes, and Shift+Tab toggles between them:
| Mode | What it allows |
|---|---|
auto |
The default. Safe autonomy: file edits run without asking (confined to the workspace), and shell runs under an OS sandbox instead of prompting. MCP tools still ask. See Auto mode & the OS sandbox. |
strict |
The review posture: read and search freely; stop and ask before a file edit or an MCP tool; auto-allow provably read-only shell, ask for the rest. |
auto is the launch default and the more autonomous tier; strict is the calm, ask-first posture. The cycle is a 2-way toggle, auto ↔ strict.
Planning is no longer a mode + legacy aliases
Earlier versions had accept-edits and plan modes and a four-way cycle. Those are gone: accept-edits folded into auto, and planning became a transient freeze you enter by dispatching a planner subagent (or with /plan) rather than a mode you cycle to (see Planning). The legacy names are still accepted on the CLI, in settings.toml, and in a resumed journal — they alias onto the new pair (default → strict, accept-edits → auto, plan → strict) with a deprecation note. This is the canonical home for the legacy alias map — every other doc links here.
Tools × decisions¶
Each mode maps a tool's kind to one of: allow (run, no prompt), ask (park on an approval modal), deny (soft-refuse with a result the model reads and moves on), auto (shell only: classify the command — provably read-only runs, else ask), or sandbox (shell only, in auto: run under the OS sandbox).
| Tool kind | Examples | auto |
strict |
|---|---|---|---|
read |
read_file, grep, glob, ls, web_* |
allow | allow |
meta |
todos, ask, task, skill, shell_list/output/kill |
allow | allow |
edit |
write_file, edit_file, edit_notebook |
allow | ask |
shell |
shell, run_code |
sandbox | auto |
mcp |
external MCP tools, ssh, git-push, git-pull, outpost-send |
ask | ask |
network |
git-fetch |
allow | ask |
browser |
browser |
allow | ask |
outpost |
outpost |
allow | ask |
MCP tools are arbitrary remote code, so they ask in both modes — they are never auto-allowed, not even in auto. (A read-only MCP tool from an approved server is adapted as a read tool instead, so those do not prompt.) See MCP. The opt-in ssh tool, the always-on git-push/git-pull tools, and the mutating outpost-send tool reuse the mcp kind for the same reason — a remote mutation (a deploy, a push, submitting/cancelling a job, answering an inbox item) always asks and is denied while planning.
git-fetch gets the lighter network kind instead of mcp: it doesn't mutate the working tree or push anything out, so it auto-allows in auto (still asks in strict — a network call is never auto-allowed there, same posture as mcp). It's still denied while planning like the other three, though — fetch writes .git/objects and updates remote-tracking refs, a real side effect the freeze must still catch even though the mode itself would otherwise let it run silently.
The browser tool gets its own browser kind with the same allow-in-auto / ask-in-strict split. It's a separate kind from network (which is documented as git-fetch-only) because a browser session — remote JavaScript, attacker-controlled pages — is a different risk class whose gate must be tunable independently. The lighter auto posture is sound because the tool's own boundary does the heavy lifting: only http/https URLs are accepted, every top-level navigation is SSRF-checked before the browser sees it, and downloads / dialogs / popups are refused automatically. Like network, it is denied while planning — a browser action can submit a form, a side effect the read-only freeze must catch.
The read-only outpost tool similarly gets its own outpost kind rather than reusing network: it's a read from the model's perspective (list jobs, fetch a report, read the inbox) against a bearer-authed, operator-owned Outpost endpoint, so it gets the same lighter network/browser-style allow-in-auto posture — but the transport (remote HTTP to a service you run, not a local git subprocess) is a distinct enough risk class to warrant its own dedicated kind rather than stretching network's "git-fetch only" definition. It's denied while planning too — a plan should be built from the current workspace, not a side-channel query against a remote scheduler. The mutating half, outpost-send, is a separate tool with kind mcp (see above) — submitting, cancelling, or answering always asks.
While the planning freeze is active, edit / shell / mcp / network / browser / outpost are denied as an overlay on top of whichever mode is active — the underlying auto/strict policy is untouched and returns the moment you approve a plan or leave the freeze.
Auto edits to protected paths still ask
Even in auto, an edit that targets a path which changes how the agent or its tooling runs — anything under .git/, a lockfile (uv.lock, package-lock.json, …), .npmrc / .yarnrc, .mcp.json, .claude.json, a shell rc file, .pre-commit-config.yaml, pyrightconfig.json, ~/.ssh/config, or .kin/settings.toml itself — is downgraded from auto-allow to ask, so it can't silently rewrite repo state, the dependency supply chain, or the harness config. See Auto mode & the OS sandbox.
The shell allowlist¶
In strict mode, the shell kind maps to auto: a command is auto-allowed only when it is provably read-only, and otherwise falls back to the approval modal. (The run_code interpreter shares the shell kind but carries no command string to classify, so it always asks in strict — arbitrary code is never provably safe — and runs sandboxed in auto.) The classifier is conservative — it only ever downgrades ask to allow for commands it can prove safe, and anything uncertain stays ask. (In auto mode, shell maps to sandbox instead — the classifier still runs, but a command it can't prove safe runs contained rather than prompting.)
It works in two tiers:
- Always-safe commands — read-only regardless of flags (
ls,cat,grep,pwd,wc,diff, the*sumhashers, and more). - Conditional commands — safe only when a per-command checker passes (
gitread-only subcommands likestatus/log/diff,findwithout-exec/-delete, and similar).
A command with command/process substitution, output redirection, or a leading VAR=value assignment is never auto-allowed. Set KIN_SHELL_ALLOWLIST=0 to turn the allowlist off and send every shell command through the approval modal.
Note
The allowlist downgrades a prompt to a run; it is not the security boundary. Containment for un-vetted commands comes from the OS sandbox in auto mode — see Auto mode & the OS sandbox.
Tri-state approvals¶
When a tool call asks, the approval modal offers three answers:
| Answer | Effect |
|---|---|
Allow once (a, or bare Enter — it's the focused default) |
Allow this one call. |
Always (s) |
Allow this call and remember it for the rest of the session — the button label names exactly what it will cover (e.g. Always: \npm` commands`). |
Deny (d / Esc) |
Refuse this call; the model reads the refusal and adapts. |
Deny with a note (n) |
Refuse and steer: type one line ("use pnpm instead") and enter — the note rides back to the model as the denial reason, so it adapts without a follow-up message. Esc closes the note input first; a second esc denies plain. |
The modal shows the call in its native form per risk kind: a file edit
renders the actual unified diff it would apply (computed with the same match
path the tool runs — what you approve is what happens); a shell command
renders syntax-highlighted with its timeout/background flags as dim metadata;
git-network and MCP calls render labeled argument rows (with a note that MCP
argument schemas come from the external server); anything else falls back to
the raw argument dump. The title names the kind (file edit · src/x.py,
shell command, MCP tool · server › name) and carries an amber accent only
for the riskier kinds (shell / MCP / network / browser) — a plain file edit
reads calm. A bare edit_file/write_file "always" covers every future
edit this session, so those prompts carry an explicit "prefer allow once"
note.
"Always" remembers a signature, not the literal call. A shell command groups by its command head — approving npm once auto-allows later npm … calls — but only for a single command with no substitution or redirect, so approving one head can never green-light a different command hidden behind a ; or &&. A compound command is scoped to that exact command string instead. An ssh grant is scoped to the exact user@host:port and the remote command, so "always allow" never bleeds to another host, to a different remote command on the same host (it runs outside the sandbox, so an approved systemctl restart must not green-light a later rm -rf), nor to the local shell tool. A git-push/git-pull/git-fetch grant is scoped to remote + branch + that tool's one risky flag (force / rebase / all), so approving a push to origin/main never covers a push to a different branch, a different remote, or the same branch with force=true. A browser navigate grant is scoped to the target origin (scheme + host + port) — "always allow browsing site A" never covers site B — and every other browser action is scoped to its action verb, so a standing grant on click never covers navigate. Every other non-shell tool groups by tool name. The grant only ever turns an ask into an allow; it never relaxes a deny, so the planning freeze still refuses edits and shell.
Cycling modes¶
The mode is a human-only control — the model cannot call set_mode, so it can never self-escalate its own permissions. You change it three ways:
- Shift+Tab toggles between
autoandstrict. /modewith no argument toggles the same 2-way cycle./mode <name>sets a specific mode (autoorstrict; the legacydefault/accept-edits/planare still accepted — see legacy-mode-aliases).
The cycle is a 2-way toggle, auto ↔ strict. See Keybindings for the full key map.
Planning¶
Planning is a transient, read-only freeze on the session — not a mode. While it is active, edits, shell, and MCP tools are refused (an overlay on top of auto/strict); reads, search, and todos still run, so the model can research and write a plan but cannot act.
There is a deliberate asymmetry: the model may enter the freeze, but only you can leave it.
- The model enters planning by dispatching a read-only
plannersubagent (task(subagent_type='planner', ...)) when a task needs design before action; the subagent researches read-only and drafts the plan withwrite_plan, whose first call auto-enters the freeze on the top-level session — entering a read-only freeze only reduces capability, so it is safe to self-trigger this way. - You enter or leave it with
/plan:/plan <task>enters the freeze and sends<task>; bare/plantoggles the freeze (your escape hatch — Shift+Tab no longer controls planning). - The model presents a finished plan with the
present_plantool, which raises an approval modal. Only your choice there (or a bare/plan) lifts the freeze.
A ⏸ planning badge sits in the status bar's left activity group (the mode badge is on the right) while the freeze is active. The freeze propagates to subagents, so a mid-plan task(...) stays read-only too. It is never persisted — a resumed session lands in strict (edits/shell ask-gated), and the unfinished plan survives as its file under .kin/plans/.
When you approve a plan, the modal offers three actions:
| Action | Effect |
|---|---|
| Approve & execute | Lift the freeze and carry out the plan with the current (primed) context. The default. |
| Approve, clear & re-inject | Drop the planning chatter and reseed history with just the plan plus an execution preamble — best for a large task where the research exploration would otherwise crowd the context. Plans are written for exactly this: the planner holds them to a self-containment bar (exact files, anchors, constraints, verification) so the plan can serve as the executor's only context. |
| Review plan first (adversarial) | Tells the model to dispatch a read-only critic agent itself (via task) that stress-tests the plan; it applies fixes with write_plan and re-presents. Stays frozen. |
Pressing Esc keeps planning. Every write_plan call saves the plan to <workdir>/.kin/plans/<date>-NN-<slug>.md unconditionally — the file is the hand-off between the planner subagent and the top-level agent — so it survives across sessions; see Configuration.
Model-writable settings¶
The model can help tune your settings mid-conversation through the read_settings / propose_settings tools — turn the guidance on with /settings. Every change still goes through a human diff-approval modal (the same gate as /edit-settings), and a hard boundary limits what the model can touch. See the settings.toml reference for what each key does:
| Tier | Keys | The model can… |
|---|---|---|
| Model-writable | temperature, top_p, top_k, max_tokens, context_window, enable_thinking, effort, thinking_type, cache_ttl, model |
…propose a change (you approve a diff). Applies to new sessions. |
| Human-only | mode, sandbox, sandbox_strict, shell_allowlist (containment); api_key, base_url, brave_api_key (secrets/endpoints); provider, provider_preset, providers, model_favorites (presets); reasoning_effort (cross-wire conflict — use the wire-agnostic effort knob, which the picker translates per-serve; a writable reasoning_effort would let the model bypass the picker and write a value the Anthropic wire won't read) |
…never. The tool refuses these with a banner naming the key — hand-edit ~/.kin/settings.toml. |
The model-writable set is an explicit positive allowlist, not "everything except the secrets" — a knob is denied until it's deliberately added to the list, so a future setting can't silently become model-writable. propose_settings is top-level only (a subagent can't propose), is frozen during planning, and never writes secrets or endpoints (the diff masks every credential and endpoint value — api_key, base_url, and any nested [[providers]] token — so your private endpoint never rides into model context). Changing the live session is still a human-only act: use /mode and /model. read_settings masks all credentials to [SET]/[NOT SET].
Managing grants¶
/grants opens a modal listing this session's "always allow" grants, with inline revoke. For scripted or quick use, /grants revoke <number|key> revokes one directly (and clears the persistent trust store for an MCP-server grant so it is not silently re-trusted next launch). The session "always allow" grants last only for the running session.