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 kind — read, 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:
- Planning freeze — if the session is in the planning freeze,
edit/shell/mcp/network/browser/outpostareDENY'd as an overlay, before the mode is even consulted, plus a handful of state-mutatingmetatools by name (propose_settings, so the model can't rewrite settings behind the freeze). Reads and the rest ofmeta(includingwrite_plan/present_plan, the way out of the freeze) still run. (networkisgit-fetchonly — seemodes-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.browseris denied because a page action — a form submit, a button click — is a real, unfreezable-after-the-fact side effect on a remote site.) - Mode decision —
autoallows edits and maps shell toSANDBOX;strictasks for edits and maps shell toAUTO. Both ask formcp.SANDBOXresolves toALLOWwhen a sandbox is available, else falls back to the allowlist;AUTOresolves toALLOW/ASKvia the command classifier. - 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 downgradedALLOW → ASK. - Session allow — a previously-chosen "always allow this session" signature short-circuits
ASK → ALLOW. Never relaxes aDENY(so the planning freeze stays read-only). - 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
readso 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_forchain — anauto-mode parent'scoderchild still gets its shell sandboxed, and astrict-mode parent's child still asks before an edit. agent_kill(the tool, or the agent panel'sx) 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 sonpmcan never green-lightnpm …; rm -rf x. ssh— scoped byuser@host:portand 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— anavigategrant 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 tonavigate.- 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.