Skip to content

Permissions — concepts

The mental model behind kin's permission gate. The day-to-day keys + cycling live in Modes & permissions; this page is the conceptual layer underneath.

Adding a new kind? See Extending kin — tools, kinds, and profiles — both mode-policy dicts + the _planning_freezes tuple must change together or the new kind silently fails open.

Decisions are a property of the tool

Every tool declares a risk kindread, edit, shell, meta, mcp, network, browser, outpost — and the active mode maps that kind to a decision. The full table lives in Modes & permissions. Three kinds share one lighter-touch posture (ALLOW in auto, ASK in strict, DENY while planning) because each is a real network call but not an arbitrary-remote-code risk like MCP: network is the git-fetch-only egress (see git network tools), browser is the headless-browser tool (see Browser), and outpost is the read-only outpost tool against the Outpost v1 API (see Outpost). The rule of thumb: reads and metas never prompt; edits and shell gate on the mode; MCP always prompts except for read-only adapted tools; network/browser/outpost are ALLOW in auto, ASK in strict.

The decision chain

loop._decision_for(tool, arguments) resolves a tool call to one of five decisions:

Decision Meaning
ALLOW Run, no prompt
ASK Park the turn on the approval modal
DENY Soft-refuse (return a refusal result the model reads and adapts to)
AUTO Shell only — the command classifier proves it read-only, else fall back to ASK
SANDBOX Shell only, in auto mode — run under the OS sandbox

The chain looks like this:

  1. Planning freeze — if the session is in the planning freeze, edit / shell / mcp / network / browser / outpost are DENY'd as an overlay, before the mode is even consulted, plus a handful of state-mutating meta tools by name (propose_settings, so the model can't rewrite settings behind the freeze). Reads and the rest of meta (including write_plan/present_plan, the way out of the freeze) still run. (network is git-fetch only — see modes-and-permissions.md; it's denied during planning because fetch writes .git/objects + remote-tracking refs, even though it doesn't touch the working tree. browser is denied because a page action — a form submit, a button click — is a real, unfreezable-after-the-fact side effect on a remote site.)
  2. Mode decisionauto allows edits and maps shell to SANDBOX; strict asks for edits and maps shell to AUTO. Both ask for mcp. SANDBOX resolves to ALLOW when a sandbox is available, else falls back to the allowlist; AUTO resolves to ALLOW/ASK via the command classifier.
  3. Auto protected-write — in auto, an edit whose target would change how the agent/tooling runs (.git/, lockfiles, .mcp.json, a shell rc file, …) is downgraded ALLOW → ASK.
  4. Session allow — a previously-chosen "always allow this session" signature short-circuits ASK → ALLOW. Never relaxes a DENY (so the planning freeze stays read-only).
  5. MCP trust — an MCP server you approved once is trusted for the session; a changed launch config re-prompts. A read-only MCP tool is adapted to read so it never prompts.

Spawning a subagent is free — the runtime gate is the safety net

task (and its background variant, task(run_in_background=true)) is kind=meta, so the spawn itself never prompts, foreground or background — there is no upfront "here's the worst-case surface, approve the whole subagent" gate. See Agents for the full spawn/lifecycle surface.

