# 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.

The mental model behind kin's permission gate. The day-to-day keys + cycling live in [Modes & permissions](/docs/kin/guide/modes-and-permissions/); this page is the conceptual layer underneath.

**Adding a new kind?** See [Extending kin — tools, kinds, and profiles](/docs/kin/internals/extending-tools/#adding-a-permission-kind) — both mode-policy dicts + the `_planning_freezes` tuple must change together. An omission fails closed to ask, but still changes the intended UX.

<!-- SOURCE: src/kin/harness/permissions.py, src/kin/harness/modes.py, src/kin/harness/session/, src/kin/harness/approvals.py, src/kin/harness/loop.py, src/kin/harness/git_trust.py, src/kin/harness/github_connection.py, src/kin/harness/tools/registry.py, src/kin/harness/tools/git_tool.py, src/kin/harness/tools/github_tool.py, src/kin/tui/app.py -->

## 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`](/docs/kin/guide/tools/#git-and-github) resolve it
from the concrete operation. The full table lives in
[Modes & permissions](/docs/kin/guide/modes-and-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](#planning-is-a-freeze-not-a-mode), `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 decision** — `auto` 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](/docs/kin/concepts/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](/docs/kin/concepts/agents/#the-agent-panel-ctrlo) (++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](/docs/kin/guide/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](/docs/kin/guide/modes-and-permissions/#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](/docs/kin/guide/auto-mode-and-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.
