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 Markdown

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. An omission fails closed to ask, but still changes the intended UX.

Decisions are a property of the tool

Every call has a risk kindread, 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:

  1. Dynamic kind — resolve this operation’s actual risk, failing closed to ASK if a tool returns an invalid kind.
  2. Planning freeze — if the session is in the planning freeze, edit / shell / mcp / network / publish / rig / outpost are DENY’d as an overlay, before the mode is consulted, plus a handful of state-mutating meta tools by name. Reads and the rest of meta (including write_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.
  3. Mode decisionauto allows edits and bounded publication and maps shell to SANDBOX; strict asks for both 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.
  4. 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.
  5. Session allow — a matching reusable “always allow this session” signature short-circuits ASK → ALLOW. Never relaxes a DENY; force-with-lease declares itself non-reusable and cannot consume this tier.
  6. 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 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 so npm can never green-light npm …; 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 — 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.

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.