How Shiin works
Note
Design document: draft. Authorization, approval, evidence, and early client/integration code exist in source; host, platform, and milestone validation remain incomplete. The specifications are the precise source for everything summarized here.
This page follows one action from the moment an agent proposes it to the moment it is recorded, then walks through four concrete examples.
The path of one action
- The agent proposes an action. The agent, running inside a host such as Claude Code, decides to run a command, edit a file, or call a tool.
- An adapter intercepts it. Before the host executes the action, the host hands it to a Shiin adapter, for example through a hook.
- The adapter normalizes it. The adapter turns the host’s payload into an action request: which agent is asking, what action type it is, what resources it touches, what its effects are, and which task is active. Anything the adapter cannot classify is marked opaque.
- The engine evaluates it. The engine takes the action request and a policy snapshot and runs five stages in a fixed order.
- A decision comes back.
The decision is
allow,deny, orpending, with a reason code and a message for the agent. - The adapter enforces it in the host’s own protocol.
For a hook, that means returning the host’s allow or deny response.
For
pending, it means asking an approver and waiting, or using the host’s native prompt, or denying with a ticket when the host cannot wait. - An audit event is written to the audit log before a non-read action is allowed to proceed.
The diagram shows the Unix daemon approval path available in source. Host integration and cross-platform validation remain open.
sequenceDiagram
participant Host as Host and agent
participant Adapter
participant Engine
participant Approver
participant Audit as Audit log
Host->>Adapter: proposed action (host payload)
Adapter->>Engine: action request
Engine->>Engine: identity, capability, policy, intent, risk
Engine->>Audit: request received, decision made
Engine-->>Adapter: pending
Adapter->>Approver: approval request
Approver-->>Adapter: resolution (allow once)
Adapter->>Audit: approval resolved, grant used
Adapter-->>Host: allow
The five stages
Stages run in this order. Later stages can only make the result stricter.
| Stage | Question it answers | Can return |
|---|---|---|
| Identity | Do we know which agent is asking, and how sure are we? | deny, pending, or pass |
| Capability | May this agent attempt this class of action at all? | deny, pending, or pass |
| Policy | What do the rules say about this action on these resources? | allow, deny, or pending |
| Intent | Does the action fit the constraints of the active task? | deny, pending, or pass |
| Risk | Does the action have a risk signal that needs escalation? | deny, pending, or pass |
Identity, capability, intent, and risk are gates: they can stop or hold an action but never allow it.
Only a policy rule, or a grant created by an approver, can produce allow.
The results combine in a fixed way, specified in Evaluation:
- If any stage returns
deny, the decision isdeny. - Otherwise, if any stage returns
pending, the decision ispending, unless a matching grant already covers it. - Otherwise, if a policy rule allows the action, the decision is
allow. - Otherwise, the decision is the default decision, which is
deny.
Four examples
The examples use this policy, written in the TOML format specified in Policy syntax. The syntax shown here is illustrative until that specification is accepted.
# .shiin/policy.toml
version = 1
[capabilities.workspace]
id = "workspace"
allow = true
action_types = ["fs.*", "shell.exec"]
[defaults]
decision = "deny"
[[rule]]
id = "edit-source"
effect = "allow"
action_types = ["fs.read", "fs.write", "fs.create"]
resources = ["./src/**", "./tests/**"]
[[rule]]
id = "no-secrets"
effect = "deny"
action_types = ["fs.read"]
resources = ["**/.env", "**/.env.*", "~/.ssh/**"]
reason = "Secret files are not readable by agents."
[[rule]]
id = "run-tests"
effect = "allow"
action_types = ["shell.exec"]
commands = ["cargo test", "cargo nextest run"]
[risk]
opaque_command = "pending"
1. An allowed edit
The agent edits src/login.rs inside the workspace.
- The adapter builds an action request with action type
fs.writeand one effect: an exact write to/home/dev/project/src/login.rs. - Identity and capability pass. The
edit-sourcerule allows the write. - No task is active, and no risk signal applies.
- The decision is
allow. An audit event is written, and the host proceeds.
2. A denied read of a secret
The agent tries to read .env to “check the configuration”.
- The action request has action type
fs.readon/home/dev/project/.env. - The
no-secretsrule matches and returnsdeny. Theedit-sourcerule does not matter, becausedenywins. - The decision is
denywith reason codedeny.policy. - The agent receives a fixed denial message; the operator can inspect the reason code and trace.
3. A command whose effects are unknown
The agent runs python scripts/cleanup.py.
- The classifier recognizes a Python interpreter running a script. It cannot know what the script will do, so the action request carries an opaque effect.
- No rule allows the command, and the opaque effect raises the
opaque_commandrisk signal. - The decision is
pendingwith reason codepending.opaque. - In v0.1, the adapter uses Claude Code’s native permission prompt. From v0.3, the request goes through the approval protocol, and an approver can allow it once.
This example also shows a limit: once the script is allowed, Shiin does not see what it does. Limitations explains why, and Using Shiin with sandboxes explains how to cover that gap.
4. An action outside the task
The developer declares an experimental task; the following is illustrative task data, not a command-line input format:
# task declaration (illustrative)
description = "Fix the login bug"
scope.paths = ["./src/auth/**", "./tests/auth/**"]
forbid.action_types = ["db.schema_change"]
expires_at = "2026-09-14T18:00:00Z"
The agent then tries to create migrations/0042_add_login_attempts.sql.
- The
edit-sourcerule would not allow this path anyway, but suppose the operator’s policy allowed writes anywhere in the workspace. - The intent stage checks the task’s
scope.pathsconstraint. The path is outside it. - The decision is
denywith reason codedeny.intent, even though standing policy allowed the write.
If the agent instead ran psql -c "ALTER TABLE users ADD COLUMN login_attempts int",
the experimental SQL classifier can identify a db.schema_change effect,
and the forbid.action_types constraint can deny it the same way. The classifier recognizes a conservative subset; unsupported or ambiguous SQL remains opaque, and host adapters do not yet use this classifier.
Where Shiin sits relative to a sandbox
Shiin and a sandbox answer different questions, and they work best together. Shiin decides before an action whether it should happen at all. A sandbox limits what can happen if an allowed action, or code it runs, misbehaves.
flowchart LR
agent["Agent proposes action"] --> shiin{"Shiin decision"}
shiin -->|deny| stop["Action refused"]
shiin -->|allow| sandbox["Action runs inside a sandbox"]
sandbox --> system["Files, network, services"]
What exists today
The engine, policy, hooks, audit log, experimental task constraints, Unix approvals, grants, receipts, replay, a Rust client, and an early MCP proxy exist in source. The identity gate currently passes through; capabilities require explicit action/effect-class grants. SQL classification remains experimental and is not wired into host adapters. The roadmap lists validation still required for each part.
| Part | Milestone |
|---|---|
| Claude Code adapter, engine, policy, audit log | v0.1 (source; host validation open) |
| Task declarations and intent binding | v0.2 (experimental source; measurements open) |
| Approval protocol and grants | v0.3 (Unix source; platform validation open) |
| Receipts and replay | v0.4 (source; external review open) |
| MCP proxy, Rust client, and SQL classifiers | v0.5 (early source; conformance/integration validation open) |