Kin / How Kin works
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.
Read as MarkdownThe 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. An omission fails closed to ask, but still changes the intended UX.
Decisions are a property of the tool
Every call has a risk kind — read, edit, shell, meta, mcp,
network, rig, outpost, or publish — and the active mode maps that kind
to a decision. Most tools declare one static kind; bounded multi-operation
tools such as git and github resolve it
from the concrete operation. The full table lives in
Modes & permissions.
Four kinds share a lighter-touch posture (ALLOW in auto, ASK in strict,
DENY while planning) because Kin owns a bounded boundary for them:
network covers structured Git sync and GitHub reads, publish covers normal
same-name push and PR open, rig is the persistent semantic computer, and
outpost is the read side of the Outpost API. Arbitrary MCP calls still ask in
both modes. 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;
bounded network/publish/Rig/Outpost calls allow in auto and 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:
- Dynamic kind — resolve this operation’s actual risk, failing closed to
ASKif a tool returns an invalid kind. - Planning freeze — if the session is in the planning freeze,
edit/shell/mcp/network/publish/rig/outpostareDENY’d as an overlay, before the mode is consulted, plus a handful of state-mutatingmetatools by name. Reads and the rest ofmeta(includingwrite_plan/present_plan, the way out of the freeze) still run. Every structured Git push is explicitly caught, including force-with-lease’s pinned-ask variant. - Mode decision —
autoallows edits and bounded publication and maps shell toSANDBOX;strictasks for both 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 matching reusable “always allow this session” signature short-circuits
ASK → ALLOW. Never relaxes aDENY; force-with-lease declares itself non-reusable and cannot consume this tier. - 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 normally 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 |
Fresh-state actions may omit always; force-with-lease does so and requires a
new review for every attempt. “Always” remembers a signature, not the
literal call:
- 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. - Ordinary
git/github— repository + operation + remote + branch, so a strict-mode grant cannot bleed to a different target. Force never reuses it. - Beam
git-push/git-pull/git-fetch— remote + branch + the tool’s one risky flag, disjoint from ordinary Git. 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.
Git credential use is a second, separate decision after the operation gate.
Its modal offers Once, Session, Trust host, or Decline over the resolved
SSH/HTTPS route. Trust host persists only a secret-free route fingerprint in
global settings; it is not an operation grant and cannot authorize a push by
itself. /grants lists and revokes both layers.
Missing GitHub login has one additional human-only round trip. A recognized
github.com operation may show Connect GitHub, run gh’s fixed web login,
and resume the parked call. Accepting it also grants the resulting exact
account/binary HTTPS route for that session, avoiding a second modal for the
same handoff. It does not approve a different Git operation: the operation’s
network / publish decision has already been evaluated independently.
Headless and child sessions cannot resolve this gate themselves.
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 / publish / rig / outpost are DENY’d as the overlay in step 2 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 strict’s opt-in): a shell that the classifier did not
auto-allow still runs in a Seatbelt / bwrap cage that confines writes to the
workspace, scrubs secrets, and follows the global network posture (available
by default, isolatable with sandbox_network=false). Structured Git gets only
its separately approved credential carve-out. Inside a disposable container,
KIN_SANDBOX=container trusts the external boundary instead. A third cut is
the protected-write denylist. See
Auto mode & the OS sandbox.
Revoking grants
/grants lists ordinary approval signatures, MCP trust, sandbox authority,
and session/persistent Git host-route trust. Inline revoke clears the matching
session or persistent record; revoking a host route makes the next remote Git
call ask again.