The safety property is downstream, not upfront:

  • The subagent inherits the parent's mode, so any destructive call inside it still goes through the normal decision_for chain — an auto-mode parent's coder child still gets its shell sandboxed, and a strict-mode parent's child still asks before an edit.
  • agent_kill (the tool, or the agent panel's x) is the human-only escape if a subagent goes off the rails — it cancels a running child, foreground or background, without cancelling the parent turn.
  • Visibility is the other half of the contract: a background child shows up immediately in the ambient strip below the composer and in the agent panel (Ctrl+O), so you can see what's running and kill it before it does something you don't want — rather than a one-time approval at dispatch time.

Tri-state answers

When a prompt asks, the approval modal returns one of three answers:

Answer Effect
once Allow this one call
always Allow this call and remember its signature for the rest of the session
deny Refuse this call; the model reads the refusal and adapts

"Always" remembers a signature, not the literal call. A handful of tools that carry a command- or url-shaped argument get a narrower signature than "the tool name" — otherwise one approved call would silently green-light a differently-targeted sibling (a different remote, a different host, a different site):

  • Shell — the command head, but only for a single, no-red-flag command (command_safety.is_single_safe_segment); a compound command is scoped to its exact string so npm can never green-light npm …; rm -rf x.
  • ssh — scoped by user@host:port and the exact command, so "always allow" on one remote command never bleeds to a different command on the same host, or the same command on a different host.
  • git-push / git-pull / git-fetch — scoped by remote + branch + the tool's one risky flag (force / rebase / all), so approving one push doesn't auto-allow a different remote, branch, or a later call with the risky flag flipped on.
  • browser — a navigate grant is scoped by the target's origin (scheme+host+port), so "always browse site A" never bleeds to site B; every other action (click/fill/press/…) is scoped by the action verb, since it acts on whatever page is already open and a grant on one action must not bleed to navigate.
  • MCP tools — scoped by tool name only, but namespaced (mcp-tool:<name>) so a standing shell/browser grant can never cross into an MCP call sharing an argument name (an MCP server's schema is server-controlled and can't be trusted to disambiguate itself).
  • Every other tool — the tool name.

The grant only ever turns ASK → ALLOW; it never relaxes a DENY. The planning freeze still refuses destructive work after an "always," because the DENY is an overlay veto, not a session-allow question.

MCP is special

MCP tool descriptions are attacker-influenceable (an MCP server can rewrite its own description). kin treats every MCP tool — regardless of readOnlyHint — as kind=MCP → ASK in every mode (including auto). The runtime gate is the protection; the description is data, never an instruction.

A read-only MCP tool (the readOnlyHint on the spec) is adapted into a read tool instead, so those do not prompt. A tool that materializes mid-session via tools/list_changed re-prompts even if the server is already trusted, because the new tool's surface has not been seen yet.

See MCP for the trust store + rug-pull defense.

Mode is human-only

The model has no set_mode tool. Mode is a human-only control, so a subagent — even one that fetched an untrusted prompt — cannot self-escalate its own permissions. shift+tab, /mode, and --mode are the only ways in or out. There are exactly two modes — auto (the default) and strict; the older default / accept-edits / plan names still resolve but alias onto the pair.

Planning is a freeze, not a mode

Planning used to be a fourth mode (plan). It is now a transient, gate-enforced freeze layered on top of whichever mode is active (session.planning). While set, edit / shell / mcp / network / browser / outpost are DENY'd as the overlay in step 1 of the chain above; the underlying auto/strict policy is untouched and returns the moment the freeze lifts.

The safety property is an asymmetry: the model may enter the freeze (dispatching the read-only planner subagent, whose write_plan call auto-enters the freeze on its first write — entering a read-only freeze is a capability reduction, so it's non-escalating and safe to self-trigger), but there is no model-reachable way to leave it. The freeze lifts only on a human action — approving in the present_plan modal or a bare /plan toggle. The freeze also propagates to subagents (a mid-plan task(...) inherits it, so it can't edit freely), and it is never persisted — a resumed mid-plan session lands in strict, where edits/shell are ask-gated, so there's no silent capability gain. See Planning for the lifecycle.

What's a "dangerous" tool, really

The mode × kind matrix is the first cut. The next layer is the OS sandbox in auto mode (and the per-mode opt-in flag KIN_SANDBOX_STRICT / sandbox_strict): a shell that the classifier didn't auto-allow still runs in auto, but inside a Seatbelt / bwrap cage that confines writes to the workspace and turns off network. The classifier gates authorized-vs-unauthorized within scope; the sandbox bounds blast radius when the classifier is wrong. (Inside a disposable, isolated container the cage is redundant and unworkable — KIN_SANDBOX=container trusts that external boundary instead, running auto-mode shell unconfined; see Trust the container.) A third cut is the protected-write denylist: in auto, an edit to a path that reconfigures the agent or its supply chain (.git/, lockfiles, .mcp.json, shell rc files, …) is downgraded back to ask. See Auto mode & the OS sandbox.

Revoking grants

/grants opens a modal listing every "always allow" signature this session has recorded — both tool/signature grants and MCP server trusts. Inline revoke clears the session grant; for an MCP-server grant it also clears the persistent on-disk trust store, so the next launch re-prompts.