Shiin documentation
Note
Project status: pre-alpha. The first experimental release provides CLI and Unix-daemon archives for Linux and macOS. Rust client and MCP integrations remain available from source; see installation and the roadmap for limits and remaining work.
Shiin is an agent-agnostic authorization layer for AI agent actions. It sits between AI agents (Claude Code, Codex CLI, Cursor, MCP clients, and custom agents) and the things they act on: files, commands, networks, and services. For every action an agent proposes, Shiin decides whether to allow it, deny it, or hold it for a human to approve, and it records evidence of that decision.
Shiin’s decisions are made by explicit, deterministic policy, never by a language model. Policies are written once and apply to every agent. Policies can also be bound to the specific task a developer gave the agent, so an action can be refused because it does not belong to that task, even if it would otherwise be allowed.
What exists, what is specified, and what is planned
| State | What it covers |
|---|---|
| Exists today | The local engine, policy, hooks, CLI, audit log, experimental tasks, Unix approvals/grants, signed evidence/replay, Rust client, MCP proxy, Unix command wrapper, schemas, conformance vectors, and documentation tooling |
| Specified | The action request, decision, evaluation, policy model, intent binding, approval protocol, audit events, receipts, adapter contract, and local API |
| Open | Independent review, live-host and external MCP validation, attested agent identity, remote approvals, Windows daemon transport, and release qualification; see the roadmap |
Start here
| If you are | Read, in order |
|---|---|
| New to Shiin | Vision, How Shiin works, Use cases, Limitations |
| Evaluating Shiin’s security | Threat model, Security model, Enforcement levels, Limitations |
| Comparing Shiin with other tools | Landscape, FAQ |
| Building an adapter or integration | Adapter contract, Action request, Decision, Local API |
| Planning to write policy | Policies, Policy model, Writing policies |
| Contributing code | Workspace and crates, Coding standards, Testing strategy, CONTRIBUTING.md |
| Proposing a change | ADR index, RFC process |
Reading the status banners
Every page starts with a banner that says what kind of page it is and how settled its content is.
- Specification pages are normative contracts. Implementations will be tested against them.
- Design document pages explain the intended design and the reasons behind it.
- Design intent: not implemented pages describe what using Shiin will look like. They are written ahead of the code so the experience can be reviewed before it is built. Nothing on those pages works yet.
- Architecture decision pages record one decision and its status.
- Project policy pages set rules for the project and its contributors.
The documentation style guide defines every label and the checks that keep banners accurate.
Giving feedback
The specification phase is the cheapest time to change the design.
- For a problem in a specification, open an issue using the spec feedback form.
- For anything that might be a way to bypass Shiin, do not open a public issue. Follow SECURITY.md instead.
- For larger changes, read the RFC process.
Vision
Note
Design document: draft. Authorization, approvals, evidence, and early client/integration code exist in source; platform, host, and milestone validation remain incomplete.
The problem
AI coding agents act with the authority of the person who runs them. An agent that can edit files can also delete them. An agent that can run tests can run any command. An agent that can fetch documentation can send data to any host the developer’s machine can reach.
Three things go wrong:
- Mistakes. A model misreads a request and deletes the wrong directory, drops the wrong database, or force-pushes over a colleague’s work.
- Manipulation. Text the agent reads (a web page, an issue, a dependency’s README, an MCP tool description) contains instructions the agent follows. This is prompt injection, and no model is reliably immune to it.
- Excessive agency. The agent simply has more functionality, access, and autonomy than the task needs. The OWASP Top 10 for LLM Applications lists this as LLM06:2025 Excessive Agency and recommends implementing authorization in downstream systems rather than relying on a language model to decide whether an action is allowed (verified 2026-09-14).
The standards community has reached the same conclusion. The NIST National Cybersecurity Center of Excellence concept paper Accelerating the Adoption of Software and AI Agent Identity and Authorization (February 2026) identifies agent identification, authorization, auditing, and non-repudiation as open problems (verified 2026-09-14).
Why today’s controls are not enough
Each agent host ships its own controls: Claude Code has permission rules and hooks, Codex CLI has approval policies and a sandbox, Cursor has hooks, and GitHub Copilot’s coding agent has a network firewall. These controls are valuable, and Shiin builds on them, but they share four gaps.
- They are separate. A developer who uses three agents maintains three configurations in three formats, and they drift apart.
- They do not know the task. A host can be told that an agent may run database commands. It cannot be told that, for this task, the schema must not change.
- They leave weak evidence. It is hard to answer, afterward, what an agent tried to do, what was decided, why, and who approved it.
- They overstate or understate their coverage. Hook-based controls are advisory: the host honors them, and they can be disabled. The vendors say so themselves. Codex documentation calls hooks “a useful guardrail, not a complete enforcement boundary” (verified 2026-09-14). Developers need to know how strong a given control is.
Permission is not proof that an action is right
Consider a developer who tells an agent: “Fix the login bug. Do not change the database schema.”
The agent is allowed to run database commands, because fixing bugs sometimes requires them. It decides to add a column. Every existing control sees an authorized action. Only a control that knows the task can see that the action contradicts the instruction.
Vision statement
Every action an AI agent takes on a person’s behalf is authorized by explicit, portable, deterministic policy, bound to the task the person actually gave, enforced as strongly as the environment allows and honestly labeled, and backed by evidence anyone can verify later.
Who Shiin is for
- Before 1.0: individual developers running one or more agents on their own machine, and the maintainers of agents and integrations who want a standard way to ask “may I do this?”
- After 1.0: teams that need shared policy, central evidence, and remote approvals. The early design preserves the constraints those features will need, as described in Deployment models.
How Shiin works, in one picture
An adapter intercepts each action an agent proposes and turns it into a standard action request. The engine evaluates that request against policy and the active task and produces a decision. Pending decisions go to a human, and every decision is recorded.
flowchart LR
agents["Agents<br/>Claude Code, Codex CLI,<br/>Cursor, MCP clients"] --> adapters["Adapters"]
adapters -->|action request| engine["Engine"]
policy["Policy and<br/>task declarations"] --> engine
engine -->|decision| adapters
engine -->|pending| approver["Approver"]
approver -->|resolution| engine
engine --> audit["Audit log"]
How Shiin works walks through this flow step by step.
Principles
Each principle is stated with what it rules out, because a principle that rules nothing out does not guide decisions.
- Agent-agnostic. Policy is written in terms of normalized action types, not host tool names. Rules out: per-host policy formats and rules that silently stop matching when a host renames a tool.
- Deterministic enforcement. The same action request and the same policy snapshot always produce the same decision. Rules out: language-model or machine-learning output as an input to any decision (ADR-0009).
- The agent is untrusted. Anything the agent or a tool server supplies, including explanations, tool annotations, and self-reported identity, can make a decision stricter but never more permissive. Rules out: allowing an action because the agent says it is safe.
- Deny wins, and the default is deny. A
denyfrom any stage overrides everything else. When nothing matches, the answer isdeny. Rules out: “allowed because no rule matched” (ADR-0011). - Fail closed. Errors, timeouts, and unreadable policy produce
deny. Rules out: silently letting actions through when Shiin itself has a problem (ADR-0013). - Intent is structured. Task constraints are explicit and machine-checkable, and a human confirms them. Rules out: enforcing natural-language instructions, or letting a model decide what the task allows (ADR-0020).
- Approval is a protocol. Approval requests, resolutions, and grants have a specified format and lifecycle that any approval channel can implement. Rules out: approvals that exist only as a dialog inside one application.
- Evidence by default. Every decision produces an audit event, and the audit log is tamper-evident. Rules out: decisions that leave no record.
- Local-first and private. Shiin runs on the developer’s machine, needs no service, and collects no telemetry. Rules out: sending action data anywhere without an explicit opt-in (ADR-0007).
- Honest about coverage. Every adapter states its enforcement level and what it cannot see. Rules out: claiming protection that an interception point cannot provide (ADR-0033).
- Complements sandboxes. Shiin decides whether an action should happen. A sandbox limits the damage if it happens anyway. Rules out: presenting Shiin as a replacement for isolation.
What makes Shiin different
Several open-source projects already intercept agent actions and allow, deny, or ask; the landscape credits them. Shiin’s contribution is the combination of four things:
- Task-intent binding. Actions are checked against the constraints of the task the developer confirmed, not only against standing policy.
- An open, agent-neutral contract. The action request, decision, and approval protocol are versioned specifications with schemas and test vectors that other tools can implement.
- Analyzable policy. Policies compile to Cedar, a formally verified authorization language, so they can be tested, explained, and reasoned about (ADR-0014).
- Explicit enforcement levels. Every integration is labeled cooperative, mediated, or OS-enforced, so its protection is never overstated.
Non-goals
| Shiin is not | Use instead |
|---|---|
| A sandbox or isolation boundary | An OS sandbox, container, or virtual machine, or the host’s own sandbox, alongside Shiin |
| A prompt-injection detector | A dedicated scanner. Shiin limits what a manipulated agent can do; it does not detect the manipulation |
| An observability or tracing dashboard | Tracing tools. Shiin’s audit log can be exported to them |
| A blocklist of dangerous commands | Shiin classifies commands into effects and evaluates policy on those effects. It does not match command text against a list |
| A filter for model output or content | Content-safety tooling for the model’s inputs and outputs |
| A replacement for operating-system permissions | Run agents as a less-privileged user where possible; see Hardening |
| Data loss prevention for every channel | An egress proxy with DLP. Shiin cannot see data leaving through an action it allowed |
Success criteria for 1.0
- Determinism. Replaying every decision in the conformance corpus reproduces it exactly.
- Performance. The budgets in Performance budgets are met on every Tier 1 platform.
- Coverage honesty. Every adapter’s enforcement level and blind spots are documented and verified against a named host version.
- Interoperability. At least three hosts are supported, and the conformance suite is published so third parties can test their own adapters.
- Security. An independent security audit is complete and its findings are published, with no unresolved in-scope bypass from the threat model.
- Stability. The v1 specifications and the policy language are frozen under the versioning policy.
Read next
Use cases
Note
Design document: draft. Authorization, approvals, evidence, and early client/integration code exist in source; platform, host, and milestone validation remain incomplete.
This page describes the situations Shiin is designed for.
Each scenario has a stable ID (UC-01 and so on).
The scenarios form the golden set: the test vectors encode them,
and the policy language must be able to express every one of them
(see the policy-model checklist in ADR-0014).
Personas
| Persona | Situation | What they need from Shiin |
|---|---|---|
| Solo developer | Uses two or three agents on a laptop, often with broad permissions to keep work flowing | Protection from costly mistakes without constant prompts |
| Security-minded lead | Responsible for a team adopting agents, needs to explain the risk to others | Clear, reviewable policy and evidence of what agents did |
| Platform engineer | Runs agents in CI or other unattended environments | Deterministic behavior with no human in the loop |
| Agent builder | Maintains an agent, host, or MCP server | A standard way to ask whether an action is authorized |
Scenarios
UC-01: One policy across several agents
Situation. A developer uses Claude Code for feature work and Codex CLI for reviews. Each host has its own permission configuration, and they disagree.
Today’s gap. Two formats, two sets of rules, no shared view.
With Shiin. Both hosts run Shiin adapters that read the same .shiin/policy.toml.
Because policy refers to action types rather than tool names,
a rule such as “agents may not read .env files” applies to both.
Needs. Claude Code and Codex adapters, policy model, action types. Milestone. v0.2.
UC-02: Keep secrets out of reach
Situation. The workspace contains .env files, and the home directory contains SSH keys and cloud credentials.
Today’s gap. Host permission rules can deny reads through the host’s file tools,
but the same secrets can be read through a shell command such as cat .env.
With Shiin. The classifier maps both the file-read tool and cat .env to an fs.read effect on the same canonical path,
so a single deny rule covers both.
Reads through code the agent runs (python -c 'open(".env")') are opaque and produce pending, not allow.
Needs. Path canonicalization, shell classifier, secret_path risk signal.
Milestone. v0.1.
UC-03: Guard against bulk deletion
Situation. An agent “cleans up” by deleting many files at once.
Today’s gap. Hosts either ask about every command, which leads to approval fatigue, or allow commands broadly.
With Shiin. Deletions within normal limits proceed under policy.
When a single action would delete more files than the configured threshold,
the bulk_delete risk signal raises the decision to pending.
Needs. Deletion effects with counts, bulk_delete signal, pending handling.
Milestone. v0.1, using the host’s native prompt for pending.
UC-04: A task-scoped session
Situation. A developer says: “Fix the login bug. Do not change the database schema or any migrations.”
Today’s gap. No host can express constraints that apply only to one task.
With Shiin. The developer confirms a task declaration
that limits writes to the authentication code and forbids db.schema_change.
Actions outside the task are denied with reason code deny.intent, even where standing policy would allow them.
When the task expires, its constraints and any grants tied to it end.
Needs. Intent binding, task lifecycle, counters. Milestone. v0.2 (experimental); SQL classification in v0.5.
UC-05: Approve consequential operations
Situation. Some operations are sometimes right and always consequential:
git push --force, terraform apply, kubectl delete, publishing a package.
Today’s gap. Approvals are dialogs tied to one application, leave little record, and cannot be answered from elsewhere.
With Shiin. Policy marks these operations pending.
An approval request shows the normalized action, not the agent’s description of it.
The approver can allow it once, allow it for a scope and a time, or deny it.
Every step is recorded.
Needs. Approval protocol, grants, destructive_vcs signal, CLI approver.
Milestone. v0.3.
UC-06: Review evidence afterward
Situation. Something went wrong during an agent session, or a lead wants to review what agents did this week.
Today’s gap. Host transcripts show what the model said, not what was authorized and why.
With Shiin. The audit log records every request, decision, approval, and grant.
shiin verify checks that the log’s hash chain is intact.
shiin replay re-evaluates past requests, including against a proposed new policy, to show what would have changed.
Needs. Audit events, receipts, replay. Milestone. v0.1 for the audit log; v0.4 for receipts, verification, and replay.
UC-07: An unattended agent in CI
Situation. An agent runs in a CI job to update dependencies. No human is watching.
Today’s gap. Hosts either prompt, which hangs the job, or run with approvals disabled.
With Shiin. Unattended mode turns every pending into deny, so the job fails fast and explains why.
The policy is tested in CI with shiin policy test against recorded cases before it is used.
Needs. Unattended mode, policy testing, deterministic evaluation. Milestone. v0.2.
UC-08: Govern MCP tools
Situation. An agent uses MCP servers for issue tracking, databases, and cloud APIs. Some servers are third-party.
Today’s gap. Tool annotations such as readOnlyHint come from the server and cannot be trusted.
With Shiin. The MCP proxy sits between the client and each server and maps tool calls to action requests. Annotations can escalate a decision but never relax one.
Needs. MCP proxy for spec revision 2026-07-28, MCP tool mapping. Milestone. v0.5.
UC-09: Shared team policy
Situation. A team wants one policy, central evidence, and approvals routed to the right people.
With Shiin. This is not planned before 1.0. The design keeps the constraints a team server will need, such as signed policy bundles and transport-independent APIs; see Deployment models.
Milestone. After 1.0.
When Shiin is the wrong tool
| Goal | Better fit |
|---|---|
| Run untrusted code safely | An OS sandbox, container, or virtual machine |
| Stop data leaving through channels that are allowed | Egress controls or a DLP proxy |
| Detect prompt injection in content | A prompt-injection scanner |
| Prove compliance with a regulation | Shiin can supply evidence, but it makes no compliance claims |
Read next
Landscape
Note
Design document: draft. Shiin has a pre-alpha source implementation. Third-party comparisons remain the dated source snapshot stated below. Every statement about another project links its source and gives the date it was verified.
Summary
Many tools already help people control what AI agents do. They intercept actions at different points, enforce decisions with different strength, and leave different evidence. Shiin is designed as a local, agent-agnostic authorization layer for agent actions. It is not a sandbox, a network firewall, or a gateway, and it is designed to work alongside all three.
Deciding whether an agent’s action proceeds, is refused, or waits for a person is not a new idea. Several open-source projects already do it, and Open-source agent action gates credits them with sources. Shiin’s case rests on the four differentiators described in the vision: task-intent binding, an open agent-neutral contract, analyzable policy, and explicit enforcement levels. Shiin’s first experimental CLI and Unix-daemon release is available for Linux and macOS; client and MCP integrations remain source-only. If you need a working tool today, evaluate the projects on this page.
How to read this page
- Facts about other projects come from their own documentation or repositories, linked beside each statement, as checked on 2026-09-14.
- A table cell that reads “not documented” means the sources checked for this page did not establish the fact. It does not mean the project lacks the capability.
- Each project is described in its own terms.
Where a project’s decision labels differ from Shiin’s
allow,deny, andpending, they are quoted as that project’s labels. - Statements about Shiin describe its design, not shipped features.
Standards that frame the problem
Shiin’s problem statement draws on three public documents, which are also useful when comparing any tool on this page:
- OWASP LLM06:2025 Excessive Agency (verified 2026-09-14).
- OWASP Top 10 for Agentic Applications 2026 (verified 2026-09-14).
- The NIST National Cybersecurity Center of Excellence concept paper on software and AI agent identity and authorization, published in February 2026 (verified 2026-09-14).
Categories
Tools in this space fall into six broad categories, and a single tool can span more than one. The categories act at different points and answer different questions, which is why they combine well.
| Category | Where it acts | Question it answers |
|---|---|---|
| Host-native controls | Inside the host, before a tool runs | What does this host let the agent do? |
| OS sandboxes and isolation | Around a running process | What can this process reach, whatever it does? |
| Network egress controls | On outbound traffic | Where may traffic go? |
| MCP and agent gateways | Between an agent and the tool servers or other agents it calls | Which tools and agents may be called? |
| Policy engines as building blocks | Inside other tools | How are rules written and evaluated? |
| Open-source agent action gates | Where an agent proposes an action | Should this action proceed, be refused, or wait for a person? |
Host-native controls
The applications that run agents ship their own controls, and they are the first thing to configure.
- Claude Code has permission rules, and
PreToolUseandPermissionRequesthooks that can return allow, deny, or ask decisions (hooks, permissions, verified 2026-09-14). Hooks can be disabled with--bareor thedisableAllHookssetting, and managed hooks cannot be disabled (hooks, verified 2026-09-14). - OpenAI Codex CLI has approval policies and sandbox modes (agent approvals and security, verified 2026-09-14).
Its
PreToolUsehooks cover Bash,apply_patch, and MCP tools (hooks, verified 2026-09-14). Its documentation calls hooks “a useful guardrail, not a complete enforcement boundary” (hooks, verified 2026-09-14). - Cursor has offered hooks since version 1.7, including
beforeShellExecution,beforeMCPExecution,beforeReadFile, andpreToolUse, with allow, deny, or ask responses and afailClosedoption (hooks, verified 2026-09-14).
How Shiin relates. Shiin’s enforcement level L1 adapters are designed to use these same hook mechanisms, so they inherit the hooks’ strengths and limits. Shiin is designed to add to host controls, not to replace them, so keep them enabled. Enforcement levels records what each adapter can and cannot see.
OS sandboxes and isolation
A sandbox limits what a process can do after it starts, whatever the reason it was started. Containers and virtual machines are the general-purpose form, and some hosts include a sandbox of their own:
- Claude Code’s sandbox applies to Bash commands and their child processes (sandboxing, verified 2026-09-14).
- Codex CLI’s sandbox modes use Seatbelt on macOS, and bwrap and seccomp on Linux (agent approvals and security, verified 2026-09-14).
How Shiin relates. Shiin decides whether an action should happen; a sandbox limits the damage if an allowed action, or the code it runs, misbehaves. Shiin cannot see what a program does after it is allowed to run, as Limitations explains, so a sandbox covers a gap that Shiin leaves. OS-enforced integration (L3) is a future enforcement level. Using Shiin with sandboxes describes the intended layering.
Network egress controls
Egress controls restrict where traffic can go, regardless of which action produced it.
- The GitHub Copilot coding agent firewall restricts outbound network access for processes started by the agent’s Bash tool (customize the agent firewall, verified 2026-09-14). It does not apply to MCP servers or setup steps, and its documentation says it “should not be considered a comprehensive security solution” (customize the agent firewall, verified 2026-09-14).
- Coder Agent Firewall, formerly called Agent Boundaries, is a process-level network firewall that restricts domains and HTTP methods and keeps audit logs (agent firewall, verified 2026-09-14). It covers network egress only (agent firewall, verified 2026-09-14).
- pipelock is an agent firewall for HTTP, MCP, A2A, and WebSocket traffic, with DLP, SSRF, prompt-injection, and tool-poisoning detection and signed action receipts (luckyPipewrench/pipelock, verified 2026-09-14). It is written in Go, with an Apache-2.0 core and Elastic-2.0 enterprise features (luckyPipewrench/pipelock, verified 2026-09-14).
How Shiin relates. Shiin’s design can deny a network action it can see, but it does not inspect traffic content, and it cannot see data leaving through an action it has allowed. Egress controls are the right layer for that, and Shiin is designed to run alongside them.
MCP and agent gateways
Gateways sit in the traffic between an agent and the tool servers or other agents it calls.
- agentgateway is a Linux Foundation proxy for MCP and A2A with RBAC rules written in CEL (agentgateway/agentgateway, verified 2026-09-14). It is written in Rust and licensed Apache-2.0 (agentgateway/agentgateway, verified 2026-09-14).
- Invariant provides rule-based guardrails for LLM and MCP applications (invariantlabs-ai/invariant, verified 2026-09-14). It is written in Python and licensed Apache-2.0 (invariantlabs-ai/invariant, verified 2026-09-14). Invariant Labs was acquired by Snyk in June 2025 (Snyk announcement, verified 2026-09-14).
- Belay, described under open-source agent action gates, can also work as an MCP proxy.
How Shiin relates. Shiin’s early MCP proxy implementation (v0.5) is an L2 mediated adapter with a narrower role than a general gateway. It turns supported MCP tool calls into action requests; conformance and end-to-end policy validation remain open. MCP tool annotations can make a decision stricter but never more permissive. If what you need is access control for MCP or A2A traffic shared across many agents, a general gateway may be the better fit.
Policy engines as building blocks
Policy engines evaluate rules. They do not intercept agent actions themselves; other tools embed them.
- Cedar is an authorization policy language and engine (cedar-policy/cedar, verified 2026-09-14).
Its
cedar-policyRust crate is licensed Apache-2.0, Cedar is a CNCF Sandbox project, and it is formally verified in Lean (cedar-policy/cedar, verified 2026-09-14). - regorus is a Rust interpreter for Rego, and it is used in Azure Policy (microsoft/regorus, verified 2026-09-14).
- Other projects on this page also build on policy languages: Cupcake uses OPA/Rego compiled to WebAssembly (eqtylab/cupcake, verified 2026-09-14), and agentgateway uses CEL (agentgateway/agentgateway, verified 2026-09-14).
How Shiin relates. Shiin builds on Cedar rather than competing with it. Operators write TOML policy, which Shiin compiles to Cedar. The choice was accepted in ADR-0014, and the source implementation uses it. Direct Cedar input remains unimplemented.
Open-source agent action gates
These projects are closest to Shiin. They act at the point where an agent proposes an action and decide what happens to it.
- Belay is a local-first security layer at the tool-call boundary for Claude Code, Codex, Cursor, and MCP, using native hooks or an MCP proxy (SECBLOK/belay, verified 2026-09-14). It returns “deny”, “ask”, or “allow” and keeps a tamper-evident audit trail (SECBLOK/belay, verified 2026-09-14). It is written in Rust and licensed AGPL-3.0, with a commercial option (SECBLOK/belay, verified 2026-09-14).
- Cupcake works through the hooks of Claude Code, Cursor, and other agents, with policies written in OPA/Rego and compiled to WebAssembly (eqtylab/cupcake, verified 2026-09-14). It is written in Rust and licensed Apache-2.0 (eqtylab/cupcake, verified 2026-09-14).
- AgentFence is deny-by-default, with “allow”, “deny”, and “ask” outcomes and Ed25519-signed, hash-chained logs (dgenio/agentfence, verified 2026-09-14). It is written in Go and licensed Apache-2.0 (dgenio/agentfence, verified 2026-09-14).
- Cerberus is a local gateway for Claude Code, Codex, Cursor, and Cline that risk-scores actions and returns one of the project’s own labels: “ALLOW”, “AUDIT”, “HITL”, or “BLOCK” (Adirdabush1/cerberus, verified 2026-09-14). It is written in TypeScript and licensed Apache-2.0 (Adirdabush1/cerberus, verified 2026-09-14).
- agent-belay is a desktop approval app that shows tool requests with diffs and command previews for Claude Code, Copilot, and others, with optional LLM-based risk scoring (mikepenz/agent-belay, verified 2026-09-14). It is written in Kotlin and licensed Apache-2.0 (mikepenz/agent-belay, verified 2026-09-14). It is a separate repository from SECBLOK/belay.
- ActionRail checks consequential tool-call arguments against live systems of record and returns one of the project’s own labels: “allow”, “hold”, or “block” (ToolJet/ActionRail, verified 2026-09-14). It is written in Python, licensed Apache-2.0, and in public beta (ToolJet/ActionRail, verified 2026-09-14).
- latchagent/latch describes itself as a “control layer for autonomous AI agents” in which risky actions wait for approval (latchagent/latch, verified 2026-09-14). It is written in TypeScript and licensed MIT (latchagent/latch, verified 2026-09-14).
- Agent Governance Toolkit provides policy enforcement, zero-trust identity, sandboxing, and audit for AI agents (microsoft/agent-governance-toolkit, verified 2026-09-14). It is written in Python and licensed MIT (microsoft/agent-governance-toolkit, verified 2026-09-14).
How Shiin relates. This is where Shiin overlaps most, as the section Where Shiin overlaps and where it differs explains.
Comparison dimensions
These questions help compare any tool on this page, including Shiin. The last column includes current and planned design; the roadmap records implementation scope.
| Dimension | Question to ask of any tool | Shiin’s design |
|---|---|---|
| Agent-agnostic | Does one configuration apply across several agents and hosts? | Policy refers to normalized action types, not host tool names, and each host gets an adapter (ADR-0025) |
| Interception point and enforcement level | Where is the action caught, and can the agent route around that point? | Every adapter is labeled L1 cooperative, L2 mediated, or L3 OS-enforced, together with what it cannot see (Enforcement levels) |
| Decision determinism | Do the same inputs always produce the same decision, and can model output change it? | The same action request and policy snapshot always produce the same decision; no language-model or machine-learning output is an input (ADR-0009) |
| Approval model | How does a person approve, and is the approval format specified beyond one application? | A versioned approval protocol with single-use and scoped grants (partial source implementation; v0.3 validation remains open) |
| Evidence | What record remains, and can someone else verify it? | A source-implemented hash-chained audit log, with signed checkpoints and receipts (partial source implementation; v0.4 validation remains open) |
| Policy portability | Can policy be reviewed, tested, and reused outside one host? | TOML compiled to Cedar (ADR-0014, accepted); direct Cedar input is planned |
| Task-intent binding | Can a decision depend on the constraints of the current task, not only on standing rules? | Structured, human-confirmed task declarations (experimental source; measurements open) |
| Local-first | Does it run on the developer’s machine without a hosted service or telemetry? | Runs locally with no telemetry, and every network feature is opt-in (ADR-0007) |
Projects at a glance
The tables below summarize the facts cited in Categories. Every row links its source, and every fact was verified on 2026-09-14. “Not documented” means the sources checked for this page did not establish the fact.
Host and platform controls
| Control | What it does (per its documentation) | Limits stated in its documentation |
|---|---|---|
| Claude Code (hooks, permissions, sandboxing; verified 2026-09-14) | Permission rules; PreToolUse and PermissionRequest hooks with allow, deny, or ask decisions; a sandbox for Bash commands and their child processes | Hooks can be disabled with --bare or disableAllHooks; managed hooks cannot be disabled |
| OpenAI Codex CLI (agent approvals and security, hooks; verified 2026-09-14) | Approval policies; sandbox modes using Seatbelt on macOS, and bwrap and seccomp on Linux; PreToolUse hooks covering Bash, apply_patch, and MCP tools | Hooks are “a useful guardrail, not a complete enforcement boundary” |
| Cursor (hooks; verified 2026-09-14) | Hooks since 1.7, including beforeShellExecution, beforeMCPExecution, beforeReadFile, and preToolUse, with allow, deny, or ask responses and a failClosed option | not documented |
| GitHub Copilot coding agent firewall (customize the agent firewall; verified 2026-09-14) | Restricts outbound network access for processes started by the agent’s Bash tool | Does not apply to MCP servers or setup steps; “should not be considered a comprehensive security solution” |
| Coder Agent Firewall (agent firewall; verified 2026-09-14) | Process-level network firewall that restricts domains and HTTP methods, with audit logs; formerly called Agent Boundaries | Network egress only |
Open-source projects
| Project | What it does (per its documentation) | Interception point | Decision outcomes (project’s own labels) | Evidence | Policy language | Language and license |
|---|---|---|---|---|---|---|
| SECBLOK/belay (verified 2026-09-14) | Local-first security layer at the tool-call boundary for Claude Code, Codex, Cursor, and MCP | Native hooks or an MCP proxy | “deny”, “ask”, “allow” | Tamper-evident audit trail | not documented | Rust; AGPL-3.0 with a commercial option |
| eqtylab/cupcake (verified 2026-09-14) | Policies for Claude Code, Cursor, and other agents | Host hooks | not documented | not documented | OPA/Rego compiled to WebAssembly | Rust; Apache-2.0 |
| dgenio/agentfence (verified 2026-09-14) | Deny-by-default action decisions | not documented | “allow”, “deny”, “ask” | Ed25519-signed, hash-chained logs | not documented | Go; Apache-2.0 |
| Adirdabush1/cerberus (verified 2026-09-14) | Risk-scores actions from Claude Code, Codex, Cursor, and Cline | Local gateway | “ALLOW”, “AUDIT”, “HITL”, “BLOCK” | not documented | not documented | TypeScript; Apache-2.0 |
| mikepenz/agent-belay (verified 2026-09-14) | Desktop approval app showing tool requests with diffs and command previews for Claude Code, Copilot, and others; optional LLM-based risk scoring | not documented | not documented | not documented | not documented | Kotlin; Apache-2.0 |
| ToolJet/ActionRail (verified 2026-09-14) | Checks consequential tool-call arguments against live systems of record; public beta | not documented | “allow”, “hold”, “block” | not documented | not documented | Python; Apache-2.0 |
| latchagent/latch (verified 2026-09-14) | “Control layer for autonomous AI agents” in which risky actions wait for approval | not documented | not documented | not documented | not documented | TypeScript; MIT |
| microsoft/agent-governance-toolkit (verified 2026-09-14) | Policy enforcement, zero-trust identity, sandboxing, and audit for AI agents | not documented | not documented | Audit | not documented | Python; MIT |
| luckyPipewrench/pipelock (verified 2026-09-14) | Agent firewall with DLP, SSRF, prompt-injection, and tool-poisoning detection | HTTP, MCP, A2A, and WebSocket traffic | not documented | Signed action receipts | not documented | Go; Apache-2.0 core, Elastic-2.0 enterprise features |
| agentgateway/agentgateway (verified 2026-09-14) | Linux Foundation proxy for MCP and A2A | MCP and A2A proxy | not documented | not documented | RBAC rules in CEL | Rust; Apache-2.0 |
| invariantlabs-ai/invariant (verified 2026-09-14) | Rule-based guardrails for LLM and MCP applications | not documented | not documented | not documented | not documented | Python; Apache-2.0 |
Where Shiin overlaps and where it differs
Overlap
Deciding, before an agent’s action runs, whether it proceeds, is refused, or waits for a person already exists in open-source projects.
- Belay and AgentFence document allow, deny, and ask outcomes together with tamper-evident or signed, hash-chained audit records (SECBLOK/belay, dgenio/agentfence, verified 2026-09-14).
- Cupcake applies policy through the hooks of Claude Code, Cursor, and other agents (eqtylab/cupcake, verified 2026-09-14).
- Cerberus returns its own “ALLOW”, “AUDIT”, “HITL”, or “BLOCK” labels from a local gateway (Adirdabush1/cerberus, verified 2026-09-14).
Related ideas exist too. Cupcake and agentgateway express policy in established languages, pipelock issues signed action receipts, and Belay is local-first and written in Rust (sources in Categories). None of these, on its own, is a reason to choose Shiin: intercepting actions through hooks, returning allow-or-deny outcomes, keeping an audit trail, writing policy as code, running locally, or being written in Rust.
Differences
Shiin’s case rests on the four differentiators in the vision. Each is a design commitment; the source implements task constraints, core wire contracts, policy evaluation, cooperative hooks, Unix approval services, evidence, and early integrations. Host validation and stronger enforcement remain open.
- Task-intent binding. Shiin checks actions against constraints in experimental, human-confirmed task declarations, as well as standing policy; measurements and host validation remain open. This page records no verified fact that another project listed here binds decisions to per-task constraints. If one does, please request a correction.
- An open, agent-neutral contract. The action request, decision, and approval protocol are designed as versioned specifications with schemas and test vectors that other tools can implement. Several projects above support more than one host, but supporting several hosts is not the same as publishing the request and approval formats as independent specifications. This page records no verified fact that another project listed here publishes such specifications.
- Analyzable policy. Policy as code is not new; Cupcake uses Rego and agentgateway uses CEL. Shiin uses Cedar behind a TOML front-end so that policies can be tested and explained (ADR-0014, accepted).
- Explicit enforcement levels. Vendors already state the limits of their own controls: Codex CLI calls hooks “a useful guardrail, not a complete enforcement boundary” (hooks, verified 2026-09-14), and GitHub says its agent firewall “should not be considered a comprehensive security solution” (customize the agent firewall, verified 2026-09-14). Shiin applies that honesty uniformly: every adapter is designed to state its enforcement level and what it cannot see (ADR-0033).
A differentiator on paper is not a working tool. Until Shiin ships, the projects above are the ones you can use.
When to use something else instead of or alongside Shiin
| If you need | Consider | Relationship to Shiin |
|---|---|---|
| A tool you can use today | One of the open-source agent action gates | Shiin also offers experimental CLI and Unix-daemon archives; see its limitations before use |
| Controls for one host, where its built-in settings meet your needs | Host-native controls | Instead or alongside: keep them enabled even if you adopt Shiin |
| To limit what an allowed command, or the code it runs, can reach | An OS sandbox, container, or virtual machine | Alongside: Shiin does not isolate processes |
| To restrict where an agent’s traffic can go, or to inspect that traffic | Network egress controls | Alongside: Shiin does not inspect traffic and cannot see data leaving through an allowed action |
| Access control for MCP or A2A traffic shared across many agents | MCP and agent gateways | Instead or alongside, depending on whether you also want per-action policy and task binding on the developer’s machine |
| A desktop approval app with diffs and command previews | mikepenz/agent-belay (verified 2026-09-14) | Instead or alongside: Shiin’s Unix approval protocol and command-line approver exist in source; desktop approvers remain future work |
| To check tool-call arguments against live systems of record | ToolJet/ActionRail (verified 2026-09-14) | Alongside: Shiin’s engine performs no I/O and decides from the action request and local policy alone |
| Policies written in Rego | eqtylab/cupcake, or microsoft/regorus to build your own (verified 2026-09-14) | Instead: Shiin proposes Cedar |
| A broad agent-governance toolkit in Python | microsoft/agent-governance-toolkit (verified 2026-09-14) | Instead or alongside |
| An authorization engine inside your own application, without agent-specific features | Cedar directly (verified 2026-09-14) | Instead: Shiin adds action types, adapters, task binding, approvals, and evidence on top of Cedar |
Keeping this page accurate
- Anyone, including the maintainers of a project described here, may request a correction by opening an issue at BAGOMBEKA-JOB-DEV/shiin.
- Every fact about another project links a public source and states the date it was verified.
- A claim that cannot be verified from a public source is removed rather than guessed, and a table cell that cannot be verified reads “not documented”.
- Maintainers re-verify every fact on this page before each release and update the
revieweddate in the page metadata. - Projects are described in their own terms and credited for their work. This page does not disparage any project.
- A project is added through a pull request that includes public sources. Being listed is not an endorsement.
Read next
FAQ
Note
Design document: draft. Authorization, approvals, evidence, and early client/integration code exist in source; platform, host, and milestone validation remain incomplete.
What is Shiin?
Shiin is an agent-agnostic authorization layer for AI agent actions. It sits between agents and the files, commands, networks, and services they act on.
Is Shiin ready to use?
Shiin is experimental; source builds and published alpha binaries are intended for evaluation. The engine, CLI, audit log, and experimental tasks exist; live-host validation and milestone exit criteria remain open. See installation and the roadmap.
What makes Shiin different?
Four things:
- Task-intent binding. Actions are checked against the constraints of the task the developer confirmed.
- An open, agent-neutral contract. The action request, decision, and approval protocol are versioned specifications with schemas and test vectors.
- Analyzable policy. Policies compile to Cedar, a formally verified authorization language.
- Explicit enforcement levels. Every integration is labeled cooperative, mediated, or OS-enforced.
Does Shiin use a language model to decide?
No. No decision ever depends on the output of a language model.
Does Shiin collect telemetry?
No. Shiin is local-first and collects no telemetry.
What can Shiin do that my host’s built-in controls cannot?
Shiin applies one policy across every agent and binds decisions to the task the developer gave. Host controls are configured separately per agent and do not know the task.
What can Shiin not do?
Shiin is not a sandbox, a network firewall, a prompt-injection detector, or a DLP. It complements those tools; it does not replace them.
Read next
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) |
Read next
Action requests
Note
Guide: draft. The source build implements the current behavior below; remaining design work is marked explicitly.
An action request is the normalized, host-independent description of one proposed action.
What it contains
An action request records:
- Which agent is asking, and its assurance level.
- The action type, such as
fs.write. - The effects: what resources the action touches, with a confidence.
- The active task, if any.
- The policy snapshot digest.
How an adapter builds it
An adapter turns a host payload into an action request. It classifies the action and its effects. Anything it cannot classify becomes an opaque effect.
Example
An agent edits src/login.rs through a file-write tool.
The adapter builds a request with action type fs.write and one effect: an exact write to /home/dev/project/src/login.rs.
Read next
Decisions
Note
Guide: draft. The source build implements the current behavior below; remaining design work is marked explicitly.
A decision is the engine’s answer to an action request.
Values
| Value | Meaning |
|---|---|
allow | The action may proceed. |
deny | The action must not proceed. |
pending | The action must wait for an approver. |
Reason codes
Every decision carries a stable, machine-readable reason code, such as deny.policy or pending.opaque.
How decisions are reached
The engine runs five stages in order: identity, capability, policy, intent, and risk.
A deny from any stage wins.
When no rule matches, the decision is deny.
Grants
Only a policy rule, or a grant created by an approver, can produce allow.
A grant never overrides deny.
Read next
Agents and identity
Note
Guide: draft. The source build implements the current behavior below; remaining design work is marked explicitly.
An agent is software that uses a model to propose actions on a person’s behalf, such as Claude Code or Codex CLI. Shiin treats every agent as untrusted.
Identity
An agent identity is the identifier of the agent that proposed an action, together with its assurance level.
Assurance levels
Current hooks report asserted identities; assurance-based identity enforcement is not implemented. The stronger levels below are planned.
| Level | Meaning |
|---|---|
asserted | The host reports the agent id. Nothing verifies it. |
attested-local | The agent id is attested by a local trust root. |
attested-os-user | The agent id is attested by the operating system user. |
attested-crypto | The agent id is attested cryptographically. Reserved for the future. |
Untrusted input
Anything the agent or a tool server supplies, including explanations, tool annotations, and self-reported identity, can make a decision stricter but never more permissive.
Read next
Capabilities
Note
Guide: draft. Explicit capability grants and refusals are implemented in source. Identity assurance and independent review remain open.
A capability is a coarse permission for an agent to attempt a whole class of actions at all, for example “may run shell commands”.
How capabilities work
The capability stage is a gate: it can stop or hold an action but never allow it.
Only a policy rule, or a grant, can produce allow.
Capabilities in policy
Capabilities are granted or denied explicitly in the policy. There is no implicit capability. Every action class and effect action class needs a grant; any matching refusal wins. Capabilities pass only this gate; policy, task, risk, and built-in denials still apply.
[capabilities.shell]
id = "shell"
allow = true
action_types = ["shell.exec"]
[capabilities.read]
id = "read"
allow = true
action_types = ["fs.read"]
These declarations let policy consider a shell command that reads a file.
They do not grant a policy allow, and omitting the read capability denies the file-read effect.
Use deny = true in a separate declaration to refuse a class even when another capability grants it.
Agent selectors match asserted identifiers; they do not prove which agent is acting.
Example
An agent with the shell.exec capability may attempt shell commands.
Without it, every shell command is denied with reason code deny.capability.
Read next
Policies
Note
Guide: draft. The source build implements the current behavior below; remaining design work is marked explicitly.
A policy is the complete set of rules, capabilities, defaults, and risk configuration that applies to an evaluation.
Layers
The designed layers are built-in, user, and workspace.
The current CLI loads .shiin/policy.toml as one operator-controlled layer; separate user/workspace loading and trust-by-hash are not implemented.
A workspace layer can restrict a user layer only; it can never make a deny into an allow unless the operator trusts it by hash.
Rules
A rule matches an action request and produces allow, deny, or pending.
Policies refer to action types, never to host tool names.
Default decision
When no rule matches, the decision is deny.
The default can be configured to pending, but it can never be allow.
Writing policy
Policy is written in TOML and compiled to Cedar. See Writing policies.
Read next
Tasks and intent
Note
Guide: draft. The source build implements the current behavior below; remaining design work is marked explicitly.
A task declaration is a structured, human-confirmed statement of what an agent is working on: a description, constraints, and an expiry.
Intent binding
Intent binding checks an action against the constraints of the active task. Natural-language descriptions of intent are recorded but never evaluated.
Constraints
The constraint set is closed:
| Constraint | Meaning |
|---|---|
scope.paths | Paths the agent may touch. |
forbid.action_types | Action types the agent may not perform. |
allow.hosts | Hosts the agent may contact. |
limits | Counters for actions such as deletions. |
expires_at | When the task ends. |
require_pending_for | Action types that always need approval. |
Expiry
When a task expires, its constraints and any grants tied to it end.
Read next
Risk signals
Note
Guide: draft. The source build implements the current behavior below; remaining design work is marked explicitly.
A risk signal is a discrete, deterministic property of an action request that can escalate a decision. Shiin does not use numeric risk scores.
Signals
| Signal | Triggered by |
|---|---|
secret_path | An effect touches a path known to hold secrets. |
outside_workspace | An effect touches a path outside the workspace. |
bulk_delete | A single action would delete more files than a threshold. |
opaque_command | An action carries an opaque effect. |
destructive_vcs | A version-control operation that can destroy shared history. |
Configuration
Each signal has a configured decision: deny, pending, or pass.
A signal that is not configured produces pass.
Read next
Approvals
Note
Guide: draft. Unix approval requests, grants, and CLI resolution exist in source. Remote approvals remain a draft proposal in RFC-0001.
An approval request is a pending decision waiting for an approver to resolve it.
Resolution
An approver can:
- Allow once. Creates a single-use grant bound to the digest of one action request.
- Allow for a scope and time. Creates a scoped grant covering an agent, action types, a resource pattern, and a workspace, which expires.
- Deny. The action is refused.
A grant never overrides deny.
Channels
Approval requests reach a human through an approval channel. The current source implementation supports a separate operator terminal on Unix. Remote approval is not implemented; RFC-0001 proposes an opt-in signed polling transport.
Deny-with-ticket
For a host that cannot wait, the adapter denies the action now and returns an approval ticket. If the agent retries after an approver approves, the retry consumes a single-use grant.
Expiry
An approval request that is never resolved expires as deny.
It never becomes allow on its own.
Read next
Adapters
Note
Guide: draft. The source build implements the current behavior below; remaining design work is marked explicitly.
An adapter connects a host to Shiin. It intercepts proposed actions, builds action requests, and enforces the resulting decision using the host’s own protocol. Adapters never make decisions.
Enforcement levels
Every adapter declares an enforcement level:
| Level | Meaning |
|---|---|
| L1 Cooperative | The host honors the decision through its hook mechanism. |
| L2 Mediated | Shiin sits in the data path. |
| L3 OS-enforced | The operating system or a sandbox enforces the decision. |
Hosts
| Host | Level | Interception | Milestone |
|---|---|---|---|
| Claude Code | L1 | hooks | v0.1 |
| Codex CLI | L1 | hooks | v0.2 |
| Cursor | L1 | hooks | v0.2 |
| MCP proxy | L2 | data path | v0.5 |
Honest labeling
Every adapter states its enforcement level and what it cannot see.
Read next
Audit and evidence
Note
Guide: draft. The source build implements the current behavior below; remaining design work is marked explicitly.
Audit log
The audit log is an append-only JSON Lines file of audit events linked by a hash chain. It is tamper-evident, not tamper-proof.
Evidence
Hook requests and decisions produce audit events before the hook responds.
Offline check and explain do not write hook audit events.
Approval and grant events exist in the Unix daemon’s source implementation.
Command and URL values are recorded as digests, as described in audit events.
Verification
shiin verify checks that the log’s hash chain is intact.
Replay
shiin replay re-evaluates captured requests with the originating replay key and can compare a different policy.
Receipts
Signed receipts attest that a decision was made when evidence recording is enabled in the daemon. A receipt proves who signed a statement about a decision, not that the action’s real outcome was correct.
Read next
Limitations
Note
Design document: draft. Authorization, approval, evidence, and early client/integration code exist in source; host, platform, and milestone validation remain incomplete.
Shiin decides whether an action should happen before it runs. These limits follow from where Shiin sits in the system.
What Shiin cannot see
- Code run by an allowed command. Once a command is allowed, Shiin does not see what it does. A sandbox covers that gap.
- Data leaving through an allowed action. Shiin cannot inspect traffic content or see data leaving through an action it allowed. An egress proxy or DLP covers that gap.
- Actions outside the interception point. An L1 adapter cannot see actions that do not pass through the hook. The adapter’s enforcement level states this honestly.
What Shiin is not
Shiin is not a sandbox, a network firewall, a prompt-injection detector, or a DLP. It complements those tools; it does not replace them.
What Shiin cannot guarantee
- No bypass. A determined attacker with access to the operating system can bypass Shiin.
- Perfect visibility. Classifiers are conservative; anything unknown becomes opaque and needs approval.
- Outcome correctness. Shiin authorizes an action; it does not guarantee the action’s real outcome was correct.
Read next
Installation
Note
Guide: draft. The first alpha,
v0.1.0-alpha.1, provides Linux and macOS CLI and Unix-daemon archives. Source builds remain available; the alpha does not freeze APIs or complete the roadmap. Windows daemon transport and path qualification remain open; no Windows archive is included in this first alpha.
Published alpha archives
Use the releases page to select a published version.
The CLI archive is named shiin-cli-<target>.tar.xz; the optional daemon archive is shiind-<target>.tar.xz.
The targets are x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl, aarch64-apple-darwin, and x86_64-apple-darwin.
Download the matching archive and its .sha256 file, then follow release verification before running either executable.
Extract the verified archive and put its executable in a directory on your PATH.
The generated shiin-cli-installer.sh and shiind-installer.sh are alternatives available on the same release.
Review a downloaded installer before running it; installation does not start a service or change a host’s hooks.
The daemon runs in the foreground when explicitly started with shiind --operator in a dedicated operator terminal.
Keep the displayed approval credential out of agent sessions.
Check the installed version with shiin --version and, if installed, shiind --version.
Linux signing/evidence initialization needs an available, unlocked Secret Service; unavailable keys fail without a file fallback.
Build from source
Shiin needs Rust. The repository pins its toolchain, so rustup selects the right version on its own.
$ git clone https://github.com/BAGOMBEKA-JOB-DEV/shiin.git
$ cd shiin
$ cargo install --locked --path crates/shiin-cli
That puts shiin in ~/.cargo/bin. Check it:
$ shiin doctor
doctor reports the workspace it would act on, whether a policy is present and valid,
and the enforcement level you are getting. It is the fastest way to see that a change
to your setup took effect.
What installing does not do
Installing changes nothing about your agents. Shiin only sees an action if a host sends it, so until you configure a host’s hook, Shiin is inert. The quickstart connects Claude Code.
Uninstalling has the same shape in reverse: removing the binary leaves the host’s hook pointing at nothing. Remove the hook configuration as well, or the host will report a failing hook on every tool call.
To build the optional Unix daemon, use cargo install --locked --path crates/shiind.
The MCP proxy and client library remain source integrations, with their limitations described in the roadmap.
Read next
Quickstart with Claude Code
Note
Guide: draft. These steps work with a source build. Read installation first.
By the end you will have refused a real action: Claude Code will ask to read a .env
file, and Shiin will say no.
1. Write a policy
From the root of the project you want to protect:
$ shiin init
$ shiin policy validate
init writes .shiin/policy.toml. It allows reads and edits under src/, tests/,
and docs/, allows a few test and status commands, denies reads of secrets, and denies
everything it does not mention.
2. Connect the hook
Claude Code can run a program before each tool use and read a decision back
(verified against https://code.claude.com/docs/en/hooks on 2026-09-14).
Add this to .claude/settings.json in the same project:
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "shiin hook claude-code",
"timeout": 10,
"onFailure": "block"
}
]
}
]
}
}
The matcher * sends every tool call to Shiin, which is what you want: a rule that
only covers file tools would miss the same file being read by a shell command.
onFailure requires Claude Code 2.1.295 or later and denies the action when the hook cannot start, crashes, or times out
(hooks reference, verified 2026-10-15).
The internal watchdog covers a running Shiin process; host configuration also covers failures before it starts.
3. Watch it refuse something
Start Claude Code in that project and ask it to read your .env file.
The hook answers, and the agent is told the action was denied.
Then look at what happened:
$ shiin log
1 2026-10-12T13:34:39Z request.received fs.read
2 2026-10-12T13:34:39Z decision.made deny (deny.policy)
$ shiin verify
the audit log is intact: 2 event(s), chain verified
Ask it to run cat .env instead. That is a shell command, not a file tool, and it is
denied by the same rule, because the classifier records the read the command performs.
4. Know what you have
$ shiin doctor
The line that matters most reads enforcement L1 cooperative. It means Claude Code
honors Shiin’s decisions but can be started without hooks, and that Shiin never sees an
action the host does not send it. Enforcement levels
explains what that does and does not protect, and
using Shiin with sandboxes covers the rest.
Read next
Your first policy
Note
Guide: draft. The policy format and the commands below work with a source build.
shiin init writes a starter policy. This page explains what it says and how to change it
with confidence.
The starter policy
version = 1
[capabilities.workspace]
id = "workspace"
allow = true
action_types = ["fs.*", "shell.exec"]
# Anything no rule allows is denied.
[defaults]
decision = "deny"
[[rule]]
id = "read-and-edit-source"
effect = "allow"
action_types = ["fs.read", "fs.list", "fs.write", "fs.create"]
resources = ["./src/**", "./tests/**", "./docs/**"]
[[rule]]
id = "no-secrets"
effect = "deny"
action_types = ["fs.read"]
resources = ["**/.env", "**/.env.*", "~/.ssh/**", "**/credentials"]
reason = "Secret files are not readable by agents."
[[rule]]
id = "run-tests"
effect = "allow"
action_types = ["shell.exec"]
commands = ["cargo test*", "cargo nextest*", "git status", "git diff*"]
[risk]
opaque_command = "pending"
secret_path = "deny"
bulk_delete = { decision = "pending", threshold = 20 }
destructive_vcs = "pending"
privilege_escalation = "pending"
Three things are worth noticing.
- The default is a denial. Every rule widens what is possible from nothing. You cannot forget to deny something, only forget to allow it, and the failure is a refusal rather than an exposure.
no-secretswins overread-and-edit-source. A denial beats an allow wherever both match, so the ordering of rules in the file does not matter.- The
[risk]table does not decide anything. It escalates.opaque_commandturns a command whose effects cannot be read, such aspython cleanup.py, into a decision for a person rather than an allow.
Try it without an agent
Put an action request in a file and decide it directly. This is the same path the hook takes, so it is a safe way to test a policy change.
$ cat > /tmp/request.json <<'JSON'
{
"schema": "shiin.action-request/v1-draft.1",
"id": "0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0b",
"session": "manual",
"agent": { "id": "claude-code", "assurance": "asserted" },
"action_type": "fs.read",
"effects": [
{ "kind": "path", "value": "/home/dev/project/.env", "confidence": "exact" }
],
"workspace": { "root": "/home/dev/project" },
"policy_snapshot": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"host": { "name": "claude-code", "tool": "Read" }
}
JSON
$ shiin explain /tmp/request.json
decision Deny
reason deny.policy
stage policy
rules no-secrets
Change value to a path under src/ and the same command reports an allow.
Make one change
Allow the formatter, and nothing more:
[[rule]]
id = "run-formatter"
effect = "allow"
action_types = ["shell.exec"]
commands = ["cargo fmt*"]
Then check the policy still compiles, which also catches a mistyped key:
$ shiin policy validate
.shiin/policy.toml is valid: 4 rule(s), 5 risk signal(s), default Deny
Read next
- Writing policies, for the full set of keys
- Policy model, for the rules the engine follows
Writing policies
Note
Guide: draft. Everything below works with a source build, except where it says planned. Policy model is the normative source.
A policy is one TOML file, .shiin/policy.toml. It says what agents may attempt in this
workspace, and nothing it does not mention is allowed.
Shape of the file
version = 1 # the format version; only 1 is understood
[capabilities.workspace]
id = "workspace"
allow = true
action_types = ["fs.*", "shell.exec"]
[defaults]
decision = "deny" # what happens when no rule matches; "pending" is allowed, "allow" is not
[[rule]] # repeat this block for each rule
id = "edit-source" # unique, and it appears in decisions and the audit log
effect = "allow" # "allow", "deny", or "pending"
action_types = ["fs.write"] # which kinds of action this rule is about
resources = ["./src/**"] # which paths
commands = ["cargo test*"] # which commands
agents = ["claude-code"] # which agents; omit for all of them
reason = "..." # shown when this rule decides
[risk] # signals that escalate a decision
opaque_command = "pending"
bulk_delete = { decision = "pending", threshold = 20 }
An unknown key is refused rather than ignored, so a typo fails closed instead of quietly widening what is allowed.
How a rule matches
A rule matches when every matcher it declares matches. A matcher you leave out is not a
constraint, so a rule with only action_types applies to every resource.
- Action types come from the registry, such as
fs.readorshell.exec. A trailing*is a wildcard, sofs.*covers the whole namespace. - Resources are globs against canonical, absolute paths.
*matches within a path segment and**matches across segments. A pattern beginning./is relative to the workspace, so it can never match a path outside it. A pattern beginning~/is relative to your home directory. - Commands are globs against the command as a whole, so
cargo test*coverscargo test --all. They are not regular expressions. - Agents are identifiers such as
claude-code. - Sessions and tasks are exact identifier lists; a task selector cannot match when no task is active.
Capabilities are separate from rules.
Every action/effect class needs an explicit capability grant, and a matching capability denial wins over a grant.
The starter policy includes grants for file operations and shell commands; an older policy without declarations now produces deny.capability.
Add only the classes the agent needs before validating and testing a migration.
Agent selectors remain asserted identifiers, not proof of identity.
A rule also matches an action whose effects are of the type it names, not only an action whose own type matches. This is what makes one rule cover both ways of reading a file:
[[rule]]
id = "no-secrets"
effect = "deny"
action_types = ["fs.read"]
resources = ["**/.env"]
That rule denies the file-read tool, and it denies cat .env as well, because the
classifier records that the command performs a read of that path.
A command whose behavior cannot be read, such as python cleanup.py, carries no such
detail: it becomes an opaque effect, which the opaque_command signal turns into a
decision for a person.
How rules combine
- A matching
denywins over everything, so rule order in the file never matters. - Otherwise a matching
pendingholds the action for a person. - Otherwise a matching
allowpermits it. - Otherwise the default decision applies, which is a denial.
Risk signals and task constraints can make a result stricter, never more permissive. Evaluation states this precisely.
Risk signals
A signal does not decide; it escalates. Configure each one with the decision it escalates to, and a threshold where it counts something:
| Signal | Fires when |
|---|---|
opaque_command | An action carries an effect Shiin cannot read, such as an interpreter or a shell substitution |
bulk_delete | A single action would delete more files than the threshold, which defaults to 20 |
destructive_vcs | A command rewrites or discards version control history, such as a force push |
privilege_escalation | A command asks for higher privileges, such as sudo |
secret_path | An effect touches a path known to hold secrets |
outside_workspace | A write lands outside the workspace |
The full definitions are in risk signals.
Testing a change
Validate the file, then decide a request with it before trusting it to an agent:
$ shiin policy validate
$ shiin explain /tmp/request.json
explain prints the stage that decided and the rules that matched, which is usually enough
to see why a rule did not apply.
Recorded policy cases run through shiin policy test; see testing policies.
Not yet available
| Feature | Arrives |
|---|---|
| Separately trusted user/workspace layers and attested identity | still open |
| Writing rules directly in Cedar | after the policy language is stable |
Read next
Using Shiin with sandboxes
Note
Guide: draft. The source build implements the current behavior below; remaining design work is marked explicitly.
Shiin decides whether an action should happen. A sandbox limits the damage if an allowed action, or the code it runs, misbehaves.
Layering
Shiin and a sandbox answer different questions. 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.
Why both
Shiin cannot see what a program does after it is allowed to run. A sandbox covers that gap. Conversely, a sandbox cannot decide that an action should not happen at all.
Recommended setup
- Enable Shiin’s adapter for your host.
- Enable the host’s sandbox for Bash commands.
- Keep host permission rules enabled alongside Shiin.
Read next
Declaring tasks
Note
Guide: draft. This works with a source build. The feature is experimental: the constraint set is deliberately small, and it will grow based on what turns out to be needed. Intent binding is the normative source.
Policy says what an agent may ever do in this workspace. A task says what it may do right now, for the work you actually asked for. An action can be refused for not belonging to the task even when policy would allow it.
Why this exists
Suppose your policy lets agents write anywhere in the workspace, which is reasonable. You ask an agent to fix a login bug. It decides the cleanest fix is a database migration and starts writing one. Policy has no opinion, because the policy was written for the repository, not for this request.
A task gives Shiin the missing context, and it comes from you rather than from the model.
Declare one
$ shiin task start "Fix the login bug" \
--scope './src/auth/**' \
--scope './tests/auth/**' \
--forbid db.schema_change \
--max-writes 20 \
--minutes 90
Quote the globs, so your shell does not expand them before Shiin sees them.
| Option | Effect |
|---|---|
--scope <glob> | Changes outside these paths are refused. Repeatable |
--forbid <action type> | These action types are refused. Repeatable |
--allow-host <host> | Network effects to any other host are refused. *.example.com matches subdomains. Repeatable |
--ask-for <action type> | These action types always need a person, even when policy allows them. Repeatable |
--max-writes <n> | Most files the task may write or create |
--max-deletes <n> | Most files the task may delete |
--max-commands <n> | Most commands the task may run |
--minutes <n> | The task ends after this long |
What it looks like in use
$ shiin task show
id 0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0b
description Fix the login bug
scope ./src/auth/**, ./tests/auth/**
forbidden db.schema_change
hosts none
ask for none
expires 2026-10-12T15:04:00Z
writes 3 of 20
deletes 0 of none
commands 0 of none
A write under src/auth/ proceeds. A write to migrations/0042.sql is refused with
reason code deny.intent, and the audit log records which task refused it.
When the work is done:
$ shiin task end
Rules worth knowing
- A task only ever narrows. It cannot permit something policy refuses. A denial from either one is a denial.
- Only you can declare a task. An agent cannot start one for itself. If it could, a manipulated agent would simply declare a task that permitted what it wanted.
- Reads are not scope-limited. A task that stopped an agent from reading the repository would stop it from working. Policy still governs what may be read, which is what keeps secrets shut.
- A refused action does not spend the budget. Counters move only when an action is allowed.
- An expired task has ended. Its constraints stop applying, and policy alone decides again. Shiin does not keep enforcing yesterday’s instructions.
- Natural language is never enforced. The description is recorded for the audit log. Only the constraints are checked, because only they can be checked exactly.
Limits of this
A task constrains the actions Shiin sees. It cannot constrain what a program does once
that program is allowed to run, so python cleanup.py is still opaque and still needs a
person. See limitations.
Counters live in .shiin/tasks/, which policy cannot grant an agent access to
(ADR-0016).
Read next
Testing policies
Note
Guide: draft. This works with a source build.
A policy is code: it decides things, and it breaks quietly. shiin policy test runs cases
you write, so a change to a rule cannot silently stop protecting something.
The cases use the same format as the project’s own test vectors, and the same runner, so what you write is checked exactly the way the specification’s corpus is.
Write a case
Put one JSON file per case in .shiin/tests/. A case that carries no policy of its own is
evaluated against your workspace’s policy, which is what makes it a regression test.
{
"schema": "shiin.test-vector/v1-draft.1",
"id": "secrets-stay-shut",
"name": "No agent reads .env, however it asks",
"action_request": {
"schema": "shiin.action-request/v1-draft.1",
"id": "0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0b",
"session": "manual",
"agent": { "id": "claude-code", "assurance": "asserted" },
"action_type": "shell.exec",
"effects": [
{ "kind": "command", "value": "cat .env", "confidence": "exact" },
{
"kind": "path",
"value": "/home/dev/project/.env",
"confidence": "inferred",
"action_type": "fs.read"
}
],
"workspace": { "root": "/home/dev/project" },
"policy_snapshot": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"host": { "name": "claude-code", "tool": "Bash" }
},
"expected": { "decision": "deny", "reason": "deny.policy" }
}
Run them
$ shiin policy test
ok secrets-stay-shut deny.policy
FAILED bulk-delete-asks expected pending/pending.risk, got allow/allow.policy
2 case(s), 1 failed
The command exits non-zero when any case fails, so it belongs in whatever runs your
checks. Point it elsewhere with shiin policy test <directory>.
What to write cases for
- Every rule you rely on to refuse something. A deny rule that stops matching is invisible until it matters.
- The ways around a rule. The same read through a file tool, through
cat, and through a script. The third is opaque, so expectpending.opaquerather than a denial. - Each risk signal you configured, with a case just over the threshold.
- Your task constraints, by including a
taskand, for a limit, ausageshowing what has been spent already. - Anything you have ever had to fix. A case is cheaper than remembering.
Fields a case may carry
| Field | Purpose |
|---|---|
action_request | The action to decide. Required |
expected | The decision and reason code. Required |
policy | A policy for this case alone. Omit to use the workspace’s |
task | An active task declaration |
usage | What the task has already spent, for limit cases |
mode | unattended to check what happens with no approver |
now | The evaluation time, for expiry cases |
Read next
Handling approvals
Note
Guide: draft. Local approval services and CLI administration are implemented in source for Unix. Host validation, Windows transport, and independent security review remain open.
Start the local service
Build the workspace and run the daemon in a dedicated operator terminal:
$ cargo build --workspace
$ shiind --operator
The daemon prints a fresh operator credential only after acquiring its exclusive state lock and binding its local socket. Keep that credential in your terminal; never give it to an agent or put it in a command argument, environment variable, or workspace file. It changes when the daemon restarts, while pending requests and grants remain in protected per-user state.
Enable the daemon explicitly for hook invocations with SHIIN_DAEMON=1.
The default endpoint is ~/.shiin/runtime/shiind.sock; an absolute SHIIN_SOCKET override selects another protected directory.
An explicitly configured daemon that is unreachable causes denial, with no fallback to daemonless evaluation.
Inspect and resolve actions
From your own terminal:
$ shiin pending
$ shiin approve EXAMPLE-TICKET allow_once 300
$ shiin approve EXAMPLE-TICKET deny 300
$ shiin approve EXAMPLE-TICKET allow_always 3600 scope.json
$ shiin grants
$ shiin revoke EXAMPLE-GRANT-ID
Replace the example identifiers with the ticket or grant shown by Shiin.
Each command reads the operator credential with terminal echo disabled.
Approval displays the normalized action and requires a typed confirmation before resolution.
A scoped grant requires an explicit JSON scope file with agent, exact action_types, resource, and workspace fields.
The resource glob applies to every path effect; the explicitly selected pattern * also covers non-path effects.
Grants remain bound to the session, active task, and policy snapshot, and expire after at most 24 hours or the task deadline, whichever comes first.
Agent-facing requests cannot resolve, list, or revoke approvals without the separate operator credential.
The built-in policy denies intercepted shiin approve, shiin revoke, and shiind commands.
This is cooperative enforcement: code already allowed to run outside interception is not isolated from the operating-system account.
As an approver
When an action is pending, an approval request is created.
You can:
- Allow once. The action proceeds; the grant is consumed.
- Allow for a scope and time. The action and similar ones proceed until the grant expires.
- Deny. The action is refused.
As a developer
With the daemon enabled, pending actions are denied immediately with a ticket. Approve from your separate terminal, then retry the action. The adapter recomputes its normalized binding rather than trusting an agent’s ticket or description.
Deny-with-ticket
For a host that cannot wait, the adapter denies the action now and returns an approval ticket. If the agent retries after an approver approves, the retry consumes a single-use grant.
Expiry
An approval request that is never resolved expires as deny.
Read next
Audit, replay, and receipts
Note
Guide: draft. The source daemon can produce signed receipts, checkpoints, and encrypted replay inputs. The workflows below require a source build and have platform and conformance limits.
Audit log
The audit log is an append-only JSON Lines file of audit events linked by a hash chain. It is tamper-evident, not tamper-proof.
Verify
shiin verify checks that the current workspace’s log hash chain is intact.
To verify a signed decision receipt against a trusted public key, run shiin verify <receipt.json> <public.json>.
Signed evidence is written under .shiin/evidence/ when the daemon starts with SHIIN_EVIDENCE=1 after shiin key init has initialized the OS keyring.
Checkpoint verification also compares the signed head with the current local audit log, so it needs the matching history.
Replay
shiin replay <encrypted-replay.json> re-evaluates a recorded request against its captured policy and inputs.
It requires the same OS keyring that encrypted the replay data.
Pass a policy path as a second argument to compare the decision under that policy.
Receipts
Signed receipts attest that a decision was made. A receipt proves who signed a statement about a decision, not that the action’s real outcome was correct. Evidence support remains partial: Windows key storage, complete payload schemas and vectors, and Tier 1 offline verification have not been validated.
Read next
Integrations
Note
Project status: pre-alpha. Hook adapters, the Unix local API, and an early MCP proxy exist in source; host and MCP conformance validation remain incomplete.
Adapters connect a host — Claude Code, Codex CLI, Cursor, an MCP client, or a custom agent — to the Shiin engine. Each integration page documents the host’s interception mechanism, the enforcement level it can provide, and what it cannot see.
The adapter contract is the same for every host. Integration pages only describe the host-specific wiring, not the contract.
Available integrations
| Host | Enforcement level | Status |
|---|---|---|
| Claude Code | L1 cooperative | Implemented in source |
| Codex CLI | L1 cooperative | Implemented; live-host validation open |
| Cursor | L1 cooperative | Implemented; live-host validation open |
| MCP proxy | L2 mediated | Partial; one downstream stdio or Streamable HTTP server and tool calls |
| Custom agents | L1/L2 via API | Partial; Unix API and Rust client exist |
| Aider in a sandbox | L1 cooperative | Planned v0.6 |
Writing a new integration
Third parties can integrate any host that exposes a pre-execution hook or that can route tool calls through the MCP proxy. See the adapter contract and local API specifications.
Read next
Claude Code
Note
Guide: draft. The source build implements the current behavior below; remaining design work is marked explicitly.
Enforcement level
L1 Cooperative.
The host honors the decision through its hook mechanism, which can be disabled with --bare or disableAllHooks.
Interception
Shiin uses Claude Code’s PreToolUse hook.
The hook receives the tool payload and returns allow, deny, or ask.
What it cannot see
- Actions that do not pass through the hook.
- Code run by an allowed command.
Setup
- Install Shiin.
- Run
shiin init. - Configure the host using the quickstart.
shiin hook claude-codereads one JSON payload from standard input; it is not an interactive setup command.
Read next
OpenAI Codex CLI
Note
Guide: draft. The adapter exists in a source build. Canonical payload field names were checked against official documentation on 2026-10-15. Live-host validation remains open; check the first decision in
shiin log.
Enforcement level: L1 cooperative, and weaker than Claude Code’s, for reasons below. See enforcement levels.
Configure the hook
Codex runs a PreToolUse hook and accepts explicit allow and deny responses
(verified against https://learn.chatgpt.com/docs/hooks#pretooluse on 2026-10-15). Put this in
.codex/hooks.json in the project, or in ~/.codex/hooks.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [{ "type": "command", "command": "shiin hook codex", "timeout": 10 }]
}
]
}
}
Codex requires a hook to be trusted before it runs. Run /hooks inside Codex and trust
this one, or it will not be consulted at all.
Pending decisions
The adapter maps pending to deny.
Codex documents permissionDecision: "ask" as unsupported: it reports a hook failure and continues the tool call
(verified against official hooks documentation on 2026-10-15).
A supported explicit denial is required to hold the action safely.
With the Unix daemon enabled, a pending response carries an approval ticket for separate operator resolution and retry.
Daemonless pending decisions remain denials without a ticket.
See Handling approvals.
What it maps
| Codex tool | Action type | Notes |
|---|---|---|
Bash, shell, local_shell | shell.exec | Classified like any command, so cat .env records the read it performs |
apply_patch | fs.write | The path is used when the payload names one |
Read | fs.read | |
| Anything else | x-codex.<tool> | No rule can match it, so it falls through to the default decision |
A patch whose payload names no readable path becomes an opaque effect because it still changes files.
The documented apply_patch payload carries patch text in tool_input.command; the current adapter does not parse that text into file effects.
This remains a classification limitation, independent of payload-field verification.
Canonical session_id, cwd, tool_name, and tool_input, including the command field for Bash and patches, were checked against official hooks documentation (verified 2026-10-15).
What this does not cover
- Hosted tools are not intercepted. Codex’s own web search does not reach a hook, so Shiin never sees it.
- Hooks are advisory. Codex’s documentation calls them “a useful guardrail, not a
complete enforcement boundary” (verified 2026-09-14), and
--dangerously-bypass-hook-trustexists. - Codex has its own sandbox and approval policy. Keep them on. Shiin decides whether an action should happen; a sandbox limits the damage when something is allowed and misbehaves. See using Shiin with sandboxes.
Verifying it works
$ shiin log
$ shiin doctor
Ask Codex to read a .env file. If the log shows no request.received event, the hook is
not reaching Shiin: check that it is trusted, and that shiin is on the path Codex uses.
If a decision looks wrong, the most likely cause is a payload field this adapter does not recognize. Please open an integration issue with the payload, secrets removed.
Read next
Cursor
Note
Guide: draft. The adapter exists in a source build and has not been tested against a running Cursor.
Enforcement level: L1 cooperative. See enforcement levels.
Configure the hooks
Cursor runs a hook per event rather than one hook for every tool, and reads back a
permission of allow, deny, or ask
(verified against https://cursor.com/docs/hooks on 2026-09-14). Put this in
.cursor/hooks.json:
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{ "command": "shiin hook cursor", "failClosed": true }
],
"beforeReadFile": [
{ "command": "shiin hook cursor", "failClosed": true }
],
"beforeMCPExecution": [
{ "command": "shiin hook cursor", "failClosed": true }
]
}
}
failClosed: true matters more here than anywhere else. Without it, Cursor allows the
action when a hook fails, which is the opposite of what you installed Shiin for.
What it maps
| Cursor event | Action type | Notes |
|---|---|---|
beforeShellExecution | shell.exec | Classified like any command, so a read it performs is recorded |
beforeReadFile | fs.read | |
beforeMCPExecution | x-cursor.mcp.<tool> | Opaque: a tool server describes its own behavior, and Shiin cannot verify it |
| Anything else | x-cursor.<event> | Opaque, so it falls through to a person or the default decision |
Shiin uses the first entry of workspace_roots as the workspace, falling back to cwd.
Known gaps
- Pending is denied by this adapter. Live-host approval behavior has not been verified, so Shiin returns
denyfor both enginedenyandpendingdecisions. The engine’s pending reason remains in the local audit log. With the Unix daemon enabled, the response includes an approval ticket for separate operator resolution and retry; daemonless operation provides no ticket. See Handling approvals. - Cloud agents skip the tool events, so an action taken there never reaches Shiin.
- MCP calls are opaque by design. Precise mapping of tool calls arrives with the MCP proxy source implementation, which sits in the configured data path.
Read next
The current Cursor hook documentation describes failClosed and the response fields used by this adapter (verified 2026-10-15).
Automated protocol checks do not establish behavior in a running Cursor installation.
MCP proxy
Note
Guide: draft. A source implementation connects to one downstream stdio or Streamable HTTP server and evaluates tool calls through the daemon. External MCP conformance, identity binding, and independent review remain open.
Enforcement level
L2 Mediated. Shiin sits in the data path between the client and each MCP server.
Interception
The shiin-mcp-proxy binary connects to one downstream server, negotiates MCP revision 2026-07-28 without legacy fallback, forwards tools/list, and sends each tools/call through the local daemon before forwarding it.
The upstream client always communicates with the proxy over stdio.
Configure SHIIN_DAEMON=1 or an absolute SHIIN_SOCKET.
For a downstream subprocess, use --workspace ABSOLUTE_PATH -- /ABSOLUTE/SERVER [ARG ...].
For a downstream HTTP server, use --workspace ABSOLUTE_PATH --http https://mcp.example/mcp.
These options are mutually exclusive.
The HTTP endpoint requires HTTPS except for literal loopback addresses; DNS names such as localhost do not enable the plaintext exception.
URLs containing credentials, query strings, or fragments are rejected.
Redirects, automatic HTTP retries, environment proxy discovery, and expired-session reconnection are disabled.
Connection setup is limited to ten seconds, with a five-second HTTP connection timeout and a sixty-second HTTP request timeout.
For a provisioned access token, append --bearer-env SHIIN_EXAMPLE_MCP_TOKEN and provide that variable through your operator-controlled credential setup.
Only the variable name goes in the command; keep token values out of arguments, workspace files, and agent input.
Bearer mode requires HTTPS even on loopback, rejects missing/invalid credentials before connecting, and marks the Authorization header sensitive so debug formatting does not reveal its value.
An optional --ca-file ABSOLUTE_PATH adds one operator-selected PEM trust root for a private HTTPS server; hostname and certificate verification remain enabled.
CA files are bounded to 64 KiB, and malformed or unavailable roots fail setup.
The provisioned-token path follows Bearer header usage and the MCP access-token requirements (verified 2026-10-16).
OAuth discovery, browser consent, token acquisition, and refresh remain planned; a provisioned token does not establish complete MCP authorization conformance.
The proxy maps each tool name to an mcp.tool.* action type, records the full tool invocation as a digest, and marks its effect opaque. It returns a caller-visible tool error, including an approval ticket when present, for deny, pending, or a daemon failure. Only allow reaches the downstream server. Other MCP methods are not proxied yet.
The target is the MCP specification revision 2026-07-28, checked on 2026-10-16. The current proxy does not use tool annotations to change its decision; annotation handling and conformance remain open.
The Rust integration test launches the actual proxy between an SDK client, a fixture server, and the Unix daemon service, with both downstream transports. It covers discovery, allow/deny/pending, argument forwarding, daemon unavailability, refusal to follow an HTTP redirect, and an authenticated HTTPS fixture with an explicit generated test trust root. Process tests also check that credential setup failures do not reveal token values. The test uses the official Rust MCP SDK (verified 2026-10-16); this coverage does not establish conformance against external reference servers. The transport design follows the MCP transport bindings (verified 2026-10-16).
What it cannot see
- Traffic not routed through the proxy.
- Tool annotations do not affect decisions in this implementation.
- Prompts, resources, and other MCP methods are not proxied yet.
Read next
Custom agents
Note
Guide: draft. The Unix local API and Rust client exist in source. Windows transport, attested agent identity, and independent validation remain open.
Enforcement level
L2 Mediated. The agent asks Shiin for decisions through the local API.
Interception
A custom agent built with shiin-client calls the local API over a Unix domain socket.
The API uses JSON-RPC 2.0. The client returns an error for an unavailable daemon or malformed response; the caller must stop the action unless Shiin returns allow.
#![allow(unused)]
fn main() {
use shiin_client::Client;
use shiin_schema::DecisionValue;
let client = Client::from_environment()?;
let decision = client.evaluate(&request).await?;
if decision.decision != DecisionValue::Allow {
return Err("Shiin did not allow the action".into());
}
}
What it cannot see
The client/service integration tests exercise real Unix RPCs for policy validation, allow/deny/pending, unattended mode, stale snapshots, operator proof, single-use and scoped grants, revocation, and built-in denial precedence. They verify grant transition events and that operator credentials do not enter the audit log. This source conformance coverage does not establish interception or identity binding in an external agent.
- Agent identity assurance: the daemon authenticates the local Unix user but does not yet bind that identity to the request’s agent id.
- Windows named-pipe transport.
Read next
Aider in a sandbox
Note
Guide: draft. Shiin’s Unix command wrapper exists in source. Automatic routing from a running Aider host has not been verified.
Enforcement level
L1 Cooperative through a shell wrapper.
Setup
The source build provides an explicit command wrapper:
$ shiin exec --session EXAMPLE-session -- /absolute/path/to/EXAMPLE-program EXAMPLE-argument
The wrapper evaluates shell.exec with conservative opaque effects and binds the exact executable and argument list.
Only allow starts the executable, after the audit record and task counters have been written.
When a daemon is configured, pending returns a ticket; retry the same arguments and session after approval.
Without the daemon, pending returns exit code 2 and does not execute the command.
This command does not automatically intercept a host’s shell calls. Verify the host routes every intended command through the wrapper before relying on it, and use a sandbox for the code an allowed command runs.
What it cannot see
- Commands typed directly at the shell without the wrapper.
- File edits, network calls, and child processes that do not pass through the wrapper.
Read next
CLI
Note
Reference: draft. The commands marked “works today” run from a source build. Published alpha archives are described in installation; registry packages are not published.
$ shiin <command>
Commands
| Command | What it does | Status |
|---|---|---|
shiin init | Writes a starter policy to .shiin/policy.toml, refusing to overwrite one | works today |
shiin policy validate [path] | Parses and compiles a policy, reporting rule and signal counts or a diagnostic | works today |
shiin check <request.json> | Decides one action request and prints the decision and reason code | works today |
shiin explain <request.json> | Decides, and also prints the stage that decided and the rules that matched | works today |
shiin hook claude-code | Serves one Claude Code PreToolUse hook call, reading the payload on standard input | works today |
shiin log [count] | Prints recent audit events, most recent last | works today |
shiin verify | Checks the audit log’s hash chain | works today |
shiin verify <evidence.json> <public.json> | Verifies a signed receipt or checkpoint; checkpoints also require matching local audit history | implemented in source; platform validation incomplete |
shiin replay <encrypted-replay.json> [policy.toml] | Replays captured inputs with the original or alternate policy | implemented in source; requires the originating OS keyring |
shiin key init, shiin key rotate, shiin key public | Initializes or rotates the evidence key and prints its public key | implemented in source on supported keyring platforms |
shiin doctor | Reports this workspace’s policy, audit log, mode, and enforcement level | works today |
shiin policy test [dir] | Runs policy cases from .shiin/tests, using the same runner as the conformance corpus | works today |
shiin task start | show | end | Declares what the agent is working on, shows it with its usage, or ends it | works today |
shiin hook codex, shiin hook cursor | The same hook service for those hosts | works today, untested against a running host |
shiin exec --session ID -- /ABSOLUTE/PROGRAM [ARG ...] | Authorizes and audits one command, then starts it only after allow | source implementation on Unix; host routing remains unvalidated |
shiin pending, shiin approve | Lists and resolves approval requests using a separate operator credential and terminal confirmation | implemented in source on Unix; validation in progress |
shiin grants, shiin revoke | Inspects and revokes grants or unresolved tickets | implemented in source on Unix; validation in progress |
shiind --operator | Runs the local approval daemon and displays its in-memory operator credential in a dedicated terminal | implemented in source on Unix; validation in progress |
shiin policy test exits non-zero when any case fails, so it belongs in whatever runs your checks.
Exit codes
shiin check and shiin explain report the decision as an exit code, so a script can branch on it.
| Code | Meaning |
|---|---|
| 0 | The action is allowed |
| 1 | The action is denied, or the command failed |
| 2 | The action is pending, so a person needs to decide |
shiin hook claude-code exits 0 whenever it produced a response, including a denial,
because the host reads the decision from the response rather than from the exit code.
It exits 2 only when it could not produce a response at all, which the host treats as blocking.
shiin exec returns 1 on denial, failure, or an unavailable daemon, and 2 for pending.
After allow it returns the child’s exit code when that code is in 0–255, or 1 when no such code is available.
The executable path must be absolute, and the session id must stay the same when retrying an approved ticket.
Arguments are passed directly to the executable; the wrapper does not interpret them through a shell.
Files it uses
See configuration. In short, policy is read from .shiin/policy.toml
and events are appended to .shiin/audit.jsonl.
Read next
Configuration
Note
Reference: draft. This describes what a source build reads today, and marks what is planned.
Files
| File | Purpose | Status |
|---|---|---|
.shiin/policy.toml | The policy for this workspace | works today |
.shiin/audit.jsonl | The append-only audit log | works today |
.shiin/tests/ | Policy cases run by shiin policy test | works today |
.shiin/config.toml | Settings that are not policy, such as the audit log’s location | planned, v0.3 |
.shiin/tasks/ | The active task declaration and its counters | works today |
Paths are relative to the workspace, which is the directory the host reports as its
working directory. Under ADR-0016 everything
under .shiin/ is a built-in protected resource, so no policy can let an agent change it.
The Unix daemon uses a private per-user runtime directory. Its default socket and approval state, plus workspace evidence paths, are listed in State and storage.
Environment variables
| Variable | Description | Status |
|---|---|---|
SHIIN_WORKSPACE | Overrides the workspace root. Useful when a host reports no working directory | works today |
SHIIN_UNATTENDED | If set to any value, a pending decision becomes a denial, because no approver can be reached | works today |
CI | Treated the same as SHIIN_UNATTENDED, since continuous integration has no approver | works today |
SHIIN_DAEMON | Set to 1 to require local daemon evaluation; an unavailable configured service denies the action | source implementation on Unix |
SHIIN_SOCKET | Absolute Unix socket override; setting it requires the daemon even without SHIIN_DAEMON=1 | source implementation on Unix |
SHIIN_EVIDENCE | Set to 1 when starting the daemon to enable signed receipts/checkpoints and encrypted replay records; unavailable keys prevent startup | source implementation; OS key-store validation remains open |
SHIIN_LOG | Log level filter | planned, v0.2 |
Options
Global options such as --config and --state are planned for v0.2, once there is
configuration beyond policy to point at. Today the workspace and the environment
variables above are the only inputs.
Read next
Error codes
Note
Reference: draft. Every code marked “emitted” is produced by the current source build and is checked against the source by
cargo xtask docs-check.
A code is stable: it is never renumbered or reused, so a script may match on it. A code names an operational problem, never a decision. A denial is not an error, and it carries a reason code instead.
Policy errors
| Code | Meaning | Status |
|---|---|---|
SHIIN-POL-0001 | The policy file could not be read. | emitted |
SHIIN-POL-0002 | The policy file is not valid TOML. | emitted |
SHIIN-POL-0003 | The policy uses an unknown key. | emitted |
SHIIN-POL-0004 | A rule has a duplicate id. | emitted |
SHIIN-POL-0005 | A rule has an invalid glob pattern. | emitted |
SHIIN-POL-0006 | A rule could not be compiled for the policy engine. | emitted |
Every one of these results in a denial, because a policy that cannot be understood cannot authorize anything (ADR-0013).
Classification errors
| Code | Meaning | Status |
|---|---|---|
SHIIN-CLS-0001 | A path could not be canonicalized. | reserved |
Canonicalization currently cannot fail: a path that does not exist is still resolved, so the classifier returns a best effort rather than an error. The code is reserved for the platform-specific cases that Windows support will introduce in v0.6.
Audit errors
| Code | Meaning | Status |
|---|---|---|
SHIIN-AUD-0001 | The audit log could not be written. | emitted |
SHIIN-AUD-0002 | The audit log’s hash chain is broken, or a line is malformed. | emitted |
SHIIN-AUD-0001 denies the action it was recording, because an action that cannot be
recorded is not allowed to proceed (ADR-0030).
General errors
| Code | Meaning | Status |
|---|---|---|
SHIIN-GEN-0001 | An internal error occurred, including a request that could not be evaluated. | emitted |
SHIIN-GEN-0002 | The operation timed out. | reserved |
SHIIN-GEN-0002 remains reserved; current hook and daemon timeout paths return a fixed denial or transport error rather than emitting that stable code.
Hook evaluation has a two-second deadline, and a local daemon exchange has a 1.5-second deadline.
Neither timeout falls back to an allow decision.
Read next
Platform support
Note
Reference: draft. The first alpha provides Linux and macOS CLI and Unix-daemon archives; Windows remains source-only. See the limitations below before relying on path matching on Windows.
Tiers
| Tier | Platforms | Support |
|---|---|---|
| Tier 1 | Linux x86_64, macOS 13+ (x86_64 and arm64) | Tested in CI. |
| Tier 2 | Windows 10+ (x86_64) | Builds; promoted toward Tier 1 in v0.6. |
| Tier 3 | Other architectures | Not guaranteed to build. |
Before 1.0
The MSRV is pinned stable minus 2 minor versions. After 1.0, library crates track latest stable minus 6 minor versions.
What Tier 2 runs today
Windows is Tier 2 because path canonicalization there is not specified yet
(ADR-0019 defers the \\?\ prefix, 8.3 short names,
junctions, and case rules to v0.6).
Continuous integration therefore runs the whole test suite on Windows except the tests whose
expectation depends on a policy glob matching a canonicalized path. Those are marked
#[cfg(unix)] in crates/shiin-cli/tests/hook.rs. What still runs on Windows is the
fail-closed behaviour that does not involve paths: an unparsable policy, an unknown policy
key, an unwritable audit log, a malformed or empty payload, a tampered audit log, an opaque
command, and the command-line tooling.
This is a real gap, not a formality: until those rules exist, a policy on Windows cannot be trusted to match the paths an operator wrote. Promotion to Tier 1 means specifying them, removing the gates, and seeing the full suite pass.
Read next
About the specifications
Note
Project status: pre-alpha. Core contracts have source implementations and conformance tests; approval, evidence, and local API implementations are partial and still need milestone validation. The first experimental CLI and Unix-daemon release is available; contracts remain subject to change before 1.0. Tests check implemented wire values against the schemas.
The specifications define the contracts that make Shiin interchangeable: the action request, the decision, the evaluation pipeline, and the approval protocol, along with JSON Schemas and test vectors that other tools can implement.
Each page distinguishes implemented behavior from remaining design work. The roadmap lists the milestone that delivers each specification’s implementation.
Status and lifecycle
| Status | Meaning |
|---|---|
draft | The specification is under active revision. Field names, IDs, and schemas may change. |
proposed | The specification has stabilized enough to send to implementers for review. |
accepted | The content is settled and may be implemented against. Before 1.0 a correction may still raise the draft minor version, recorded in the revision history; after 1.0 a change requires a superseding version. |
superseded | A newer version replaces this one. Links to the replacement. |
The status field in each page’s metadata reflects where the specification is in this lifecycle, not whether code exists.
Versioning
Each specification states its version near the top, in the form shiin.<name>/v<major>-draft.<minor>:
- The major version is part of the schema
$idURN (for example,urn:shiin:schema:action-request:v1). - A draft minor number (
.1,.2, …) increments for any change that is not a new major version. - A new major version is issued when a change is not backward compatible.
- The version string in the specification text, the
$idin the schema, and the schema file name must all agree.
Requirement IDs
Every normative requirement in a specification starts with a bold, bracketed requirement ID, for example **[AR-001]**.
- IDs are unique across all documentation and are defined only in their owning
kind=specpage. - The prefix indicates which specification owns the requirement:
| Prefix | Specification |
|---|---|
AT | Action types |
AR | Action request |
DEC | Decision |
EVAL | Evaluation |
ID | Identity |
POL | Policy model |
SYN | Policy syntax |
INT | Intent binding |
RISK | Risk signals |
APR | Approval protocol |
AUD | Audit events |
RCPT | Receipts |
ADP | Adapter contract |
API | Local API |
SEC | Security model |
Schemas and test vectors
- JSON Schemas live in
schemas/and are named<name>.v<major>.schema.json. - Shared definitions live in
common.v1.schema.jsonand are referenced by$id. - Test vectors live in
test-vectors/and are validated againsttest-vector.v1.schema.json.
Specification list
| Specification | Milestone | Schema |
|---|---|---|
| Action types | v0.1 | — |
| Action request | v0.1 | schema |
| Decision | v0.1 | schema |
| Evaluation | v0.1 | schema |
| Identity | v0.1 | — |
| Policy model | v0.1 | — |
| Policy syntax | v0.1 | schema |
| Intent binding | v0.2 | schema |
| Risk signals | v0.1 | schema |
| Approval protocol | v0.3 | schema |
| Audit events | v0.1 | schema |
| Receipts | v0.4 | receipt, checkpoint, encrypted replay |
| Adapter contract | v0.1 | — |
| Local API | v0.5 | schema |
| Test vectors | m0 | schema |
Reading the specifications
- Start with How Shiin works for the end-to-end flow.
- Read the action request and decision for the core messages.
- Read evaluation for how decisions are combined.
- Read the adapter contract for integration.
- Consult the test vectors for concrete examples.
Related reading
Action types
Note
Specification: draft. Implemented in part in the source build. Remaining requirements and external validation are not complete.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
An action type is a dotted identifier that classifies an action. Policies refer to action types, never to host tool names.
Conventions
- Action types use lowercase letters, digits, and dots.
- Each top-level namespace groups related effects.
- New namespaces go through the RFC process and produce an ADR.
Namespace fs
Filesystem effects.
| Action type | Meaning |
|---|---|
fs.read | Read a file or directory entry. |
fs.list | List directory entries or search for matching paths. |
fs.write | Write or overwrite a file. |
fs.create | Create a new file, directory, or symlink. |
fs.delete | Delete a file, directory, or symlink. |
fs.move | Move or rename a file or directory. |
fs.chmod | Change file permissions or ownership. |
[AT-001] fs.read MUST cover reading a file through any host mechanism, including a file-read tool and a shell command such as cat.
[AT-002] fs.write, fs.create, fs.delete, fs.move, and fs.chmod MUST each cover the corresponding operation through any host mechanism.
Namespace shell
Effects of running a program through a shell.
| Action type | Meaning |
|---|---|
shell.exec | Execute a program or shell command. |
[AT-003] shell.exec MUST cover any program the agent starts through a shell, a command tool, or a terminal.
Namespace network
Network effects.
| Action type | Meaning |
|---|---|
net.connect | Open a connection to a remote host. |
net.fetch | Fetch a URL over HTTP or HTTPS. |
[AT-004] net.connect MUST cover any outbound TCP or UDP connection.
[AT-005] net.fetch MUST cover any HTTP or HTTPS request.
Namespace db
Database effects.
| Action type | Meaning |
|---|---|
db.query | Run a read query against a database. |
db.mutate | Run a write query against a database. |
db.schema_change | Change a database schema, including migrations. |
[AT-006] db.query, db.mutate, and db.schema_change MUST cover the corresponding operation through any host mechanism, including SQL clients and ORM tools.
Unknown actions
[AT-007] An action that the adapter cannot classify MUST be recorded with an opaque effect rather than an invented action type.
Classifier status
shiin-classify exposes a narrow SQL classifier for simple statements and a migration classifier that marks migration contents opaque.
Complex or unsupported SQL remains opaque.
No host adapter invokes these functions yet, so they do not provide database interception.
Revision history
- v1-draft.1 (2026-10-01): Initial draft.
Action request
Note
Specification: accepted. Implemented in part in the source build. Remaining requirements and external validation are not complete.
Version:
shiin.action-request/v1-draft.1.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
An action request is the normalized, host-independent description of one proposed action. An adapter builds it and the engine evaluates it.
Fields
[AR-001] An action request MUST be a JSON object with the following top-level fields:
| Field | Type | Required | Description |
|---|---|---|---|
schema | string | yes | The schema identifier, shiin.action-request/v1-draft.1. |
id | string (UUID) | yes | A unique identifier for this request. |
session | string | yes | The host’s session identifier. |
agent | object | yes | The agent identity. |
action_type | string | yes | An action type, such as fs.write. |
effects | array of effect | yes | The effects of the action. |
task | string or null | no | The active task declaration id, or null. |
policy_snapshot | string | yes | The digest of the policy snapshot used. |
host | object | yes | Host-specific context. |
[AR-002] id MUST be a version 4 UUID.
Agent identity
[AR-003] The agent object MUST contain:
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | The agent identifier. |
assurance | string | yes | The assurance level: asserted, attested-local, attested-os-user, or attested-crypto. |
Effects
[AR-004] Each element of effects MUST be a JSON object with:
| Field | Type | Required | Description |
|---|---|---|---|
kind | string | yes | A free-form label for the effect, such as path or host. |
value | string | yes | The concrete value, such as a canonical path or host name. |
confidence | string | yes | exact, inferred, or opaque. |
[AR-005] An effect with confidence: "opaque" MUST indicate that Shiin cannot determine the effect before the action runs.
[AR-006] An effect with confidence: "exact" MUST be a value Shiin has verified, such as a canonicalized path.
[AR-007] An effect with confidence: "inferred" MUST be a value Shiin believes but has not verified.
Host context
[AR-008] An action request MUST carry a workspace object with an absolute, canonical root.
[AR-009] Policy globs written relative to the workspace, such as ./src/**, MUST be resolved against workspace.root before matching, so that a relative pattern can never match a path outside the workspace.
[AR-010] The host object MUST contain:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | The host name, such as claude-code. |
tool | string | no | The host’s tool name, if applicable. |
payload | object or null | no | The raw host payload, redacted of secrets. |
Examples
{
"schema": "shiin.action-request/v1-draft.1",
"id": "0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0b",
"session": "sess-001",
"agent": {
"id": "agent-001",
"assurance": "asserted"
},
"action_type": "fs.write",
"effects": [
{
"kind": "path",
"value": "/home/dev/project/src/login.rs",
"confidence": "exact"
}
],
"task": null,
"workspace": {
"root": "/home/dev/project",
"vcs": "git"
},
"policy_snapshot": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"host": {
"name": "claude-code",
"tool": "Write",
"payload": null
}
}
{
"schema": "shiin.action-request/v1-draft.1",
"id": "not-a-uuid",
"session": "sess-001",
"agent": {
"id": "agent-001",
"assurance": "asserted"
},
"action_type": "fs.write",
"effects": [],
"task": null,
"policy_snapshot": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"host": {
"name": "claude-code"
}
}
Revision history
- Accepted on 2026-10-12. Accepted: schemas and every example validate, and the ADR-0014 spike evaluated requests in this shape against the whole corpus.
- v1-draft.2 (2026-10-12): Added the required
workspaceobject, so workspace-relative policy globs can be resolved (AR-008, AR-009). Renumbered the host requirement to AR-010. - v1-draft.1 (2026-10-01): Initial draft.
Decision
Note
Specification: accepted. Implemented in part in the source build. Remaining requirements and external validation are not complete.
Version:
shiin.decision/v1-draft.1.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
A decision is the engine’s answer to an action request.
Decision values
[DEC-001] A decision MUST be one of allow, deny, or pending.
[DEC-002] allow means the action may proceed.
[DEC-003] deny means the action must not proceed.
[DEC-004] pending means the action must wait for an approver.
Reason codes
[DEC-005] Every decision MUST carry a stable, machine-readable reason code.
[DEC-006] Reason codes MUST follow the form <decision>.<stage> or <decision>.<signal>, such as deny.policy or pending.opaque.
[DEC-007] The reason codes defined by this specification are:
| Code | Decision | Meaning |
|---|---|---|
allow.policy | allow | A policy rule allowed the action. |
allow.grant | allow | A grant covered the action. |
deny.identity | deny | The agent identity is not acceptable. |
deny.capability | deny | The agent lacks a capability for this action class. |
deny.policy | deny | A policy rule denied the action. |
deny.intent | deny | The action does not fit the active task. |
deny.risk | deny | A risk signal denied the action. |
deny.failure | deny | A failure produced a fail-closed deny. |
deny.builtin | deny | A built-in protected resource was touched. |
deny.unattended | deny | A pending result could not be resolved because no approver is available. |
deny.default | deny | No rule matched, so the default decision applied. |
pending.identity | pending | The agent identity needs confirmation. |
pending.capability | pending | The agent capability needs confirmation. |
pending.intent | pending | The action needs confirmation against the task. |
pending.risk | pending | A risk signal escalated the action. |
pending.opaque | pending | An opaque effect needs approval. |
pending.grantable | pending | The action needs a grant or approval. |
Agent-facing message
[DEC-008] Every decision MUST carry a message intended for the agent.
[DEC-009] The message MUST NOT reveal policy structure, error codes, paths, or other internal detail that could help an agent probe for a bypass.
Decision object
[DEC-010] A decision MUST be a JSON object with:
| Field | Type | Required | Description |
|---|---|---|---|
schema | string | yes | shiin.decision/v1-draft.1. |
request_id | string (UUID) | yes | The id of the action request. |
decision | string | yes | allow, deny, or pending. |
reason | string | yes | The reason code. |
message | string | yes | The agent-facing message. |
trace | array | no | The trace of stage results, in explain mode. |
approval | object or null | no | An approval request, when pending. |
Examples
{
"schema": "shiin.decision/v1-draft.1",
"request_id": "0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0b",
"decision": "deny",
"reason": "deny.policy",
"message": "Reading secret files is not allowed.",
"trace": null,
"approval": null
}
{
"schema": "shiin.decision/v1-draft.1",
"request_id": "not-a-uuid",
"decision": "allow",
"reason": "allow.policy",
"message": "ok",
"trace": null,
"approval": null
}
Revision history
- Accepted on 2026-10-12. Accepted: the reason-code registry is now enforced by
cargo xtask docs-check, and the spike produced every code the corpus expects. - v1-draft.1 (2026-10-01): Initial draft.
Evaluation
Note
Specification: accepted. Implemented in part in the source build. Remaining requirements and external validation are not complete.
Version:
shiin.evaluation/v1-draft.1.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
The engine evaluates an action request against a policy snapshot and produces a decision. The engine performs no I/O and is deterministic: the same inputs always produce the same decision (ADR-0009, ADR-0010).
The identity stage currently returns pass; the capability gate requires explicit action/effect-class grants, and Unix approval grants are evaluated by the daemon path in source.
Policy, task constraints, risk escalation, unattended denials, and five-stage traces exist in source. Host and cross-platform validation remain open.
Stages
[EVAL-001] The engine MUST run exactly five stages in this fixed order: identity, capability, policy, intent, and risk (ADR-0012).
[EVAL-002] Each stage returns one of allow, deny, pending, or pass.
[EVAL-003] A stage that returns allow is a policy stage; only the policy stage may produce allow.
[EVAL-004] The identity, capability, intent, and risk stages are gates: they may return deny, pending, or pass, but never allow.
Inputs
[EVAL-014] The engine’s inputs MUST be exactly the action request, the policy snapshot, the active task declaration if one exists, the evaluation mode, and the current time.
[EVAL-015] The engine MUST NOT read any other input, so that the same inputs always produce the same decision (ADR-0010).
[EVAL-016] The evaluation mode MUST be either attended, meaning an approver can be reached, or unattended, meaning none can.
[EVAL-017] In unattended mode, a decision that would be pending MUST instead be deny with reason code deny.unattended.
Combining
[EVAL-005] If any stage returns deny, the decision is deny (ADR-0011).
[EVAL-006] Otherwise, if any stage returns pending, the decision is pending, unless a matching grant already covers the action.
[EVAL-007] Otherwise, if the policy stage allows the action, the decision is allow.
[EVAL-008] Otherwise, the decision is the default decision, which is deny.
Fail-closed
[EVAL-009] Any failure inside evaluation MUST produce deny with reason code deny.failure (ADR-0013).
Trace
[EVAL-010] In explain mode, the engine MUST return a trace recording each stage’s result.
[EVAL-011] A trace MUST include the stage name, the result, and the reason code or rule id that produced it.
Policy snapshot
[EVAL-012] The engine MUST evaluate against a single immutable policy snapshot identified by a digest.
[EVAL-013] The snapshot digest MUST be recorded in the action request and the audit event.
Examples
{
"schema": "shiin.evaluation/v1-draft.1",
"request_id": "0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0b",
"decision": "allow",
"reason": "allow.policy",
"trace": [
{ "stage": "identity", "result": "pass", "reason": "identity.asserted" },
{ "stage": "capability", "result": "pass", "reason": "capability.fs_write" },
{ "stage": "policy", "result": "allow", "reason": "rule:edit-source" },
{ "stage": "intent", "result": "pass", "reason": "intent.no_task" },
{ "stage": "risk", "result": "pass", "reason": "risk.none" }
]
}
Revision history
- Accepted on 2026-10-12. Accepted: the ADR-0014 spike implements this pipeline and reproduces all 8 test vectors.
- v1-draft.2 (2026-10-12): Stated the engine’s inputs, and defined attended and unattended evaluation modes (EVAL-014 to EVAL-017).
- v1-draft.1 (2026-10-01): Initial draft.
Identity
Note
Specification: draft. Implemented in part in the source build. Remaining requirements and external validation are not complete.
Version:
shiin.identity/v1-draft.1.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
The identity stage answers: do we know which agent is asking, and how sure are we?
Current hook adapters supply asserted identities. The engine’s identity stage is a pass-through; unknown-identity and assurance-based enforcement below are not implemented.
Assurance levels
[ID-001] An agent identity MUST carry an assurance level.
[ID-002] The assurance levels, weakest first, are:
| Level | Meaning |
|---|---|
asserted | The host reports the agent id. Nothing verifies it. |
attested-local | The agent id is attested by a local trust root. |
attested-os-user | The agent id is attested by the operating system user. |
attested-crypto | The agent id is attested cryptographically. Reserved for the future. |
Behavior
[ID-003] The identity stage is a gate: it may return deny, pending, or pass, but never allow.
[ID-004] An unknown agent identity MUST produce deny with reason code deny.identity.
[ID-005] An identity whose assurance is too low for the action class MUST produce pending with reason code pending.identity.
[ID-006] An identity that is accepted MUST produce pass.
Untrusted input
[ID-007] Agent-supplied identity claims, including self-reported ids and tool annotations, can make a decision stricter but never more permissive.
Revision history
- v1-draft.1 (2026-10-01): Initial draft.
Policy model
Note
Specification: accepted. Implemented in part in the source build. Remaining requirements and external validation are not complete.
Version:
shiin.policy-model/v1-draft.2.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
A policy is the complete set of rules, capabilities, defaults, and risk configuration that applies to an evaluation (ADR-0015).
The source build implements explicit capabilities, rules, defaults, built-in protections, and configured risk signals.
Separately loaded trusted layers and complete snapshot persistence are not implemented.
The Unix approval service applies grants after fresh evaluation; a grant cannot override a denial.
The CLI currently loads one operator-controlled .shiin/policy.toml file; it does not implement workspace trust-by-hash.
Rules
[POL-001] A rule MUST match an action request and produce allow, deny, or pending.
[POL-002] A rule MUST have a unique id within its policy layer.
[POL-003] A rule MUST specify an effect: allow, deny, or pending.
[POL-004] A rule MAY match on action_type, resources, commands, agents, sessions, and tasks.
[POL-005] A rule that matches no action request MUST have no effect.
Capabilities
[POL-006] A capability is a coarse permission for an agent to attempt a whole class of actions at all.
[POL-007] A capability MUST be granted or denied explicitly; there is no implicit capability.
The capability gate checks the request’s action class and every distinct effect action class.
An absent grant or any matching capability refusal produces deny.capability; a capability grant only passes this gate and never authorizes an action by itself.
Agent selectors use the adapter’s asserted identifier; they do not establish identity assurance.
The snapshot format is now shiin.policy/v1-draft.2.
Older snapshots and replay-engine versions are refused rather than silently receiving implicit capabilities.
Default decision
[POL-008] The default decision is deny.
[POL-009] The default decision MAY be configured to pending, but it can never be allow.
Built-in protected resources
[POL-010] A built-in protected resource MUST never be made allowable for agents by policy (ADR-0016).
[POL-011] Built-in protected resources include Shiin’s own state, keys, and audit log, host hook configuration, and approval commands.
Policy layers
[POL-012] Policy is drawn from layers, in order: built-in, user, and workspace (ADR-0015).
[POL-013] A workspace layer can restrict a user layer only; it can never make a deny into an allow unless the operator trusts it by hash.
[POL-014] An organization layer is reserved for the future.
Policy snapshot
[POL-015] A policy snapshot is the immutable input to one evaluation: compiled policy, grants, active tasks, and counters.
[POL-016] A policy snapshot MUST be identified by a digest recorded in the action request and the audit event.
Examples
{
"schema": "shiin.policy/v1-draft.2",
"layers": [
{
"name": "built-in",
"digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"rules": [
{
"id": "protect-audit-log",
"effect": "deny",
"action_types": ["fs.write"],
"resources": [".shiin/audit.jsonl"],
"reason": "The audit log is a protected resource."
}
]
}
],
"default": "deny"
}
Revision history
- v1-draft.2 (2026-10-16): Added explicit capability declarations, default-deny capability enforcement, and session/task rule selectors; prior snapshots require migration by the operator.
- Accepted on 2026-10-12. Accepted: the spike compiled policies in this model to Cedar and reproduced every expected decision.
- v1-draft.1 (2026-10-01): Initial draft.
Policy syntax
Note
Specification: draft. The source build implements the format below. Capabilities and session/task rule matchers are implemented in source; identity assurance and separately trusted layers remain open.
Version:
shiin.policy-syntax/v1-draft.3.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
Policy is written in TOML and compiled to Cedar (ADR-0014). This page defines the TOML front-end.
Top level
[SYN-001] A policy file MUST be a TOML document with top-level version = 1.
[SYN-002] [defaults].decision MUST be deny or pending, and MUST default to deny when the table is omitted.
[SYN-003] A policy file MAY contain a top-level capabilities table.
The table maps capability identifiers to declarations; each declaration’s id equals its table key.
Omitting the table grants no action classes.
Rules
[SYN-004] Rules MUST be array elements of a top-level rule array, written as [[rule]] in TOML.
[SYN-005] Each rule MUST be a table with id and effect keys.
[SYN-006] id MUST be a unique string within the file.
[SYN-007] effect MUST be allow, deny, or pending.
[SYN-008] A rule MAY match on action_types, resources, commands, agents, sessions, and tasks.
Session and task selectors are exact identifier lists. Every nonempty selector must match; a task selector does not match an absent active task. The daemon replaces caller-supplied task claims with its active workspace task before evaluating these selectors.
[SYN-009] Resource globs beginning with ./ MUST resolve against the canonical workspace root.
Absolute globs and operator-home globs beginning with ~/ are also supported.
[SYN-010] commands MUST be exact command strings or glob patterns.
Capabilities
[SYN-011] A capability MUST be a table with id and either allow or deny.
[SYN-012] A capability MAY match on action_types and agents.
The allow or deny value is exactly true; false, absent, or simultaneous effects are rejected.
action_types is a nonempty list using exact classes or a trailing * wildcard.
Every matching denial wins, and every request/effect class needs an explicit matching grant.
Empty agent selectors apply to every asserted identifier; they provide no attestation.
Risk configuration
[SYN-013] Risk signals are configured with a top-level risk table.
[SYN-014] Each signal MAY be set to allow, deny, or pending.
allow means the risk gate passes; it never authorizes an action.
A signal may instead be a table with decision and an optional positive threshold.
The bulk_delete default threshold is 20.
[SYN-015] Unknown policy keys, unknown risk signal names, and unknown detailed risk settings MUST be rejected.
Example
{
"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/**"]
},
{
"id": "no-secrets",
"effect": "deny",
"action_types": ["fs.read"],
"resources": ["**/.env", "**/.env.*", "~/.ssh/**"],
"reason": "Secret files are not readable by agents."
}
],
"risk": {
"bulk_delete": { "decision": "pending", "threshold": 20 },
"secret_path": "deny"
}
}
Revision history
- v1-draft.3 (2026-10-16): Implemented explicit capability tables and session/task selectors, including validation and denial when a class lacks a grant.
- v1-draft.2 (2026-10-15): Reconciled the draft with the implemented TOML front-end and its JSON representation; documented unsupported keys and fail-closed risk configuration.
- v1-draft.1 (2026-10-01): Initial draft.
Intent binding
Note
Specification: draft. Implemented in part in the source build. Remaining requirements and external validation are not complete.
Version:
shiin.intent-binding/v1-draft.2.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
Intent binding checks an action against the constraints of the active task declaration (ADR-0020).
Task declaration
[INT-001] A task declaration MUST be a structured, human-confirmed statement with a description, constraints, and an expiry.
[INT-002] A task declaration MUST have a unique id.
[INT-003] The description is recorded but never evaluated.
Constraints
[INT-004] The constraint set is closed. Only these kinds are allowed:
| Constraint | Meaning |
|---|---|
scope.paths | Paths the agent may touch. |
forbid.action_types | Action types the agent may not perform. |
allow.hosts | Hosts the agent may contact. |
limits | Counters for actions such as deletions. |
expires_at | When the task ends. |
require_pending_for | Action types that always need approval. |
[INT-005] A constraint MUST be machine-checkable.
Behavior
[INT-006] The intent stage is a gate: it may return deny, pending, or pass, but never allow.
[INT-007] An action outside scope.paths MUST produce deny with reason code deny.intent.
[INT-008] An action in forbid.action_types MUST produce deny with reason code deny.intent.
[INT-009] An action exceeding a limits counter MUST produce deny with reason code deny.intent.
[INT-010] An action in require_pending_for MUST produce pending with reason code pending.intent.
[INT-011] When no task is active, the intent stage MUST produce pass.
Expiry
[INT-012] An expired task MUST no longer constrain actions.
[INT-013] Grants tied to an expired task MUST end.
Example
{
"schema": "shiin.intent-binding/v1-draft.1",
"task": {
"id": "task-001",
"description": "Fix the login bug",
"scope": {
"paths": ["./src/auth/**", "./tests/auth/**"]
},
"forbid": {
"action_types": ["db.schema_change"]
},
"expires_at": "2026-09-14T18:00:00Z"
},
"action": {
"action_type": "fs.write",
"effects": [
{ "kind": "path", "value": "/home/dev/project/migrations/0042.sql", "confidence": "exact" }
]
},
"result": "deny",
"reason": "deny.intent"
}
Evaluation plan
The first evaluation uses every published conformance vector that carries a task declaration. Each case is evaluated with its recorded task, counters, mode, and time, then evaluated again with the task removed. The report records the expected and actual decision/reason, changes from the policy-only baseline, false allows against non-allow labels, and false denials against allow labels. Any disagreement with a labelled case fails the command.
$ cargo xtask intent-evaluation
The documentation CI job runs this measurement and keeps its report in the job log. The 2026-10-16 hosted measurement matched all nine task cases, including their reason codes. Eight outcomes changed from the policy-only baseline; the corpus recorded zero false allows among eight non-allow labels and zero false denials among one allow label. The small specification corpus establishes reproducible behavior on the declared constraints, not real-world false-positive or false-negative rates. The next evaluation requires independently labelled task/action sequences, named host versions, interception results, and operator review of unnecessary holds and missed task violations. Task declarations remain experimental until that broader evaluation is available.
Revision history
- v1-draft.2 (2026-10-12): The schema now carries every constraint kind the closed set names.
allow.hosts,limits, andrequire_pending_forwere described in the text but missing from the schema, so a task that used them failed validation. - v1-draft.1 (2026-10-01): Initial draft.
Risk signals
Note
Specification: draft. Implemented in part in the source build. Remaining requirements and external validation are not complete.
Version:
shiin.risk-signals/v1-draft.1.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
A risk signal is a discrete, deterministic property of an action request that can escalate a decision (ADR-0021). Shiin does not use numeric risk scores.
Behavior
[RISK-001] The risk stage is a gate: it may return deny, pending, or pass, but never allow.
[RISK-002] A signal is triggered by matching a property of the action request.
[RISK-003] A triggered signal produces the decision configured for it: deny, pending, or pass.
Signals
secret_path
[RISK-004] secret_path is triggered when an effect touches a path known to hold secrets, such as **/.env, **/.env.*, ~/.ssh/**, or cloud credential files.
[RISK-005] secret_path is configured in the policy risk table.
outside_workspace
[RISK-006] outside_workspace is triggered when an effect touches a path outside the workspace root.
bulk_delete
[RISK-007] bulk_delete is triggered when a single action would delete more files than a configured threshold.
[RISK-007a] A signal that uses a threshold MUST read it from its policy entry’s threshold field, and MUST apply the documented default of 20 files when the entry gives no threshold.
opaque_command
[RISK-008] opaque_command is triggered when an action carries an opaque effect.
destructive_vcs
[RISK-009] destructive_vcs is triggered by version-control operations that can destroy shared history, such as git push --force.
Configuration
[RISK-010] Every signal MUST have a configured decision: deny, pending, or pass.
[RISK-011] A signal that is not configured MUST produce pass.
Examples
{
"schema": "shiin.risk-signals/v1-draft.1",
"signals": [
{
"name": "secret_path",
"triggered": true,
"decision": "deny",
"reason": "deny.risk"
},
{
"name": "bulk_delete",
"triggered": false,
"decision": "pending",
"reason": null
}
]
}
Revision history
- v1-draft.1 (2026-10-01): Initial draft.
Approval protocol
Note
Specification: draft. Local approval state, grants, revocation, and Unix daemon integration exist in source. Host validation and independent security review remain open.
Version:
shiin.approval-protocol/v1-draft.1.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
An approval request is a pending decision waiting for an approver to resolve it (ADR-0027, ADR-0028).
Approval request
[APR-001] An approval request MUST be created when the decision is pending and no matching grant covers it.
[APR-002] An approval request MUST carry the action request id, the decision, the reason code, and the agent-facing message.
[APR-003] An approval request MUST show the normalized action, not the agent’s description of it.
Resolution
[APR-004] A resolution MUST be one of allow_once, allow_always, or deny.
[APR-005] allow_once creates a single-use grant bound to the digest of one action request.
[APR-006] allow_always creates a scoped grant covering an agent, action types, a resource pattern, and a workspace.
[APR-007] A grant MUST expire.
[APR-008] A grant MUST never override deny.
Approver
[APR-009] An approver MUST be a human authorized to resolve approval requests.
[APR-010] An approver MUST NOT be able to approve through an intercepted channel that the agent controls.
Deny-with-ticket
[APR-011] For a host that cannot wait, the adapter MUST deny the action now and return an approval ticket.
[APR-012] If the agent retries after an approver approves, the retry MUST consume a single-use grant.
Lifecycle
[APR-013] An approval request that is never resolved MUST expire as deny.
[APR-014] An approval request MUST be revocable before it is resolved.
Local implementation
The Unix daemon uses a 0700 per-user directory and a 0600 local socket, checks peer credentials, and holds an exclusive state lock. An in-memory 256-bit operator credential is disclosed only in an explicitly started operator terminal. Agent-facing requests cannot administer approvals without that credential. It is never included in tickets, audit records, files, or environment variables.
The retry binding hashes the normalized request with only its fresh correlation id excluded.
Agent, session, action type, every effect, task, workspace, policy snapshot, and host context remain bound.
This lets a host retry the same action with a new request UUID without making the ticket itself an authorization credential.
Single-use consumption is persisted before an allow response.
Scoped grants require an explicit agent, exact action types, a resource glob, and a workspace; session, task, and snapshot remain bound as well.
The initial pending deadline is five minutes, and grant lifetimes are positive and at most 24 hours, capped by an active task’s expiry.
The service re-evaluates the current policy and task before consulting grants. A deny is never eligible for a grant. State corruption, failed audit writes, failed persistence, missing task counters, and a configured but unreachable daemon all fail closed.
Example
{
"schema": "shiin.approval-protocol/v1-draft.1",
"request_id": "0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0b",
"decision": "pending",
"reason": "pending.opaque",
"message": "The command's effects are unknown.",
"ticket": "ticket-001",
"created_at": "2026-10-01T13:58:33Z",
"expires_at": "2026-10-01T14:03:33Z",
"resolution": null
}
Revision history
- Implementation update (2026-10-15): Added local service behavior and the retry binding; the normative draft version is unchanged.
- v1-draft.1 (2026-10-01): Initial draft.
Audit events
Note
Specification: draft. Requests, decisions, approval and grant events, hash chaining, redacted summaries, and local verification exist in source. Signed evidence and replay are implemented in part; platform and conformance validation remain open.
Version:
shiin.audit-events/v1-draft.2.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
An audit event is one record in the audit log (ADR-0029, ADR-0030).
Audit log
[AUD-001] The audit log MUST be an append-only JSON Lines file.
[AUD-002] Each event MUST be a JSON object on one line.
[AUD-003] Events MUST be linked by a hash chain: each event includes the hash of the event before it.
Event kinds
[AUD-004] The event kinds are:
| Kind | Meaning |
|---|---|
request.received | An action request was received. |
decision.made | A decision was produced. |
approval.requested | An approval request was created. |
approval.resolved | An approval was resolved. |
grant.used | A grant was consumed. |
grant.created | A grant was created. |
grant.revoked | A grant was revoked. |
Required fields
[AUD-005] Every event MUST carry:
| Field | Type | Description |
|---|---|---|
schema | string | shiin.audit-event/v1-draft.2. |
id | string (UUID) | The event id. |
timestamp | string (ISO 8601) | When the event occurred. |
kind | string | The event kind. |
prev | string | The digest of the previous event; the first uses sha256: followed by 64 zeroes. |
self | string | The digest of this event. |
seq | integer | Position in the log, beginning at 1. |
payload | object | An allowlisted summary and the SHA-256 digest of the original normalized body. |
The hash is SHA-256 of the compact UTF-8 JSON event with self omitted and object keys sorted lexicographically at every depth.
Array order is preserved.
The writer verifies the existing chain before extending it and syncs every appended event.
Draft.1 logs emitted before this reconciliation used a different, nonconforming shape. They are not silently accepted or migrated. Preserve them separately before starting a new log; do not delete evidence.
Redaction
[AUD-006] Events MUST NOT contain secret values such as tokens, key material, file contents, or environment variables.
[AUD-007] Paths, keys, identifiers, and digests are allowed.
Current summaries retain request, agent, session, task, host, workspace, decision, and snapshot identifiers, plus path and count effects.
Command, URL, and other effect values are replaced with value_digest.
File contents, raw host payloads, free-text messages, and unknown fields are omitted.
The resulting summary is not a complete action request.
An audit summary alone cannot support exact replay because sensitive effect values are redacted. The daemon stores replay inputs separately in encrypted evidence; replay and its key path have a source implementation, with platform and external review still open.
Durability
[AUD-008] A non-read action MUST NOT proceed until its audit event is durably recorded.
Example
{
"schema": "shiin.audit-event/v1-draft.2",
"id": "0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0c",
"timestamp": "2026-10-01T13:58:33Z",
"kind": "decision.made",
"prev": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"self": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"seq": 1,
"payload": {
"digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222"
},
"request_id": "0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0b",
"decision": "deny",
"reason": "deny.policy"
}
Revision history
- v1-draft.2 (2026-10-15): Reconciled wire names, sequence numbers, hash construction, and redacted summaries; explicitly reject incompatible earlier logs.
- v1-draft.1 (2026-10-01): Initial draft.
Receipts
Note
Specification: draft. Normative design. Signed receipts, checkpoints, and encrypted replay artifacts are implemented in source; schema and cross-platform conformance remain incomplete.
Version:
shiin.receipts/v1-draft.3.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
A receipt is a signed statement that attests a decision was made (ADR-0031, ADR-0032).
Envelope
[RCPT-001] A receipt MUST use a DSSE (Dead Simple Signing Envelope) envelope.
[RCPT-002] A receipt MUST be signed with Ed25519.
Payload
[RCPT-003] The payload MUST include the action request id, the decision, the reason code, and the policy snapshot digest.
[RCPT-004] The payload MUST include the signing key identifier.
Verification
[RCPT-005] Receipt verification checks the signature and contents.
[RCPT-006] Verification proves who signed a statement about a decision, not that the action’s real outcome was correct.
Key management
[RCPT-007] Signing keys MUST be stored as described in Signing key storage.
[RCPT-008] A decision receipt MUST include the SHA-256 digest of the canonical action request whose decision it attests.
[RCPT-009] A decision receipt and audit checkpoint MUST include their signing time as an RFC 3339 timestamp.
Checkpoints
A checkpoint signs a position in the audit log so an observer can compare a local history with a trusted head.
[RCPT-010] A checkpoint MUST include the unsigned sequence number, the SHA-256 digest at that position, the signing key identifier, and its signing time.
The checkpoint’s sequence number is one-based for audit events; sequence zero identifies the genesis digest.
Encrypted replay inputs
Replay artifacts preserve the inputs needed to re-evaluate one decision. They are encrypted locally and signed so replay requires the originating replay key while authenticity can be checked before decryption.
[RCPT-011] A replay artifact MUST encrypt the recorded request, policy, task, usage, evaluation time, mode, decision, grants, and engine version using XChaCha20-Poly1305 with a fresh 192-bit nonce.
[RCPT-012] Replay decryption MUST bind the replay key identifier and request identifier as authenticated associated data.
Current implementation
The daemon writes signed decision receipts, encrypted replay inputs, and a signed audit checkpoint under .shiin/evidence/ when it starts with SHIIN_EVIDENCE=1 and an initialized signing key.
shiin verify <evidence.json> <public.json> verifies a receipt offline.
For a checkpoint, it also checks the matching local .shiin/audit.jsonl history, so copying only the checkpoint and public key to another machine is insufficient.
shiin replay <encrypted-replay.json> requires the matching local replay key; the optional policy argument performs a what-if comparison.
Windows support, conformance vectors for all evidence payloads, and Tier 1 offline verification remain open.
Checkpoint example
{
"schema": "shiin.checkpoint/v1-draft.1",
"key_id": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"seq": 1,
"head": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"signed_at": "2026-10-16T13:58:33Z"
}
Encrypted replay example
{
"schema": "shiin.replay-encrypted/v1-draft.1",
"key_id": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"request_id": "0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0b",
"nonce": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"ciphertext": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}
Example
{
"schema": "shiin.receipt/v1-draft.3",
"key_id": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"request_id": "0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0b",
"decision": "allow",
"reason": "allow.policy",
"policy_snapshot": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"request_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"signed_at": "2026-10-01T13:58:33Z"
}
Revision history
- v1-draft.1 (2026-10-01): Initial draft.
- v1-draft.2 (2026-10-16): Add the request digest bound by the source implementation.
- v1-draft.3 (2026-10-16): Specify signed checkpoints and encrypted replay claims and their validation fields.
Adapter contract
Note
Specification: accepted. Implemented in part in the source build. Remaining requirements and external validation are not complete.
Version:
shiin.adapter-contract/v1-draft.1.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
An adapter connects a host to Shiin. An adapter intercepts proposed actions, builds action requests, and enforces the resulting decision using the host’s own protocol. Adapters never make decisions (ADR-0025, ADR-0026).
Interception
[ADP-001] An adapter MUST intercept proposed actions before the host executes them.
[ADP-002] An adapter MUST normalize the host payload into an action request (AR-001).
[ADP-003] An adapter MUST NOT classify an action more permissively than the host payload allows. Anything the adapter cannot classify MUST become an opaque effect (ADR-0018).
Enforcement
[ADP-004] An adapter MUST enforce the decision using the host’s own protocol.
[ADP-005] An adapter MUST return allow only when the decision is allow.
[ADP-006] An adapter MUST return deny when the decision is deny.
[ADP-007] For a pending decision, an adapter MUST either wait for an approver, use the host’s native prompt, or deny with a ticket (ADR-0028).
With the local daemon configured, pending actions are denied with an approval ticket on all three adapters. Daemonless Codex and Cursor still deny pending actions without a ticket when native holding behavior is unsupported or unverified. The full approval workflow requires the configured local service; daemonless operation does not fully satisfy ADP-007.
[ADP-008] An adapter MUST produce deny on any error, timeout, or crash (ADR-0013).
Enforcement level
[ADP-009] Every adapter MUST declare an enforcement level: L1 cooperative, L2 mediated, or L3 OS-enforced (ADR-0033).
[ADP-010] An adapter’s documentation MUST state what it cannot see.
Audit
[ADP-011] An adapter MUST write an audit event before a non-read action is allowed to proceed.
[ADP-012] An adapter MUST record every decision, approval, and grant.
Hosts
| Host | Enforcement level | Interception | Milestone |
|---|---|---|---|
| Claude Code | L1 | hooks | v0.1 |
| Codex CLI | L1 | hooks | v0.2 |
| Cursor | L1 | hooks | v0.2 |
| MCP proxy | L2 | data path | v0.5 |
Revision history
- Accepted on 2026-10-12. Accepted: the contract matches the hook behavior verified for Claude Code, Codex CLI, and Cursor on 2026-09-14.
- v1-draft.1 (2026-10-01): Initial draft.
Local API
Note
Specification: draft. The Unix daemon implements JSON-RPC evaluation, policy validation, and operator approval methods;
shiin-clientexposes evaluation and policy validation. Windows named pipes and attested agent identity remain unimplemented.Version:
shiin.local-api/v1-draft.2.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
The local API lets custom agents and integrations ask Shiin for decisions (ADR-0023, ADR-0024).
Transport
[API-001] The local API MUST run over a Unix domain socket on Linux and macOS, and a Windows named pipe on Windows.
[API-002] The transport MUST be local-only and MUST NOT accept connections from remote hosts.
Protocol
[API-003] The protocol MUST be JSON-RPC 2.0.
[API-004] The methods are:
| Method | Description |
|---|---|
shiin.evaluate | Evaluate an action request and return a decision. |
shiin.evaluate.unattended | Evaluate without an available approver; pending becomes deny. |
shiin.policy.validate | Validate a policy and return diagnostics. |
shiin.approve.resolve | Resolve an approval request. |
shiin.pending | List pending requests; requires the operator credential. |
shiin.grants | List grants; requires the operator credential. |
shiin.revoke | Revoke a request or grant; requires the operator credential. |
[API-007] Evaluation methods MUST carry an action request as params.
Policy validation MUST carry source (the policy text) and workspace (the workspace root) as string fields in params.
Authentication
[API-005] The caller’s identity MUST be established at the attested-local assurance level or higher. The current service verifies that Unix peers share the daemon user’s UID but does not yet bind that credential to the request’s agent identity.
Failure
[API-006] Any error, timeout, or unreachable daemon MUST produce a fail-closed deny. shiin-client returns an error; callers MUST stop the requested action unless the decision is allow.
Example
{
"jsonrpc": "2.0",
"id": "0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0b",
"method": "shiin.evaluate",
"params": {
"schema": "shiin.action-request/v1-draft.1",
"id": "0190f3a0-1c2d-4e3f-8a4b-5c6d7e8f9a0c",
"session": "sess-001",
"agent": { "id": "agent-001", "assurance": "asserted" },
"action_type": "fs.read",
"effects": [
{ "kind": "path", "value": "/home/dev/project/src/login.rs", "confidence": "exact" }
],
"task": null,
"policy_snapshot": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"host": { "name": "custom-agent" },
"workspace": { "root": "/home/dev/project" }
}
}
{
"jsonrpc": "2.0",
"id": "EXAMPLE-policy-validation",
"method": "shiin.policy.validate",
"params": {
"source": "version = 1\n[defaults]\ndecision = 'deny'\n",
"workspace": "/home/dev/project"
}
}
Revision history
- v1-draft.2 (2026-10-16): Bind evaluation and policy-validation parameters to their schemas; the Rust client uses the daemon’s
sourcefield and rejects unsupported decision versions. - v1-draft.1 (2026-10-01): Initial draft.
- Implementation update (2026-10-16): Documented the existing Unix daemon methods and Rust client. Windows transport and identity binding remain open.
Test vectors
Note
Specification: draft. The engine conformance test and
shiin policy testexecute every vector.
Test vectors encode the golden use cases and are the conformance corpus that implementations are checked against.
Format
Each test vector is a JSON file in docs/spec/test-vectors/ validated against test-vector.v1.schema.json.
A test vector records one action request, the policy it is evaluated against, and the expected decision.
Optional fields carry the remaining engine inputs, so that every vector can be decided from the vector alone:
| Field | Meaning |
|---|---|
task | The active task declaration, for vectors that exercise intent binding. |
mode | attended or unattended. Absent means attended. |
usage | What the task has used before this action, for vectors that exercise limits. |
now | The evaluation time, for vectors that exercise a task expiry. |
Golden use cases
| ID | Use case | Vector file |
|---|---|---|
| TV-001 | An allowed edit | tv-001-allowed-edit.v1.json |
| TV-002 | A denied read of a secret | tv-002-denied-secret.v1.json |
| TV-003 | A command with unknown effects | tv-003-opaque-command.v1.json |
| TV-004 | An action outside the task | tv-004-outside-task.v1.json |
| TV-005 | A bulk deletion | tv-005-bulk-delete.v1.json |
| TV-006 | A destructive VCS operation | tv-006-destructive-vcs.v1.json |
| TV-007 | An unattended deny | tv-007-unattended-deny.v1.json |
| TV-008 | No rule matches, so the default decision applies | tv-008-default-deny.v1.json |
| TV-009 | A secret read through a shell command, denied by the same rule as a direct read | tv-009-secret-via-shell.v1.json |
| TV-010 | A task’s write limit is reached | tv-010-task-limit.v1.json |
| TV-011 | An expired task no longer constrains | tv-011-task-expired.v1.json |
| TV-012 | A task asks a person about an action type | tv-012-task-asks.v1.json |
| TV-013 | A task limits which hosts may be reached | tv-013-task-host.v1.json |
| TV-014 | Task scope denial wins over required approval | tv-014-task-deny-before-pending.v1.json |
| TV-015 | A task forbids a read performed through a shell command | tv-015-task-forbidden-effect.v1.json |
| TV-016 | Workspace-relative task scope excludes another workspace | tv-016-task-scope-root.v1.json |
| TV-017 | A spent task budget wins over required approval | tv-017-task-budget-before-pending.v1.json |
| TV-018 | Outside-workspace risk denies an otherwise allowed write | tv-018-outside-workspace-risk.v1.json |
| TV-019 | Secret-path risk denies an otherwise allowed read | tv-019-secret-path-risk.v1.json |
| TV-020 | Missing capability denies an otherwise allowed action | tv-020-missing-capability.v1.json |
| TV-021 | Capability denial wins over a broad capability grant | tv-021-capability-deny-wins.v1.json |
| TV-022 | Every effect class requires an explicit capability | tv-022-capability-effect.v1.json |
| TV-023 | A capability does not replace policy authorization | tv-023-capability-not-authority.v1.json |
| TV-024 | Session and task rule selectors match together | tv-024-rule-session-task.v1.json |
| TV-025 | A different session cannot match a scoped rule | tv-025-rule-session-mismatch.v1.json |
| TV-026 | An absent task cannot match a task-scoped rule | tv-026-rule-task-missing.v1.json |
Running
Implementations run every vector and check the decision matches. The source runner checks the reason code as well as the decision.
$ cargo xtask conformance
This writes target/conformance/report.json with the tested Git revision, per-case expected/actual outcomes, and SHA-256 digests of the copied vectors and schemas.
An optional directory argument selects the bundle location.
The documentation workflow publishes the core-conformance artifact for third-party implementations to download and compare.
The report covers core engine behavior; it is unsigned and does not establish host interception, identity assurance, external MCP conformance, or independent security review.
Revision history
- v1-draft.6 (2026-10-16): Migrated existing policy snapshots to explicit capabilities and added capability denial/precedence/effect coverage and session/task selector cases.
- v1-draft.5 (2026-10-12): Added TV-014 to TV-019 for task denial precedence, forbidden effects, workspace-relative scope boundaries, and configured path risk signals.
- v1-draft.4 (2026-10-12): Added TV-010 to TV-013 for task limits, expiry, ask-for, and host constraints, and the
usageandnowfields they need. - v1-draft.3 (2026-10-12): Added TV-009, which pins UC-02: a rule written for
fs.readmust also cover a read performed by a shell command. - v1-draft.2 (2026-10-12): Added TV-008 for the default decision, a workspace root to every request, and the optional
taskandmodefields, so TV-004 and TV-007 are decidable from their own inputs. - v1-draft.1 (2026-10-01): Initial draft.
Threat model
Note
Specification: draft. Implemented in part in the source build. Remaining requirements and external validation are not complete.
This page defines the threats Shiin is designed to resist.
Threats are referenced throughout the documentation as **[ST-<category>-<nn>]**.
STRIDE categories
| Category | Meaning |
|---|---|
| S | Spoofing identity |
| T | Tampering with data or logs |
| R | Repudiation (denying an action) |
| I | Information disclosure |
| D | Denial of service |
| E | Elevation of privilege |
Threats
Spoofing
[ST-S-01] An agent presents a false identity. Shiin treats the agent as untrusted and never allows an action because the agent says it is safe.
[ST-S-02] An approver is impersonated. Approver identity is established through an approval channel that the agent does not control.
Tampering
[ST-T-01] An agent modifies the audit log. Chain verification detects altered records and broken links. An unsigned local chain cannot establish whether its entire contents were replaced or its tail was removed. Signed checkpoints exist in source when evidence recording is enabled. They help detect replacement or truncation before the signed head, but cannot establish integrity after that checkpoint.
[ST-T-02] An agent modifies policy. The designed trust model loads trusted layers and lets a workspace layer only restrict. The current CLI instead loads one operator-controlled workspace file; separate trust-layer loading is unimplemented.
[ST-T-03] An agent modifies Shiin’s own state. Shiin’s state is a built-in protected resource that no policy can make allowable.
Repudiation
[ST-R-01] An agent denies it performed an action. Every decision is recorded in the audit log with a stable id.
Information disclosure
[ST-I-01] An agent learns policy structure to probe for a bypass. Agent-facing messages never reveal policy structure, error codes, or paths.
[ST-I-02] An agent reads secret values through logs. Logs never contain secret values; identifiers and digests are used instead.
[ST-I-03] An agent reads the audit log. The audit log is a built-in protected resource.
Denial of service
[ST-D-01] An agent floods Shiin with requests.
The engine is synchronous and bounded; the adapter enforces timeouts that produce deny.
The daemon rejects noncanonical workspace roots and oversized normalized actions before policy evaluation; focused tests cover both bounds.
Audit append verifies the full existing chain with a streaming reader, so prior event payloads no longer accumulate in memory before an authorization can complete.
Regression tests reject early, middle, late, and malformed-tail corruption without appending; this preserves ST-T-01 while reducing aggregate-log memory usage.
Individual oversized audit records and total verification latency still require bounds.
[ST-D-02] A malicious policy file hangs Shiin. Policy compilation is bounded and fails closed.
Elevation of privilege
[ST-E-01] An agent uses a grant beyond its scope.
A grant is bound to a digest or a scope and never overrides deny.
[ST-E-02] An agent disables Shiin. Adapters declare their enforcement level honestly; hook-based controls can be disabled.
[ST-E-03] An agent runs code that bypasses Shiin.
Shiin cannot see what an allowed command does; a sandbox covers that gap.
The MCP proxy forwards a tool call only after allow; its unit test confirms that deny and pending are not forwarded.
Its process integration test also checks that daemon unavailability prevents downstream execution and that plaintext tool arguments are omitted from audit events.
The downstream HTTP transport retains that authorization path and requires an explicitly configured endpoint.
TLS is required outside literal loopback addresses; URL credentials, query strings, redirects, automatic HTTP retries, environment proxies, and expired-session reconnection are refused or disabled.
Endpoint and redirect regression tests cover these boundaries; real remote-server authentication and independent transport review remain open (ST-I-02, ST-T-02).
Provisioned MCP bearer credentials now come from an explicitly named environment variable and are sent only to an HTTPS endpoint with redirects disabled.
Sensitive header marking, temporary-buffer zeroization, bounded credential/CA input, and fixed setup errors reduce accidental disclosure; tests check invalid/plaintext configuration and an authenticated TLS fixture (ST-I-02).
An explicit operator CA can extend trust without disabling hostname or certificate checks.
OAuth acquisition/refresh, external authenticated-server validation, and independent transport review remain open.
The cooperative command wrapper binds the exact executable/argument list and starts a child only after a durable allow.
Its process tests cover pending, denial, damaged policy/audit state, unavailable services, literal argument forwarding, and child status propagation (ADR-0013, ADR-0030).
Commands launched outside the wrapper and the internals of allowed child processes remain outside its interception point.
Out of scope
Shiin is not designed to resist:
- Operating-system level attacks against the developer’s account.
- Hardware or firmware attacks.
- Supply-chain attacks against Shiin’s dependencies, which are covered by the supply-chain policy.
Current changes and independent review
The 2026-10-15 reconciliation strengthens existing controls without making a failure or unknown configuration more permissive. AI assistance was used; this section is a review packet, not an independent review or security sign-off. The M0 external-review criterion remains open.
| Threat | Change or evidence | Remaining review |
|---|---|---|
| ST-I-01 | Hook failure and decision messages omit internal reason codes, file paths, and error text; regression tests exercise both response protocols. | Verify messages and exit behavior in running hosts, including startup failures. |
| ST-I-02 | Audit summaries omit free text and raw host input; command and URL values become digests. | Review identifier/path exposure and the limits of hashing low-entropy values. |
| ST-T-01 | Audit wire records follow the versioned schema and an existing chain is verified before append. | Review incompatible old-log handling and the absence of signed anchors. |
| ST-D-01 | Existing timeout, panic, contention, and budget tests remain required; the performance command measures local latency. | Large-input/log bounds and cross-platform memory measurements remain incomplete. |
| ST-S-01 | Hook identities remain asserted and the identity stage passes through. | Review the lack of assurance enforcement before claiming identity protection. |
| ST-T-02 | Built-in protections cannot be granted by operator rules. | Review single-file policy loading against the designed trust layers. |
| ST-E-02 | Cursor and Codex pending decisions are denied rather than relying on unsupported or unverified holding behavior; Claude Code also denies a pending response carrying a daemon ticket instead of treating it as a native prompt. | The adapter regression test covers this branch. Check host configuration, disabling controls, and every supported host version. |
Additional topics for the reviewer are shell classification coverage, path-error and Unicode/case handling, task timestamp parsing, and policy matching for multiple effects. The Unix approval state machine and grant consumption, receipt signature verification, and replay have source implementations with focused tests. Their security properties still require independent review; Windows approval transport and live-host behavior remain unvalidated.
The external reviewer should record their name, reviewed commit or diff, platform and host versions, findings, and resolution of each finding. No reviewer has supplied that record yet.
Local approval service changes
The capability gate now requires explicit grants for the request class and every effect class, and any matching capability refusal wins. Missing, invalid, or ambiguous declarations cannot produce allow; capability grants alone do not authorize an action (ADR-0011, ADR-0012). Seven new golden cases cover capability enforcement and session/task rule selectors, and the signed replay suite exercises them as well. The snapshot/engine version changes prevent older replay inputs from silently receiving new capability semantics. Session and agent identifiers remain asserted; they are selectors, not attestation (ST-S-01, ST-E-01). The daemon continues to replace a caller’s task reference with its authoritative active task before matching rules.
The Rust client now exercises the real daemon contract over a local socket, including policy validation, non-allow decisions, and stale snapshots.
Unsupported decision versions are rejected before an integration can act on them; this tightens failure handling without changing policy decisions (ADR-0013).
Replay regression tests now exercise the signed/encrypted conformance corpus through the CLI’s evaluation routine, plus input bindings, grant coverage, deny precedence, and what-if comparisons.
These tests use explicit fixture keys without accessing or replacing the operator’s OS keyring.
The separate native key-store lifecycle test uses the same storage methods under a random test-only service namespace on hosted runners (ADR-0032).
It checks key persistence, rotation and retained-key replay, plus refusal of corrupt, deleted, or mismatched entries without regenerating a selected key (ST-T-01, ST-I-02).
The production service name remains fixed, and ordinary tests never access an operator’s key store.
The first-alpha dependency refresh selects native stores through keyring-core directly, replacing the Linux Zbus transport with the DBus Secret Service backend (ADR-0032).
Service/username bindings, key fingerprints, error handling, and refusal of a corrupt selected key stay unchanged; there is no plaintext or mock fallback (ST-I-02, ST-T-01).
The native lifecycle test remains required on every supported operating system after this backend change.
This validates storage behavior, not isolation from unmediated same-user processes or independent security review.
The schema runtime feature now separates OS clock/randomness functions from pure wire types and deterministic timestamp formatting.
Adapters explicitly retain the runtime feature and its fail-closed randomness behavior; isolated engine builds omit it.
The WebAssembly check and dependency guard check this separation without changing an authorization outcome (ST-T-01, ST-D-01).
The separate fuzz workspace exercises action-request parsing/validation and shell/SQL classification with bounded inputs and memory.
Its authorization target also checks determinism, unattended behavior, and capability refusal invariants against arbitrary validated requests (ST-E-01).
The determinism check exposed unordered determining-rule identifiers in otherwise identical decision traces.
The compiler now sorts those identifiers before returning any policy outcome; a regression compares serialized decisions repeatedly for multiple matching allow, deny, and pending rules (ST-R-01).
This canonicalizes evidence without changing which rules match, denial precedence, or an authorization outcome (ADR-0010).
Miri interprets pure schema tests; these robustness checks supplement the existing fault-injection tests and do not constitute independent review (ST-T-02, ST-D-01).
The v0.3 source implementation addresses ST-E-01 and ST-E-02 by requiring an out-of-band operator credential for resolution, listing, and revocation. The local transport authenticates the peer’s Unix user identity; an asserted agent role is insufficient to administer approvals. Tests reject missing or incorrect credentials and generated transition sequences check that a single-use grant cannot authorize twice. The Rust client’s socket integration also exercises missing/forged operator proof, changed-action retries, single-use consumption, reusable scopes, built-in denial precedence, and revocation through the real service. It checks that those transitions are audited and operator credentials are absent from the log. After a single-use grant is spent, a new pending retry now receives a fresh unresolved ticket instead of reusing the already resolved ticket. The client lifecycle test exercises reapproval while retaining exact-action binding and denial precedence (ST-E-01). Grant bindings include the session, task, workspace, policy snapshot, action type, and effects; only the fresh request correlation UUID is excluded. Explicit denials are re-evaluated before grants and cannot be overridden. Audit and state writes finish before an allow response.
The credential is generated in memory and shown only in a dedicated operator terminal. Terminal allocation alone is not proof of a human; the separate credential and typed confirmation provide the operator authorization step. Allowed code running outside interception still shares the operating-system account and is outside the cooperative adapter’s enforcement boundary. Review of this boundary, restart/crash behavior, state-file races, and Windows transport remains required.
Security model
Note
Specification: draft. Implemented in part in the source build. Remaining requirements and external validation are not complete.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
Fail-closed
[SEC-001] Any failure, error, timeout, or unreadable policy MUST produce deny (ADR-0013).
[SEC-002] A failure that happens after a decision but before the adapter answers MUST also produce deny.
Deny wins
[SEC-003] A deny from any stage overrides every other result, including grants.
[SEC-004] When no rule matches, the decision is deny.
The agent is untrusted
[SEC-005] Anything the agent or a tool server supplies can make a decision stricter but never more permissive.
[SEC-006] Agent-supplied identity claims, tool annotations, and explanations are treated as untrusted input.
Gates
[SEC-007] The identity, capability, intent, and risk stages are gates: they may return deny, pending, or pass, but never allow.
[SEC-008] Only a policy rule or a grant may produce allow.
Built-in protected resources
[SEC-009] A built-in protected resource MUST never be made allowable for agents by policy (ADR-0016).
[SEC-010] Protected resources include Shiin’s own state, keys, and audit log, host hook configuration, and approval commands.
Determinism
[SEC-011] The same action request and the same policy snapshot always produce the same decision. No language-model or machine-learning output is an input to any decision (ADR-0009).
Honest coverage
[SEC-012] Every adapter states its enforcement level and what it cannot see (ADR-0033).
Sandboxes
[SEC-013] Shiin decides whether an action should happen. A sandbox limits the damage if an allowed action, or the code it runs, misbehaves.
Revision history
- v1-draft.1 (2026-10-01): Initial draft.
Enforcement levels
Note
Specification: draft. Implemented in part in the source build. Remaining requirements and external validation are not complete.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
An enforcement level states how strongly a Shiin decision is enforced for a given adapter (ADR-0033).
Levels
L1 Cooperative
[ENF-001] The host honors the decision through its hook mechanism.
[ENF-002] The hook can be disabled with a host flag such as --bare or disableAllHooks.
[ENF-003] An L1 adapter cannot see actions that do not pass through the hook.
L2 Mediated
[ENF-004] Shiin sits in the data path, as the MCP proxy does.
[ENF-005] The host must route traffic through Shiin for the decision to apply.
L3 OS-enforced
[ENF-006] The operating system or a sandbox enforces the decision.
[ENF-007] L3 is a future enforcement level.
Per-adapter levels
| Adapter | Level | Cannot see |
|---|---|---|
| Claude Code | L1 | Actions outside hooks; code run by an allowed command. |
| Codex CLI | L1 | Actions outside hooks; code run by an allowed command. |
| Cursor | L1 | Actions outside hooks; code run by an allowed command. |
| MCP proxy | L2 | Traffic not routed through the proxy. |
| Shell wrapper | L1 | Commands typed directly at the shell. |
Honest labeling
[ENF-008] Every adapter’s documentation MUST state its enforcement level and what it cannot see.
[ENF-009] An adapter MUST NOT claim protection that its interception point cannot provide.
Revision history
- v1-draft.1 (2026-10-01): Initial draft.
Cryptography
Note
Project policy: draft. This page is the only home of Shiin’s cryptography rules (ADR-0031, ADR-0032).
Algorithms
| Use | Algorithm |
|---|---|
| Receipt signatures | Ed25519 |
| Audit hash chain | SHA-256 |
| Receipt envelope | DSSE |
Key management
Signing key generation, rotation, and public-key export use the OS keyring, following ADR-0032. Replay encryption keys are stored alongside the signing key in the keyring. Native key-store lifecycle checks pass on hosted Linux, macOS, and Windows runners, including persistence, rotation, retained-key replay, and corrupt-entry refusal. The testing strategy describes the isolated fixture and its limits. Live operator-environment qualification and independent review remain open.
Randomness
Shiin uses the operating system’s randomness source for ids and keys. The engine crates never use a custom randomness source.
Read next
Vulnerability disclosure
Note
Project policy: draft. This page is the only home of Shiin’s vulnerability disclosure rules (ADR-0006).
What is a vulnerability
A way to get an action past Shiin’s decisions is a security vulnerability, even in a specification.
How to report
Report vulnerabilities privately. Do not open a public issue.
What happens
- The maintainers acknowledge receipt privately.
- A fix is prepared and coordinated with the reporter.
- A public advisory is published when the fix is released.
Out of scope
- Issues in third-party dependencies are reported to the maintainers of those dependencies.
- Issues in the developer’s operating system or hardware are out of scope.
Read next
Hardening
Note
Project policy: draft. This page is the only home of Shiin’s hardening guidance.
Run agents as a less-privileged user
Where possible, run agents as a less-privileged user. Shiin is not a replacement for operating-system permissions.
Keep host controls enabled
Shiin’s L1 adapters use host hooks, which can be disabled. Keep the host’s own controls enabled alongside Shiin.
Use a sandbox
Shiin decides whether an action should happen. A sandbox limits the damage if an allowed action, or the code it runs, misbehaves.
Keep Shiin updated
Shiin is updated through ordinary pull requests and releases. Signed release binaries with a software bill of materials are produced for every release.
Verify releases
See Verifying releases.
Read next
Verifying releases
Note
Guide: draft. The first alpha publishes archive checksums, signed build provenance, dependency metadata, and SBOMs. Apply these steps to the assets on the versioned release page.
A release is built by a workflow in this repository, from a tag, on GitHub’s runners. Verifying one answers two questions: did I download what was published, and was it built from this repository’s source.
1. Check the archive against its checksum
Every archive is published with a .sha256 file beside it.
$ sha256sum --check shiin-cli-x86_64-unknown-linux-gnu.tar.xz.sha256
A checksum only proves the download was not corrupted. It proves nothing about origin, because whoever could replace the archive could replace the checksum.
2. Verify the build provenance
Each archive carries a signed attestation naming the repository, the workflow, and the commit it was built from. That is what ties a binary to this source.
$ gh attestation verify shiin-cli-x86_64-unknown-linux-gnu.tar.xz \
--repo BAGOMBEKA-JOB-DEV/shiin
Expect the output to name this repository and the release workflow. If verification fails, do not run the binary.
3. Read the bill of materials
Each release includes shiin-cli.cdx.xml and shiind.cdx.xml, listing package dependencies and versions in CycloneDX format.
$ gh release download <version> --pattern '*.cdx.xml'
Use it to check whether a published advisory affects a release you already have.
What this does not tell you
- That the source is correct. Provenance ties a binary to a commit. Reviewing the commit is a separate thing.
- That the build is reproducible. Reproducible builds are a goal, not a promise; see release engineering.
- That Shiin is enforcing anything. A verified binary that no host calls does nothing.
Run
shiin doctorand read enforcement levels.
Embedded dependency metadata
The release configuration enables cargo auditable metadata in binaries.
The dist option selects the auditable build command (verified 2026-10-18).
The archive provenance and checksums remain the origin and integrity checks; embedded dependency metadata does not replace them.
Read next
Architecture overview
Note
Design document: draft. Authorization, approvals, evidence, and early client/integration code exist in source; platform, host, and milestone validation remain incomplete.
Shiin is a local, agent-agnostic authorization layer for AI agent actions. It sits between agents and the files, commands, networks, and services they act on.
Components
flowchart LR
agent["Agent"] -->|proposed action| adapter["Adapter"]
adapter -->|action request| engine["Engine"]
policy["Policy and task declaration"] --> engine
engine --> decision{"Decision"}
decision -->|allow or deny| adapter
decision -->|pending| approver["Approver"]
approver -->|resolution| adapter
decision --> audit["Audit log"]
Engine
The engine is the pure, synchronous component that evaluates an action request against a policy snapshot and returns a decision. It performs no I/O (ADR-0010).
Adapters
An adapter connects a host to Shiin. It intercepts proposed actions, builds action requests, and enforces the resulting decision using the host’s own protocol. Adapters never make decisions.
Audit log
The audit log is an append-only JSON Lines file of audit events linked by a hash chain. It is tamper-evident, not tamper-proof.
Crates
The component map is described in Workspace and crates.
The engine crates are shiin-schema, shiin-classify, shiin-policy, and shiin-engine.
Deployment
Shiin is local-first and daemonless in v0.1 (ADR-0022). The Unix per-user daemon is implemented in source for approvals; Windows transport and broader platform validation remain open. See Deployment models.
Read next
Decision pipeline
Note
Design document: draft. Authorization, approvals, evidence, and early client/integration code exist in source; platform, host, and milestone validation remain incomplete.
The decision pipeline is the path an action takes from proposal to enforcement.
Steps
- Intercept. An adapter receives the proposed action from the host.
- Normalize. The adapter builds an action request.
- Evaluate. The engine runs five stages in order: identity, capability, policy, intent, and risk.
- Decide. The engine produces a decision:
allow,deny, orpending. - Record. Request and decision audit events are synced; a write failure denies the action.
- Enforce. The adapter returns the decision in the host’s own protocol.
Stage order
Later stages can only make the result stricter.
The identity, capability, intent, and risk stages 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.
Failure
Any failure produces deny (ADR-0013).
Diagram
This diagram shows the Unix daemon path when approval handling is enabled.
The daemon evaluates and audits the request, creates a ticket for pending, and lets a separate operator terminal resolve it.
The identity gate currently passes through; the capability gate denies missing or explicitly refused action/effect classes.
sequenceDiagram
participant Host as Host and agent
participant Adapter as Host adapter
participant Daemon as Unix daemon
participant Approver as Approver
participant Audit as Audit log
Host->>Adapter: proposed action (host payload)
Adapter->>Daemon: action request
Daemon->>Daemon: identity, capability, policy, intent, risk
Daemon->>Audit: request received, decision made
Daemon-->>Adapter: pending ticket
Adapter->>Approver: ticket shown in operator terminal
Approver->>Daemon: authenticated resolution
Daemon->>Audit: approval resolved, grant consumed
Daemon-->>Adapter: allow
Adapter-->>Host: allow
Read next
Deployment models
Note
Design document: draft. Authorization, Unix approvals, signed evidence, and early client/integration code exist in source; platform and milestone validation remain incomplete.
Shiin is designed to run in several configurations. The early design preserves the constraints those features will need.
Local, daemonless
v0.1. A single user runs Shiin on their own machine. The adapter runs as a process for each hook invocation. No daemon, no service, no telemetry (ADR-0022).
Daemon
v0.3. A per-user daemon (shiind) holds approval state and grants.
Adapters talk to it over the local API (ADR-0023).
The Unix implementation is available in source; see handling approvals for the operator terminal and explicit daemon configuration.
Team
After 1.0. A team policy server provides shared policy, central evidence, and remote approvals. It needs signed policy bundles and transport-independent APIs.
Unattended
v0.2. In CI or other unattended environments, every pending becomes deny.
The policy is tested with shiin policy test before it is used.
Read next
State and storage
Note
Design document: draft. The local authorization core, approval service, and signed evidence paths exist in source; platform support and milestone validation remain incomplete.
Shiin stores its state on the developer’s machine and collects no telemetry (ADR-0007).
Locations
| State | Location | Milestone |
|---|---|---|
| Policy | .shiin/policy.toml in the workspace | v0.1 |
| Audit log | .shiin/audit.jsonl in the workspace | v0.1 |
| Approval requests and grants | ~/.shiin/runtime/approvals.json, beside the Unix daemon socket | v0.3, source implementation |
| Signing keys | OS keyring, following ADR-0032 | v0.4, partial source implementation |
| Tasks | .shiin/tasks/ in the workspace | v0.2 |
| Receipts and encrypted replay | .shiin/evidence/<request-id>.receipt.json and <request-id>.replay.json | v0.4, when daemon evidence is enabled |
| Signed checkpoint and public keys | .shiin/evidence/checkpoint.json and <key-id>.public.json | v0.4, when daemon evidence is enabled |
An absolute SHIIN_SOCKET changes the daemon directory and places approvals.json beside that socket.
The directory is private to the current Unix user; operator credentials are held in memory and change on restart.
Durability
A non-read action does not proceed until its audit event is durably recorded (ADR-0030).
Redaction
The audit log never contains secret values (AUD-006).
Read next
Workspace and crates
Note
Project policy: draft. This page is the only home of Shiin’s crate list. The workspace has twelve product crates and
xtask, including partial approval, evidence, and integration implementations. Host, platform, and milestone validation remain incomplete.
Shiin is built as a single Cargo workspace. This page describes the repository layout, the crates in the workspace, what each crate is responsible for, and the rules that keep the crate graph small, acyclic, and honest about what exists. The decision behind the layout is ADR-0035.
What exists today
| Path | State | Purpose |
|---|---|---|
Cargo.toml | exists | Virtual workspace manifest with shared package metadata, [workspace.dependencies], and [workspace.lints] |
rust-toolchain.toml | exists | Pinned toolchain; see Toolchain and MSRV |
rustfmt.toml, .editorconfig | exist | Formatting rules; see Coding standards |
.cargo/config.toml | exists | Defines the cargo xtask alias |
xtask/ | exists | Repository tooling; today it provides cargo xtask docs-check |
crates/ | exists | Product crates, each created in the milestone that needs it |
fuzz/ | exists | Wire-request and classification fuzz targets in a separate nightly workspace |
The workspace section of today’s Cargo.toml is:
[workspace]
resolver = "3"
members = [
"crates/shiin-approval",
"crates/shiin-audit",
"crates/shiin-classify",
"crates/shiin-client",
"crates/shiin-cli",
"crates/shiin-engine",
"crates/shiin-hooks",
"crates/shiin-ipc",
"crates/shiin-mcp-proxy",
"crates/shiin-policy",
"crates/shiin-schema",
"crates/shiind",
"xtask",
]
The alias in .cargo/config.toml is:
[alias]
xtask = "run --quiet --package xtask --"
Layout
Cargo.toml virtual workspace manifest (no root package)
rust-toolchain.toml pinned toolchain
rustfmt.toml formatter settings
.editorconfig editor settings
.cargo/config.toml cargo aliases, including `cargo xtask`
crates/<name>/ twelve product crates with milestone validation in progress
xtask/ repository tooling, never published
fuzz/ fuzz targets, a separate nightly workspace
docs/ this documentation
- The root
Cargo.tomlis a virtual manifest. It has no[package]section, so no crate is privileged by living at the root. - Product crates live in
crates/<name>/, and the directory name is the crate name. xtask/follows thecargo xtaskconvention for repository automation written in Rust. It setspublish = false. Product crates never depend on it.- The
fuzz/directory has its own[workspace]table and lockfile. Its nightly-only dependencies never enter the main workspace, its lockfile, or its MSRV check. See Testing strategy. - Every member inherits
edition,rust-version,license,repository, andauthorsfrom[workspace.package]. - Every member sets
[lints] workspace = true, exceptshiin-platform, which declares its own table as described in Coding standards.
Crates
The table lists every product crate in the workspace, its responsibility, and the milestone that owns its implementation and validation.
| Crate | Responsibility | Milestone |
|---|---|---|
shiin-schema | Wire types for action requests, decisions, approval messages, and audit events, matching the JSON Schemas in docs/spec/schemas/ | v0.1 |
shiin-classify | Normalizers for paths, shell commands, URLs, and experimental SQL, and the derivation of effects from them | v0.1 (SQL: v0.5; host integration open) |
shiin-policy | The TOML front-end and its compilation to Cedar, policy layering, and diagnostics | v0.1 |
shiin-engine | The pure, synchronous evaluation pipeline | v0.1 |
shiin-audit | The hash-chained audit log, verification, replay, and receipts behind a feature | v0.1 (receipts: v0.4; external review open) |
shiin-hooks | Host hook adapters: a claude_code module, then codex and cursor modules | v0.1 (Codex and Cursor: v0.2) |
shiin-cli | The shiin binary: init, check, explain, policy validate, policy test, hook, task, pending, approve, log, verify, replay, and doctor | v0.1 |
shiin-approval | The approval state machine and grants | v0.3 (source; review open) |
shiin-ipc | Local transport over Unix domain sockets and Windows named pipes | v0.3 (Unix source; Windows validation open) |
shiind | The per-user daemon | v0.3 (Unix source; platform validation open) |
shiin-client | The Rust client for daemon evaluation and policy validation | v0.5 (early source; conformance open) |
shiin-mcp-proxy | The MCP proxy with upstream stdio and downstream stdio or Streamable HTTP | v0.5 (HTTP: v0.6; external conformance open) |
shiin-platform | The only crate allowed to contain unsafe code; created only if needed | as needed |
xtask | Repository tooling | exists |
fuzz/ | Fuzz targets; a separate workspace, not a member crate | v0.1 |
shiin-cli gains its subcommands milestone by milestone; the CLI reference says which command arrives when.
The engine crates are shiin-schema, shiin-classify, shiin-policy, and shiin-engine.
Several rules on this page and on Async and concurrency apply to that group.
Rules
Never pre-create crates
- A crate is created in the pull request that adds its first real functionality, in the milestone the table names.
- Empty, placeholder, or “coming soon” crates are not created, in the repository or on crates.io. Crate names are not reserved on crates.io in advance.
- A pull request that adds, splits, merges, or renames a crate updates the table on this page in the same change.
- A change that alters the dependency direction below, or adds a crate to the set of stable public APIs, needs an ADR.
Stable and internal APIs
shiin-schemaandshiin-clientare the only crates intended to become stable public APIs. Other projects may depend on them, and after 1.0 they follow the guarantees in Versioning and stability.- Every other product crate is published to crates.io, because a published crate can only depend on other published crates, but is documented as internal before 1.0. Its crate-level documentation starts with a sentence saying that the API is internal to Shiin and can change in any release.
xtaskis never published.
Dependencies are declared once, with default features off
- Every third-party dependency is declared in
[workspace.dependencies]with a version requirement anddefault-features = false. - Members reference it with
workspace = trueand enable only the features they use. - Default features have to be disabled at the workspace level: a member can add features to a workspace dependency but cannot remove default features that the workspace entry leaves enabled.
- Admitting a dependency at all is governed by Dependencies and supply chain.
Today’s Cargo.toml does not fully follow this rule yet.
jsonschema sets default-features = false, but the regex, serde_json, and walkdir entries do not.
Several product dependencies also retain default features.
Reviewing and disabling unnecessary defaults remains open supply-chain work.
No networking in the engine crates
- The engine crates never open network connections and never depend on crates that do, such as HTTP clients, TLS stacks, or async runtimes.
shiin-engineperforms no I/O of any kind (ADR-0010).shiin-classifyandshiin-policymay read the local file system where their job requires it, such as resolving a path or loading a policy file.cargo xtask dependency-checkinspects the normal (non-dev) dependency tree of each engine crate and fails if a known network stack or async runtime appears. The documentation CI job runs this check; the prohibited-crate list still needs review when new dependencies are admitted.
shiin-engine builds for WebAssembly as a purity guard
From v0.4, CI builds shiin-engine for the wasm32-unknown-unknown target on every pull request.
The isolated check disables the schema crate’s default runtime feature through its workspace dependency.
OS clocks and randomness remain explicitly enabled by adapters and services; wire types and timestamp formatting do not require them.
- The build fails if the engine, or anything it depends on, needs operating-system facilities that target lacks, such as native system libraries, platform-specific system calls, or an operating-system randomness source. That makes it a cheap, continuous guard against I/O and platform dependencies creeping into the engine.
- The guard is not complete.
The standard library still compiles calls such as file and socket operations for that target and only fails at run time,
so the build does not catch them.
Planned
disallowed_methodsanddisallowed_typesClippy configuration for the engine crates covers that gap. - The guard starts at v0.4, when replay makes the engine’s determinism a guarantee users rely on.
- Building for this target is a test, not a support promise. Supported platforms are listed in Platform support.
Dependency direction
Dependencies point from binaries, through adapters and services, down to the engine crates, and never back up.
shiin-platform, if it is ever created, sits below everything that needs it and depends on no other Shiin crate.
flowchart TD
subgraph binaries["Binaries"]
cli["shiin-cli<br/>(shiin)"]
daemon["shiind"]
proxy["shiin-mcp-proxy"]
end
subgraph services["Adapters, approvals, evidence, and transport"]
hooks["shiin-hooks"]
approval["shiin-approval"]
audit["shiin-audit"]
client["shiin-client"]
ipc["shiin-ipc"]
end
subgraph enginecrates["Engine crates: no networking"]
engine["shiin-engine"]
policy["shiin-policy"]
classify["shiin-classify"]
schema["shiin-schema"]
end
platform["shiin-platform<br/>(only if needed)"]
binaries --> services
binaries --> enginecrates
services --> enginecrates
client --> ipc
engine --> policy
engine --> schema
policy --> schema
classify --> schema
binaries -.-> platform
services -.-> platform
classify -.-> platform
The diagram is enforced by these rules:
- A crate never depends on a crate in a group above it, and libraries never depend on binaries.
- The engine crates depend only on each other and on third-party crates that satisfy the no-networking rule.
shiin-enginedoes not depend onshiin-classify. Adapters classify what a host reports into an action request; the engine only evaluates action requests.shiin-clientdepends only onshiin-schema,shiin-ipc, and third-party crates, so embedding the public client library never pulls in Cedar or the engine.shiin-platformdepends on no other Shiin crate.shiin-engineandshiin-schemanever depend on it.xtaskmay depend on product crates, for example to validate examples against real types; no product crate depends onxtask.
Dependencies within a group are allowed when they respect these rules; Cargo rejects cycles.
Adding a crate
- Confirm the crate is in the table for the current milestone, or update the table in the same pull request.
- Create
crates/<name>/Cargo.tomlinheriting the[workspace.package]fields and[lints] workspace = true. - Write crate-level documentation stating the crate’s responsibility and, before 1.0, that its API is internal (unless it is
shiin-schemaorshiin-client). - Admit each new dependency under Dependencies and supply chain.
- If the crate is security-sensitive, add it to Security-sensitive changes.
Related pages
- Architecture overview maps components to these crates.
- Toolchain and MSRV
- Coding standards
- Unsafe code policy
Toolchain and MSRV
Note
Project policy: draft. This page is the only home of Shiin’s Rust toolchain version and minimum supported Rust version (MSRV). The pinned toolchain, edition, and MSRV below are already set in the workspace files.
Shiin pins the exact compiler every contributor and every CI job uses, and separately promises the oldest compiler its crates build with. The decision is recorded in ADR-0034.
Current baseline
| Setting | Value | Where it is set |
|---|---|---|
| Edition | 2024 | edition in [workspace.package] of Cargo.toml |
| Dependency resolver | 3 | resolver in [workspace] of Cargo.toml |
| Pinned toolchain | 1.98.1 | channel in rust-toolchain.toml |
| MSRV | 1.96 | rust-version in [workspace.package] of Cargo.toml |
| Formatter style edition | 2024 | style_edition in rustfmt.toml |
The pinned toolchain
rust-toolchain.toml is:
[toolchain]
channel = "1.98.1"
components = ["rustfmt", "clippy"]
profile = "minimal"
- rustup reads this file, so every
cargocommand run inside the repository uses Rust 1.98.1 withrustfmtand Clippy. - CI installs the same toolchain from the same file.
The documentation workflow in
.github/workflows/docs.ymlalready does this. - Pinning makes lint and formatting results reproducible. A new stable release can add Clippy lints, and with warnings denied in CI an unpinned toolchain would break unrelated pull requests on release day.
Bumping the toolchain
- The toolchain is bumped only by a pull request that changes
rust-toolchain.toml. - The same pull request fixes any new warnings, so
mainstays green. - Bumps normally follow a new stable Rust release within one six-week release cycle, not on its first day, so that early point releases are picked up.
- A toolchain bump that also raises the MSRV follows the MSRV rules below.
Minimum supported Rust version
The MSRV is the oldest Rust release that the workspace’s crates are guaranteed to build with.
| Period | Crates | MSRV rule |
|---|---|---|
| Before 1.0 | All crates | Pinned stable minus 2 minor versions |
| After 1.0 | Library crates | Latest stable minus 6 minor versions, about eight months of releases |
| After 1.0 | Binary crates (shiin, shiind, and other executables) | May track latest stable minus 2 |
Today the pinned stable is 1.98, so the MSRV is 1.96.
The longer window after 1.0 matters most for shiin-schema and shiin-client,
which other projects embed with their own compiler constraints (see Workspace and crates).
Most users install binaries prebuilt, so binaries can adopt new compiler features sooner.
Declaring the MSRV
- The MSRV is declared once, as
rust-versionin[workspace.package], and inherited by every member. - After 1.0, a binary crate that tracks a newer MSRV overrides
rust-versionin its own manifest. - Resolver 3 is MSRV-aware.
When Cargo resolves dependency versions, it prefers versions whose declared
rust-versionis compatible with the workspace’s, so updating the lockfile does not silently require a newer compiler. The resolver can only use what dependencies declare, so the MSRV check below is still needed.
Raising the MSRV
- An MSRV bump is allowed only in a minor release, never in a patch release. Before 1.0, a minor release is a change of the second number, for example 0.3.x to 0.4.0.
- Every MSRV bump is listed as its own item in the changelog of that minor release.
- The pull request explains which language or library feature, or which dependency, needs the newer compiler.
- The MSRV is never raised beyond what the rules in the table allow.
Checking the MSRV
The MSRV is checked with cargo-hack, which runs the check with each package’s declared rust-version:
$ cargo hack check --rust-version --workspace
This check is planned to run in CI on every pull request; see Continuous integration. It builds only; tests run on the pinned toolchain.
Nightly Rust
Nightly Rust is used for exactly two things:
- Fuzzing, because cargo-fuzz needs nightly compiler features.
- Miri, the interpreter that detects undefined behavior in
unsafecode.
Nightly is never required of contributors. Building, testing, linting, and checking the documentation all work on the pinned stable toolchain. CI jobs that need nightly use a dated nightly named in the workflow, updated by pull request, so a nightly regression cannot break them unannounced. See Testing strategy and Unsafe code policy.
Setting up
-
Install rustup by following the instructions at rustup.rs.
-
From the repository root, install the pinned toolchain named in
rust-toolchain.toml, then run a check to confirm the setup:$ rustup toolchain install $ cargo xtask docs-check -
Optionally, for fuzzing or Miri, install a nightly toolchain and cargo-fuzz:
$ rustup toolchain install nightly --component miri $ cargo install --locked cargo-fuzz
The documentation tools are installed as described in Documentation tooling and CI. Other Cargo tools used by CI, such as cargo-hack, cargo-nextest, and cargo-deny, are listed on the pages that use them.
Changing the baseline
- Toolchain and MSRV bumps within the rules on this page are ordinary pull requests.
- Changing the rules themselves, or moving to a new Rust edition, requires a new ADR that supersedes ADR-0034.
Coding standards
Note
Project policy: draft. Rules for how Rust code in this repository is formatted, linted, named, documented, and logged, and how commits are written. The formatter settings and the workspace lint table already exist; per-crate additions are planned.
These standards apply to every crate in the workspace, including xtask.
Most of them are enforced by tools, so a pull request that passes CI already follows them.
The rest are checked in review.
Formatting
-
Code is formatted with
rustfmtusing the repository’srustfmt.toml:style_edition = "2024" -
CI runs
cargo fmt --all --check, and a pull request with unformatted code fails. -
#[rustfmt::skip]is used only where formatting destroys meaning, such as an aligned table of constants, with a comment saying why. -
Editors pick up whitespace rules from
.editorconfig:root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = space indent_size = 4 [*.{md,toml,yml,yaml,json}] indent_size = 2
Lints
The workspace lint table
Every crate inherits this table from the root Cargo.toml with [lints] workspace = true.
It is quoted here exactly as it appears in the file:
[workspace.lints.rust]
unsafe_code = "forbid"
missing_docs = "warn"
unreachable_pub = "warn"
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -1 }
unwrap_used = "deny"
expect_used = "warn"
panic = "deny"
todo = "deny"
dbg_macro = "deny"
print_stdout = "deny"
print_stderr = "deny"
allow_attributes_without_reason = "deny"
undocumented_unsafe_blocks = "deny"
Why each forbid and deny exists
A panic or stray output in a hook process is not just a bug. Hosts can treat a crashed hook as permission to proceed, and some parse a hook’s output as its answer, so these lints protect fail-closed behavior as much as code quality.
| Lint | Level | Why |
|---|---|---|
unsafe_code | forbid | Unsafe code is not allowed outside shiin-platform. forbid, unlike deny, cannot be overridden by an allow or expect inside the crate. See Unsafe code policy. |
clippy::unwrap_used | deny | unwrap() turns an unexpected None or Err into a panic. Errors are propagated with ? and handled as described in Error handling. |
clippy::panic | deny | An explicit panic! in a hook or the engine can crash the process that was supposed to answer deny. |
clippy::todo | deny | Unfinished code does not merge. A todo! reached at run time panics. |
clippy::dbg_macro | deny | dbg! writes to standard error, bypasses logging configuration, and can print sensitive values. |
clippy::print_stdout | deny | In a hook, standard output carries the response the host parses, so a stray line can corrupt the decision. Libraries never print. |
clippy::print_stderr | deny | Standard error can reach the host and the agent, so stray output can leak internal detail. Diagnostics go through tracing or a binary’s dedicated output module. |
clippy::allow_attributes_without_reason | deny | Every lint suppression states why it is needed, so reviewers can judge it and remove it when the reason is gone. |
clippy::undocumented_unsafe_blocks | deny | Every unsafe block carries a // SAFETY: comment. It matters only in shiin-platform, but it is set everywhere so the rule cannot be forgotten. |
The warn-level entries matter too, because CI denies all warnings:
| Lint | Level | Why |
|---|---|---|
missing_docs | warn | Public items are documented; see Documentation comments. |
unreachable_pub | warn | Items marked pub that are not reachable from the crate’s public API become pub(crate), so the public surface that SemVer checks see is the real one. |
clippy::pedantic | warn, priority -1 | Enables the pedantic group. The lower priority lets individual entries in the same table override lints from the group. |
clippy::expect_used | warn | expect is allowed only where an invariant makes failure impossible, and the suppression must say which invariant. |
clippy::panic and clippy::todo do not cover unimplemented!, indexing, or arithmetic overflow.
Reviewers treat unimplemented! like todo!, and the engine crates add the lints below for indexing and arithmetic.
Warnings in CI
CI runs Clippy with every warning denied:
$ cargo clippy --workspace --all-targets --all-features -- -D warnings
Locally, warn-level lints stay warnings, so work in progress still compiles.
Nothing with a warning merges.
The documentation workflow runs the same command without --all-features today, because xtask defines no features; see Continuous integration.
Planned additions for the engine, classification, and policy crates
shiin-engine, shiin-classify, and shiin-policy handle input that an untrusted agent controls, such as paths, command lines, and counters.
When they are created, they add two lints:
| Lint | Level | Why |
|---|---|---|
clippy::indexing_slicing | deny | Indexing with [] panics when out of bounds. These crates use get and handle None. |
clippy::arithmetic_side_effects | warn | Integer overflow panics in debug builds and wraps silently in release builds, and a wrapped counter could let an agent exceed a task limit. These crates use checked or saturating arithmetic. |
Cargo does not let a crate combine [lints] workspace = true with its own lint entries,
so these crates declare the additions as attributes at the top of lib.rs:
#![allow(unused)]
fn main() {
// crates/shiin-engine/src/lib.rs (planned)
#![deny(clippy::indexing_slicing)]
#![warn(clippy::arithmetic_side_effects)]
}
The shiin-platform exception
shiin-platform, if it is created, is the only crate that does not inherit the workspace table.
A crate that inherits unsafe_code = "forbid" cannot allow unsafe code anywhere, so shiin-platform declares its own [lints] table instead.
That table copies every workspace entry, changes unsafe_code to deny so individual modules can opt in with #[expect],
and adds stricter lints for unsafe code:
# crates/shiin-platform/Cargo.toml (planned; the other workspace entries are copied unchanged)
[lints.rust]
unsafe_code = "deny"
unsafe_op_in_unsafe_fn = "deny"
[lints.clippy]
undocumented_unsafe_blocks = "deny"
multiple_unsafe_ops_per_block = "deny"
A planned xtask check compares this table with the workspace table so the copy cannot drift.
Suppressing a lint
- Suppress a lint with
#[expect(lint, reason = "...")], not#[allow].expectproduces a warning when the lint stops firing, so stale suppressions are found and removed. - Put the attribute on the smallest item that needs it.
- The reason says why the lint is wrong in this case, not what the lint does.
#[allow(lint, reason = "...")]is acceptable only where the lint fires on some targets or feature combinations but not others, becauseexpectwould then warn on the configurations where the lint does not fire.- Today, the workspace table enforces the reason, and review enforces the preference for
expect.
xtask shows the pattern for a binary that legitimately writes to the terminal:
#![allow(unused)]
#![expect(
fn main() {
clippy::print_stdout,
clippy::print_stderr,
reason = "docs-check is a terminal command that reports its findings"
)]
}
Naming and API design
- Follow the Rust API Guidelines, in particular their naming, conversion (
as_,to_,into_), and getter conventions. - Use glossary terms for type and function names:
ActionRequest,Decision,PolicySnapshot,ApprovalRequest, andGrant, not synonyms. - Decision values are
Allow,Deny, andPendingin Rust, matchingallow,deny, andpendingon the wire. - Engine and policy types never contain host tool names; host names appear only in adapter modules.
- Public enums that may gain variants are
#[non_exhaustive]. - Identifiers such as agent IDs and request IDs are newtypes, not bare
Stringoru64values. - Functions that return a value the caller should not ignore, such as a
Decision, are#[must_use].
Documentation comments
- Every public item has a doc comment.
missing_docsenforces this. - Public functions, types, and traits include at least one example in their doc comment. Examples are doctests, so they compile and run in CI.
- Functions that return
Resulthave an# Errorssection, and functions that can panic have a# Panicssection. The pedantic Clippy group checks both. - Every crate has crate-level documentation (
//!) stating its responsibility and, before 1.0, whether its API is internal. - Doc comments describe behavior and contracts. When a normative specification defines the behavior, the comment links to it instead of restating it.
Logging
- Code logs with
tracingevents and spans. - Libraries emit events but never install a subscriber, and they never print. Binaries configure where logs go.
- The hook path never writes logs to standard output, which carries the host protocol. Where log files live is described in State and storage.
shiin-enginedoes not log. It returns a trace as part of its result, which keeps it pure and makes explanations reproducible.- Levels:
errorfor failures that lead to a fail-closeddeny,warnfor degraded operation,infofor lifecycle events, anddebugortracefor development detail.
No secrets in logs
- Never log raw tool input, file contents, command output, environment variables, tokens, approval secrets, or key material.
- Log identifiers and digests instead, such as a request ID or a payload hash.
- Types that hold secrets implement
Debugby hand so that they print a placeholder, never their contents. - The audit log is not a log in this sense; its redaction rules are in Audit events.
- See also Privacy and telemetry.
Commit messages
Commits follow Conventional Commits 1.0.0, because release tooling generates changelogs from them (see Release engineering).
- The subject is
type(scope): summary, written in the imperative and in lowercase, with no trailing period. - Types are
feat,fix,perf,refactor,test,docs,build,ci, andchore. - The scope is the crate name without the
shiin-prefix (engine,classify),xtask, or a documentation area (spec,security). - A breaking change adds
!after the scope and aBREAKING CHANGE:footer explaining the migration. - Every commit carries a Developer Certificate of Origin sign-off, which
git commit -sadds.
$ git commit -s -m "fix(classify): treat unrecognized shell builtins as opaque"
Error handling
Note
Project policy: draft. How Shiin’s crates represent, report, and recover from failures. Product crates implement fail-closed hook handling; some diagnostics and error-boundary details below remain design requirements.
Shiin guards actions for an untrusted agent. Its error handling therefore has one overriding goal: a failure must never let an action through. This page follows ADR-0036, and the resulting failure semantics are specified in the security model and ADR-0013.
Taxonomy
Every unhappy outcome belongs to exactly one of four categories.
| Category | Examples | Representation | What happens |
|---|---|---|---|
| Decisions | A rule denies a write; an action needs approval | A Decision value, returned normally | The adapter enforces it. It is never an Err. |
| Operational errors | An unreadable policy file, a failed audit write, a malformed host payload, an IPC timeout | A per-crate thiserror enum marked #[non_exhaustive] | Mapped to a fail-closed deny in one place, logged with its error code |
| User diagnostics | A syntax error in policy.toml, a shadowed rule | Diagnostic data with source spans, rendered by the CLI with miette | Shown to the operator; at run time, an invalid policy still fails closed |
| Bugs | A violated internal invariant | debug_assert! in development builds; no panics on the engine hot path | Fixed in code. If a hook panics anyway, its panic hook answers deny. |
Decisions are not errors
A deny is a successful evaluation that reached a negative answer.
It is returned as Ok, or as a plain Decision, never as an Err.
This keeps ? from accidentally turning an authorization result into an error path, and it keeps error handling free to treat every Err as a failure.
A pending decision is also a decision, not an error.
Operational errors
- Each library crate defines its own error enum with
thiserror. A large crate may define one enum per major module. - Error enums are
#[non_exhaustive], so adding a variant is not a breaking change. - Variants carry structured context, such as the path or key involved, not preformatted strings.
- An error that wraps another marks it with
#[source]or#[from]and does not repeat the inner message in its own, so reporting the error chain does not print the same text twice. - Variants never carry secret values such as file contents, tokens, or key material. Paths, keys, identifiers, and digests are allowed.
- Libraries do not use
anyhow,eyre, orBox<dyn Error>in their APIs. Callers must be able to match on errors, and every error must keep its stable code.
User diagnostics
- Errors in operator-written input, above all policy files, are reported with the exact location and a suggested fix.
shiin-policyexposes diagnostics as plain data: an error code, a message, source spans, and optional help text.shiin-clirenders that data withmiette, showing the offending lines of the policy file. Keepingmiettein the binary keeps rendering concerns out of the library.- Commands such as
shiin policy validatereport every diagnostic they find, not only the first. - Diagnostics are for the operator. They never reach the agent.
Bugs
- The engine hot path is
shiin-engine’s evaluation function and everything it calls. It contains nounwrap,expect,panic!, unchecked indexing, or unchecked arithmetic, enforced by the lints in Coding standards. debug_assert!is fine for documenting internal invariants. It is compiled out of release builds and turns invariant violations into test and fuzzing failures.- A condition that untrusted input can trigger is never only asserted. It is handled as an operational error.
- Outside the hot path,
expectis allowed where an invariant makes failure impossible, with a lint suppression naming the invariant.
Errors become decisions in exactly one place
An operational error that occurs while handling an action must end in deny.
That conversion is implemented once:
a single function in shiin-engine (shown below as deny_on_failure, a placeholder name) is the only code that constructs a deny decision for a failure.
- The hook binary, the daemon, and the MCP proxy route every
Errto that function at their outermost request handler. None of them builds its own failure decision. - The engine uses the same function for failures inside evaluation, so evaluation itself never returns an
Err. - The resulting decision carries the failure reason code defined in Decision. The operational error’s code is recorded in the audit event and the log, not in the agent-facing message.
- Failures that happen after a decision but before the adapter answers, such as a failed audit write for a non-read action, also go through this function,
so the answer becomes
deny(ADR-0030).
A single conversion point means there is one place to review, test, and fuzz, and no code path can invent a different, more permissive answer to a failure.
Error codes
- Every operational error variant and every diagnostic has a stable code, for example
SHIIN-POL-0003. - Codes are listed, with their meaning and remedy, in Error codes, which is the only home of the list.
- A code is never renumbered or reused, even after the error it named is removed.
- Codes appear in CLI output, logs, and audit events, so a report can be matched to documentation without the full message.
- A planned test checks that every code used in the source appears in the reference and that every code in the reference is used or marked retired.
Message style
- Messages start with a lowercase letter and have no trailing period:
failed to read policy file "/home/dev/project/.shiin/policy.toml". - Proper nouns and code identifiers keep their case.
- A message says what failed, not how to fix it. Fixes go in diagnostic help text or in the error-code reference.
- Messages are concise and avoid words such as “error” or “fatal”, which the reporting context already conveys.
What the agent sees
Internal errors never reach the untrusted agent verbatim.
| Audience | What it receives |
|---|---|
| Agent, through the host | The deny decision and the fixed agent-facing message for failures defined in Decision. No error text, path, error code, or stack trace. |
| Operator | CLI diagnostics, the error code, and the full error chain in logs |
| Audit log | An event recording the failure reason code and the error code, without secret values |
Error messages can reveal file layout, policy structure, or which check failed, which an agent could use to probe for a bypass. The threat model treats deny-reason oracles as an in-scope threat.
The hook binary’s panic hook
Hosts treat a crashed hook as permission to proceed, so a panic in the hook binary must still produce deny.
- The first statement of the hook binary’s
maininstalls a panic hook withstd::panic::set_hook, before any input is read. - The panic hook writes the host’s deny response and exits with the host’s deny exit status. The response encoding for each host is defined on that host’s integration page and in the adapter contract.
- The response is a fixed, pre-encoded byte string prepared at start-up. The panic hook does not allocate new buffers, take locks that the panicking code might hold, or call code that can panic again.
- The panic hook runs before unwinding or aborting, so it works with either panic strategy.
- A separate deadline watchdog answers
denybefore the host’s hook timeout, as the adapter contract requires.
The panic hook does not cover every crash. A process killed by a signal, the operating system’s out-of-memory handler, or a stack overflow exits without running it, and some hosts treat that exit as permission to proceed. The hook path therefore avoids unbounded recursion and unbounded allocation driven by untrusted input, and the residual risk is recorded in the security model. A fault-injection test that makes the hook panic and time out is an exit criterion for v0.1; see Testing strategy.
Illustrative example
The following Rust is illustrative only. It illustrates the design and is not a copy of the current source API.
#![allow(unused)]
fn main() {
// Illustrative only: not real Shiin code. Names and codes are placeholders.
use std::path::PathBuf;
/// Errors from loading and compiling a policy.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PolicyError {
/// The policy file could not be read.
#[error("failed to read policy file {path:?}")]
Read {
/// The file that could not be read.
path: PathBuf,
/// The underlying I/O error.
#[source]
source: std::io::Error,
},
/// The policy uses a key that the policy syntax does not define.
#[error("unknown key `{key}` in policy")]
UnknownKey {
/// The unrecognized key.
key: String,
},
}
impl PolicyError {
/// Returns the stable code listed in the error-code reference.
#[must_use]
pub const fn code(&self) -> ErrorCode {
match self {
Self::Read { .. } => ErrorCode::new("SHIIN-POL-0001"),
Self::UnknownKey { .. } => ErrorCode::new("SHIIN-POL-0003"),
}
}
}
/// The hook binary's outermost request handler.
///
/// Every failure is routed to the single conversion function; nothing else
/// constructs a failure decision.
fn handle(input: &[u8], state: &HookState) -> Decision {
match evaluate_hook_input(input, state) {
Ok(decision) => decision,
Err(error) => {
tracing::error!(code = %error.code(), error = %error, "evaluation failed, denying");
shiin_engine::deny_on_failure(error.code())
}
}
}
}
Unsafe code policy
Note
Project policy: draft. Where
unsafeRust is allowed, and what every use of it must include. The workspace-wideforbidalready exists inCargo.toml;shiin-platformdoes not exist.
Unsafe Rust turns off compiler checks that Shiin relies on to guard actions for an untrusted agent. A memory-safety bug in Shiin could let a crafted input change a decision, so unsafe code is forbidden everywhere except one small, heavily reviewed crate. This page follows ADR-0037.
The rule
unsafe_code = "forbid"is set in[workspace.lints.rust]in the rootCargo.toml. Every crate that inherits the workspace lints, includingxtasktoday, cannot containunsafeblocks,unsafe fn, orunsafe impl.forbidcannot be overridden by analloworexpectattribute inside the crate.- The only exception is
shiin-platform.
shiin-platform
shiin-platformis created only when a concrete feature needs an operating-system facility that no safe crate provides. Until then, it does not exist.- The pull request that creates it links an ADR recording the need and the safe alternatives that were ruled out.
- It contains thin wrappers around operating-system calls and nothing else: no policy logic, no parsing of untrusted formats, and no business rules.
- Every wrapper exposes a safe API.
The crate has no
pub unsafe fn, so unsafety never leaks to callers. - Wrappers validate their inputs before the unsafe call and convert every failure into a typed error.
- It declares its own lint table instead of inheriting the workspace one, as described in Coding standards.
- It depends on no other Shiin crate, and
shiin-engineandshiin-schemanever depend on it; see Workspace and crates.
A likely first need is identifying the client process of a Windows named pipe for the local API, which safe crates may not cover. That is an expectation, not a decision.
Prefer safe wrappers
Before any code reaches for unsafe, try these in order:
- The Rust standard library.
- Crates that already encapsulate the unsafe call behind a safe, well-tested API.
On Unix-like systems that is usually
rustix; for sockets and pipes it can be the async runtime itself. - Only then, a wrapper in
shiin-platform.
On Windows, windows-sys is the preferred source of declarations instead of hand-written extern blocks.
Its functions are raw bindings, so every call to them is itself unsafe and belongs in shiin-platform.
Hand-written extern declarations are not used when a maintained binding crate provides the function.
transmute, static mut, and exported symbols such as #[unsafe(no_mangle)] are not used.
Requirements for every unsafe block
- A
// SAFETY:comment directly above the block, explaining which invariants make it sound and where they are established.clippy::undocumented_unsafe_blocksenforces the presence of the comment; review enforces its accuracy. - One unsafe operation per block, so each comment justifies exactly one thing.
shiin-platformenablesclippy::multiple_unsafe_ops_per_blockfor this. - A
# Safetysection on any privateunsafe fn, stating what callers must guarantee. Unsafe operations inside anunsafe fnstill need their ownunsafeblock (unsafe_op_in_unsafe_fn). - The smallest possible scope. Safe code does the preparation and the checking, and the unsafe block contains only the call.
- Tests that exercise the safe wrapper on every supported platform the wrapper targets, including error paths.
- Miri runs the crate’s tests on a nightly toolchain in CI.
Miri cannot execute most foreign-function calls, so tests that need the real operating system are skipped under
cfg(miri); Miri then covers the pure-Rust handling of pointers and buffers around the calls, and platform tests cover the rest. See Testing strategy. - Two-maintainer review. A change that adds or modifies unsafe code needs approval from two maintainers. While the project has fewer than two maintainers, it needs review from an outside reviewer experienced with unsafe Rust, named in the pull request. Unsafe changes are always security-sensitive changes.
Unsafe code in dependencies
Forbidding unsafe code in Shiin’s own crates does not remove it from the binaries: many dependencies contain some. Unsafe usage is one of the admission criteria in Dependencies and supply chain, and reviewers weigh:
- how much unsafe code the crate contains, and whether it is necessary for the crate’s purpose;
- whether its unsafe blocks are documented and tested, for example with Miri or fuzzing;
- the crate’s history of soundness advisories in the RustSec database and how quickly they were fixed;
- whether a safe alternative of similar quality exists.
Tools that count unsafe usage inform this review but do not decide it.
Soundness bugs
A soundness bug in shiin-platform, or one in a dependency that affects Shiin, is handled as a potential vulnerability
through Vulnerability disclosure, not as an ordinary bug.
Scope notes
- The fuzz targets in
fuzz/form a separate workspace and do not inherit the workspace lints. They contain no unsafe code of their own. - Code generated by macros from dependencies is covered by the dependency review above.
Async and concurrency
Note
Project policy: draft. This page is the only home of Shiin’s async and concurrency rules (ADR-0024).
Engine
The engine is pure and synchronous. It performs no I/O and uses no async runtime.
Adapters and services
Adapters and services may use Tokio, but it is confined:
- The runtime is created in the binary and shut down before exit.
- Libraries do not create their own runtime.
- Engine crates never depend on an async runtime.
Concurrency
Evaluation is deterministic. Concurrent evaluations must not share mutable state.
Read next
Dependencies and supply chain
Note
Project policy: draft. This page is the only home of Shiin’s dependency and supply-chain rules (ADR-0038).
Admission
Every third-party dependency must be admitted with a reason. Reviewers weigh:
- How much unsafe code the crate contains, and whether it is necessary.
- Whether its unsafe blocks are documented and tested.
- Its history of soundness advisories in the RustSec database.
- Whether a safe alternative of similar quality exists.
Declaring dependencies
Key storage uses keyring-core with explicit native platform stores, following ADR-0032.
The Linux store uses the vendored dbus-secret-service-keyring-store with its Rust cryptography feature; macOS and Windows retain their native stores.
The store contract uses the same service and username attributes and requires encrypted Secret Service communication (verified 2026-10-18).
This removes the prior Zbus dependency chain identified by RUSTSEC-2026-0370, RUSTSEC-2026-0371, and RUSTSEC-2026-0372.
No advisory or yanked-crate check is disabled.
Vendored native DBus code adds a C dependency within the store; the native key-store lifecycle job exercises it against the runner’s actual Secret Service.
The v0.6 downstream MCP HTTP transport admits reqwest for HTTP and TLS rather than implementing either protocol in Shiin.
It is confined to the proxy binary; it does not enter the engine crates.
The SDK transport uses its asynchronous client with redirects, automatic retries, and environment proxies disabled.
axum is admitted only as a development dependency to run the HTTP MCP fixture; it adds no product listener.
Both dependencies disable default features and are checked by the existing dependency-policy CI job.
Transitive HTTP/TLS dependencies still require supply-chain review before a release.
Development-only rcgen, rustls, tokio-rustls, and hyper-util generate a temporary certificate and serve the authenticated TLS MCP fixture.
Private fixture keys are generated at runtime and are never committed.
allocation-counter is admitted only for the optional xtask/memory-profile feature.
It measures allocations made by synchronous component calls through a safe API; no product binary or engine dependency graph includes the counter.
The measurement contract counts only the current thread (verified 2026-10-16).
Its allocator wrapper contains the unsafe system-allocator calls; Shiin adds no unsafe allocator implementation.
The ordinary latency jobs keep the default allocator, so instrumentation does not change the reported latency samples.
This admission is for measurement tooling and does not replace dependency review before release.
Every dependency is declared in [workspace.dependencies] with a version requirement and default-features = false.
Members reference it with workspace = true and enable only the features they use.
Supply chain
cargo denyis enforced in CI, following ADR-0038;cargo vetis not configured.- Dependencies are updated through ordinary pull requests.
- A dependency that gains a soundness advisory is treated as a potential vulnerability.
Where the policy lives
deny.toml at the repository root is the enforced form of this policy, and ci.yml runs
cargo deny check on every pull request. Two decisions in that file are worth knowing:
- Two extra licenses are allowed:
Apache-2.0 WITH LLVM-exception, which is standard for the Rust project’s own low-level crates, andMIT-0, which is MIT without the attribution requirement. Both are permissive. - Wildcard versions are refused, except for path dependencies. Shiin’s own crates are path dependencies inside this workspace, so they carry no version requirement to review. An external wildcard would let an unreviewed upgrade arrive silently.
Duplicate versions warn rather than fail, because they are usually a transitive dependency’s choice rather than ours.
Read next
Testing strategy
Note
Project policy: draft. This page is the only home of Shiin’s testing rules.
Levels
| Level | What it covers | Where |
|---|---|---|
| Unit | Single functions and types | In-module tests modules. |
| Integration | Crate boundaries and the engine | crates/*/tests/. |
| Conformance | The test vectors | docs/spec/test-vectors/. |
| Fault injection | Crashes, timeouts, damaged state, and failed audit writes | CLI unit and integration tests. |
| Platform | One supported platform each | CI jobs. |
Test vectors
Every test vector in docs/spec/test-vectors/ must pass before a milestone is considered complete.
cargo xtask conformance packages the core vectors, schemas, and a revision-bound result report; the documentation workflow publishes the bundle as a downloadable artifact.
The CLI evidence tests also sign, encrypt, verify, decrypt, and replay each vector, using fixture keys without a live OS keyring.
Client integration tests connect to the actual Unix daemon service over a local socket to check both sides of the JSON-RPC contract.
cargo xtask intent-evaluation reports the task-labelled corpus against a policy-only baseline and fails if a decision or reason disagrees with its label.
cargo xtask dependency-check rejects known network stacks and async runtimes in the normal transitive dependency graph of the four engine crates.
CI separately checks the isolated engine for WebAssembly and tests the schema crate without OS clock/randomness functions.
The hosted release measurements also exercise 100/500-rule policies and hook calls against a thousand-event audit log, checking every observed latency against the existing budgets.
The separate optional heap measurement checks parsing/compilation and engine allocations with 1/100/500 rules; its instrumentation is confined to xtask, and latency samples retain their ordinary allocator.
Fuzzing
fuzz/ is a separate nightly workspace with wire-request, classification, and authorization targets.
The wire target parses and validates action requests and checks that validated records survive a serialization round trip.
The classification target exercises shell tokenization and verifies that unsupported SQL syntax stays opaque.
The authorization target exercises deterministic decisions for validated arbitrary requests, unattended non-pending behavior, and refusal when capabilities are absent or explicitly denied.
It uses a fixed compiled policy and re-evaluates the gate with stricter capability inputs; the seed requests come from the full golden corpus.
cargo xtask fuzz-smoke seeds the targets from published action vectors and representative command/SQL inputs, then runs each for twenty seconds with a 64-KiB input bound and a 1-GiB resident-memory limit.
The hosted robustness workflow runs this command on nightly-2026-10-15; no fuzz build is required on a developer’s laptop.
These bounded smoke runs do not establish exhaustive parser or engine security coverage.
Miri
The hosted robustness workflow interprets the schema tests without runtime clocks or randomness on nightly-2026-10-15.
This covers pure wire validation and timestamp formatting, not native Cedar, TLS, OS key stores, or IPC.
Native key stores
cargo xtask native-keyring-check runs an explicitly opted-in lifecycle test on ephemeral CI runners.
It exercises the actual platform backend through the same private store implementation as production, using a random shiin-EXAMPLE-native-test- service namespace.
The production service remains fixed at shiin; no environment setting or public API changes its namespace.
The test checks persistence, signing and verification, rotation, retained-key replay, wrong-key rejection, deleted keys, and corrupt selected entries without implicit replacement.
Cleanup deletes only entries created in the isolated namespace.
Ordinary workspace tests skip this test, so they do not access an operator’s key store.
The Linux job starts a temporary D-Bus session and unlocks an isolated runner’s Secret Service with an explicit EXAMPLE fixture password through stdin.
The daemon’s foreground and unlock behavior follows its manual (verified 2026-10-16).
The Rust task owns and stops the test daemon.
macOS and Windows jobs use their runner’s native backend.
These tests do not establish protection against arbitrary unmediated code running as the same operating-system user.
Property tests
The Unix approval state machine has transition-sequence tests for expiry, revocation, binding, and single-use grant consumption. Cross-platform service tests and the independent security review remain open.
Read next
Performance budgets
Note
Project policy: draft. This page is the only home of Shiin’s performance budgets.
Latency
| Path | Budget | Platform |
|---|---|---|
| Hook evaluation | 50 ms | Tier 1 |
| Hook total including process start | 200 ms | Tier 1 |
| Policy compilation | 1 s | Tier 1 |
Memory
| Path | Budget |
|---|---|
| Engine evaluation | 10 MB |
| Policy compilation | 100 MB |
Measurement
The ci.yml release-latency jobs run the following measurements on hosted Linux and macOS runners.
The same commands provide a repeatable local latency sample:
$ cargo build --release --workspace --locked
$ cargo run --release --locked -p xtask -- performance target/release/shiin
$ cargo run --release --locked -p xtask -- performance-scaling target/release/shiin
$ cargo run --release --locked -p xtask --features memory-profile -- memory
The command checks every observed latency against the existing budgets and reports median, p95, and maximum. Hook samples include process start, policy loading, evaluation, and durable audit writes against a small growing log. They exercise the real hook executable without launching a host. The scaling command checks engine/compilation latency with 100 and 500 rules and complete hook latency with 100 rules and an audit log seeded with 1,000 events. The hosted Linux and macOS jobs run both commands against the same budgets and refuse to measure a fixture that returns a denial. The optional memory command measures peak live heap bytes allocated during policy parsing/compilation and during each of 25 engine calls with 1, 100, and 500 rules. It enforces the 100-MB compilation and 10-MB evaluation budgets using decimal megabytes. Compilation includes the resulting source/compiled policy; evaluation starts with those fixtures already resident and includes decision creation and drop. The allocation counter records the current thread only (verified 2026-10-16). These are heap deltas, excluding pre-existing fixtures, stack, allocator overhead, native allocations, and other threads. The reported Linux process peak RSS includes the harness and dependencies and remains a separate observation. Neither measurement establishes a total process or component resident-memory limit. Audit append and verification now stream existing records, retaining the current chain head instead of all prior event payloads. Append still checks the entire chain and syncs the new record before success; tests seed a thousand-event chain and reject early, middle, and late corruption without changing the file. Reading the log for display still collects its records, and individual-record bounds remain open. Workloads beyond those bounded scaling samples, full resident-memory profiling, and results on other platform architectures remain open.
Read next
Continuous integration
Note
Project policy: draft. This page is the only home of Shiin’s CI rules.
Workflows
| Workflow | When it runs | What it does |
|---|---|---|
ci.yml | Pull requests and pushes to main | Formatting, lints, and tests on each platform tier; release latency measurements on Linux and macOS; isolated engine WebAssembly check; the minimum supported Rust version; cargo deny; and a check that every action is pinned by commit |
docs.yml | Pull requests, pushes to main, and manual dispatch | Documentation checks, stale-specification warnings, intent measurements, dependency checks, and book publication from main |
robustness.yml | Pull requests and pushes to main | Native key-store lifecycle checks on Linux, macOS, and Windows; bounded parser fuzzing and pure wire-type tests under Miri on a dated nightly |
release.yml | Pull requests and a version tag | Builds and qualifies five Unix targets, generates SBOMs and dependency metadata, attests archives, and publishes tagged releases; generated by dist and repaired by cargo xtask release-ci-fix |
sbom.yml | Manual dispatch | Produces an additional JSON CycloneDX diagnostic as a build artifact |
link-health.yml | Weekly and manual dispatch | Checks public links with bounded concurrency and updates one bot-owned issue when links fail |
Tests run on Tier 1 platforms as a blocking check, and on Tier 2 without blocking; see platform support.
Permissions
The docs workflow has read-only contents permission and does not persist checkout credentials.
Regenerating the release workflow
release.yml is generated by dist generate and repaired by the repository’s Rust automation:
$ dist generate
$ cargo xtask release-ci-fix
$ cargo xtask release-ci-fix --check
The ci.yml pin and generated-template checks fail if regeneration leaves a tag reference or omits the expected archive/SBOM qualification.
Third-party actions
Third-party actions are pinned by commit SHA, with the release tag in a comment.
Nightly jobs
Nightly jobs use a dated nightly named in the workflow, updated by pull request.
Read next
Release engineering
Note
Project policy: draft. This page is the only home of Shiin’s release rules (ADR-0039).
Commits
Commits follow Conventional Commits 1.0.0. Release tooling generates changelogs from them.
Sign-offs
Every commit carries a Developer Certificate of Origin sign-off, which git commit -s adds.
Versions
Versions before 1.0 may contain breaking changes. After 1.0, patch, minor, and major versions follow semantic versioning.
Builds
The first alpha builds Linux and macOS archives, including a musl target configured for static linking. Windows remains covered by source-level CI; Windows path behavior and daemon transport are not qualified for an initial binary release.
Artifacts
Release archives have signed build-provenance attestations, SHA-256 checksums, and CycloneDX bills of materials. Platform code signing and notarization are not configured; a provenance attestation is separate from an operating system’s executable signature.
What is wired up today
The first published release is v0.1.0-alpha.1.
Its tagged workflow publishes the CLI and Unix-daemon archives, installers, checksums, build provenance, and SBOMs.
The candidate pipeline is exercised on pull requests before publication:
| Piece | State |
|---|---|
dist-workspace.toml with five Unix targets and shell installers | configured for CLI and daemon archives |
release.yml, generated by dist and repaired by Rust automation | candidate archives are built on hosted pull-request runners |
cargo xtask release-smoke <artifact-directory> | checks archive checksums, extraction, executable versions, all three hook protocols, and audit verification |
Build provenance through github-attestations | configured for release archives |
| CycloneDX bills of materials | generated and uploaded in the release workflow; sbom.yml is an optional manual JSON diagnostic |
cargo auditable dependency metadata | configured in dist |
| Publishing crates to a registry | deliberately not configured; every crate is publish = false until the naming decision in ADR-0003 is settled for a release |
The dist configuration supports embedded dependency metadata, SBOMs, pinned actions, and pull-request artifact builds (verified 2026-10-18).
After dist generate, run cargo xtask release-ci-fix.
That command repairs the generated SBOM output reference and cache-action pin, adds archive qualification, and prevents superseded candidate builds from accumulating.
Its check mode refuses unknown templates; CI runs cargo xtask release-ci-fix --check.
Tag publication is never canceled by a newer pull-request candidate.
The alpha remains experimental. It does not declare milestone exit criteria complete, freeze a public API, or replace independent security review.
First-alpha qualification record
The candidate implementation at 53cc41d2ae81b20e8b9430b901af8edfc7e4f3df passed its hosted qualification on 2026-10-18:
- Workspace and performance checks: formatting, Clippy, tests on Linux/macOS/Windows, MSRV, WebAssembly engine isolation, dependency policy, Linux/macOS latency, and bounded heap measurements.
- Robustness checks: native key-store lifecycle on all three systems, parser fuzzing, and wire types under Miri.
- Documentation and conformance: documentation checks and the 26-case conformance artifact.
- Release candidate artifacts: all five Unix archive targets, archive smoke checks, build attestations, and global packaging.
These results qualify that revision; they do not certify later source changes or establish live-host behavior.
The published tag points to merge commit 687836ae55d280e4492c34256f3be9cd044f6362, whose source tree matches the qualified final candidate da83b04.
The tagged publication run builds and qualifies the release archives before publication.
Human DCO certification and security-sensitive-change review remain human responsibilities; automation does not supply either certification.
Read next
Security-sensitive changes
Note
Project policy: draft. This page is the only home of Shiin’s security-sensitive change rules.
What is security-sensitive
A change is security-sensitive if it affects:
- The decision pipeline, including any evaluation stage.
- Policy compilation or evaluation.
- The audit log, receipts, or key management.
- Unsafe code in
shiin-platformor dependencies. - Adapter enforcement or fail-closed behavior.
Review
Security-sensitive changes need:
- A link to the relevant ADR or RFC.
- Review from a maintainer experienced in the area.
- For unsafe code, two-maintainer review or an outside reviewer.
Disclosure
A change that might be a way to bypass Shiin is handled through Vulnerability disclosure, not as an ordinary bug.
Read next
Architecture decision records
Note
Reference: accepted. The index of every architecture decision in the project.
An architecture decision record (ADR) captures one significant decision: the context, the options considered, the choice, and its consequences. ADRs are never deleted. A decision that changes is superseded by a new ADR that links back to it.
When to write an ADR
Write an ADR when a decision:
- constrains how several components or future contributors work,
- is expensive to reverse,
- chooses between credible alternatives, or
- changes a previous ADR.
Changes to accepted specifications, new action-type namespaces, and policy-language features go through the RFC process, and the RFC produces the ADR.
Statuses
| Status | Meaning |
|---|---|
proposed | A recommendation is written but not yet ratified, or its confirmation step is still pending |
accepted | The decision is in effect |
deferred | The question is recognized but intentionally postponed to a named milestone |
superseded | Replaced by a later ADR, which is linked |
deprecated | No longer relevant, with no replacement |
Process
- Copy the template to
NNNN-short-title.mdusing the next free number. - Open a pull request with status
proposed, and add the record to the index below. - A maintainer accepts the decision by changing its status when the pull request merges, following GOVERNANCE.md.
- To change a decision, write a new ADR that supersedes the old one, and update both records.
Index
Project
| ADR | Title | Status |
|---|---|---|
| 0001 | Record architecture decisions | accepted |
| 0002 | Rust for product and tooling | accepted |
| 0003 | Project name: Shiin | accepted |
| 0004 | Apache-2.0 license and DCO | accepted |
| 0005 | Governance model | accepted |
| 0006 | Vulnerability reporting | accepted |
| 0007 | No telemetry | accepted |
| 0008 | mdBook documentation | accepted |
Engine semantics
| ADR | Title | Status |
|---|---|---|
| 0009 | Deterministic enforcement | accepted |
| 0010 | Pure synchronous engine | accepted |
| 0011 | Decision values and combining | accepted |
| 0012 | Evaluation stage order | accepted |
| 0013 | Fail-closed failure semantics | accepted |
| 0014 | Cedar policy engine with TOML front-end | accepted |
Policy, classification, intent, and risk
| ADR | Title | Status |
|---|---|---|
| 0015 | Policy layering and trust | accepted |
| 0016 | Built-in protected resources | accepted |
| 0017 | JSON wire format and schemas | accepted |
| 0018 | Conservative action classification | accepted |
| 0019 | Path canonicalization | accepted |
| 0020 | Structured intent binding | accepted |
| 0021 | Discrete risk signals | accepted |
Deployment, API, adapters, and approvals
| ADR | Title | Status |
|---|---|---|
| 0022 | Local-first, daemonless deployment | accepted |
| 0023 | JSON-RPC local API | accepted |
| 0024 | Tokio, confined | accepted |
| 0025 | Adapter roadmap | accepted |
| 0026 | Adapter extensibility | accepted |
| 0027 | Approval resolutions and grants | accepted |
| 0028 | Deny-with-ticket for hosts that cannot wait | accepted |
Evidence
| ADR | Title | Status |
|---|---|---|
| 0029 | Hash-chained JSON Lines audit log | accepted |
| 0030 | Audit durability | accepted |
| 0031 | DSSE and Ed25519 receipts | accepted |
| 0032 | Signing key storage | accepted |
| 0033 | Enforcement levels | accepted |
Rust engineering
| ADR | Title | Status |
|---|---|---|
| 0034 | Edition, MSRV, and toolchain | accepted |
| 0035 | Workspace layout | accepted |
| 0036 | Error handling | accepted |
| 0037 | Unsafe code policy | accepted |
| 0038 | Supply chain policy | accepted |
| 0039 | Release tooling | accepted |
| 0040 | Platform support tiers | accepted |
Deferred
| ADR | Title | Status |
|---|---|---|
| 0041 | Remote approver transport | deferred to v0.6 |
| 0042 | Language bindings | deferred until after 1.0 |
ADR-0001: Record architecture decisions
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, ADR template, RFC process, Documentation style guide
Context and problem statement
Shiin is being designed in full before any product code is written, so many consequential choices are made at the same time. Contributors who arrive later need to know what was decided, why, and which alternatives were rejected. Without that record, settled questions get reopened, or a decision is reversed by someone who never saw its reasons. Discussion in issues and pull requests is hard to find later and carries no status. The project needs a durable, reviewable, and findable record of each significant decision.
Decision drivers
- Decisions are findable next to the specifications they shape, and versioned with them.
- A reader can tell whether a decision is in effect, awaiting confirmation, or replaced.
- History is preserved: a reversed decision still explains why it was once made.
- Records are plain Markdown that renders on GitHub and in the documentation book (ADR-0008).
- Writing a record is cheap enough that people actually do it.
Considered options
- MADR 4 records in
docs/adr/, superseded but never deleted - A shorter context, decision, and consequences format in
docs/adr/ - Decisions recorded only in issues, pull requests, and discussions
- A wiki of decisions outside the repository
Decision outcome
Chosen option: “MADR 4 records in docs/adr/”, because it keeps decisions versioned with the specifications
and makes every record state the options considered and how the decision will be confirmed.
- Each record follows the ADR template, which is based on MADR 4.
- Records are numbered sequentially as
NNNN-short-title.md, and a number is never reused. - Every record is listed with its status in the ADR index.
- Records are never deleted. A decision that changes is superseded by a new record, and the two records link to each other.
- Changes to accepted specifications, new action-type namespaces, and policy-language features go through the RFC process, and an accepted RFC produces an ADR.
- A record whose confirmation step has not yet run stays
proposed, even when its recommendation is firm. - A record explains a decision. Normative rules live in the specification the record links to.
Consequences
- Good, because the reasons for a design outlive the conversations in which it was made.
- Good, because the
proposedstatus keeps unconfirmed decisions visible, such as ADR-0014. - Good, because superseding instead of rewriting keeps an honest history.
- Bad, because significant changes carry the extra work of writing a record.
- Bad, because superseded records accumulate, and readers must follow links to reach the current decision.
Confirmation
cargo xtask docs-checkchecks that ADR numbers are unique, that every title starts with# ADR-NNNN:, that the index links every record, and that everyADR-NNNNreference points to an existing record.- Review rule: a pull request that changes what an accepted record decides is not merged. It adds a superseding record instead. Fixes to wording, links, and status fields are allowed.
- Review rule: a pull request that meets a criterion in When to write an ADR is not merged without a linked record.
Pros and cons of the options
MADR 4 records in the repository
- Good, because records are reviewed in the same pull requests as the specifications they affect.
- Good, because the template’s considered-options and confirmation sections force the reasoning to be written down.
- Bad, because the template is longer than a minimal format.
A shorter format in the repository
- Good, because records are quicker to write.
- Bad, because rejected options and confirmation steps are easy to leave out, and those are what later readers most need.
Issues, pull requests, and discussions only
- Good, because no extra files or process are needed.
- Bad, because decisions are scattered across threads, have no status, and cannot be checked automatically.
A wiki outside the repository
- Good, because pages are easy to edit.
- Bad, because wiki pages are not versioned with the specifications and do not ship with the documentation.
More information
- Status meanings and the process for adding a record are in the ADR index.
- Revisit this decision if the number of records makes the index hard to navigate.
ADR-0002: Rust for product and tooling
Note
Architecture decision: accepted. No product code exists yet. The
xtaskdocumentation tooling is the only Rust crate in the workspace today.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: Workspace and crates, Toolchain and MSRV, Documentation tooling and CI, ADR-0008, ADR-0034, ADR-0037, ADR-0042
Context and problem statement
Shiin runs on the path of every agent action: a host starts or calls an adapter, waits for its answer, and only then proceeds. The adapter parses untrusted input from agents and hosts, including shell commands, file paths, and tool payloads. Developers must be able to install Shiin without first installing a language runtime. The project also needs repository tooling for documentation checks and releases, and every extra language adds a toolchain that contributors and CI must install, update, and secure. The question is which language to use for the product, and whether the tooling uses the same one.
Decision drivers
- Memory safety in code that parses untrusted input.
- Predictable latency on the hook path, with no garbage-collection pauses, within the performance budgets.
- Fast process start-up, because v0.1 runs a new process for each hook invocation (ADR-0022).
- Self-contained binaries with no runtime to install.
- Direct, in-process use of the Cedar policy engine, whose reference implementation,
cedar-policy, is a Rust crate (verified 2026-09-14). - One toolchain for contributors, CI, and the supply-chain controls in ADR-0038.
Considered options
- Rust for the product and all tooling
- Rust for the product, with Python or Node.js scripts for tooling
- Go for the product and tooling
- TypeScript on Node.js for the product and tooling
Decision outcome
Chosen option: “Rust for the product and all tooling”, because it meets the safety, latency, and distribution drivers and lets the project run a single toolchain.
- Every product component is a Rust crate in one Cargo workspace, as listed in Workspace and crates.
- Repository tooling is Rust.
Checks run through
cargo xtask, and the documentation tools (mdbook,mdbook-mermaid,lychee,typos, andrumdl) are installed withcargo install --locked(ADR-0008). - Building, testing, documenting, and releasing Shiin require no Python and no Node.js. CI workflow files are configuration, not a second tooling language.
- Binaries are self-contained, and fully static on musl targets.
- The edition, MSRV, and toolchain pinning are decided in ADR-0034, and the rules for
unsafecode in ADR-0037. - Agents written in other languages integrate through the JSON wire format (ADR-0017) and, later, language bindings (ADR-0042).
Consequences
- Good, because memory-safety bugs are excluded from the parsers that handle untrusted input, outside the single crate allowed to contain
unsafecode. - Good, because the policy engine is linked in-process rather than called across a language boundary.
- Good, because contributors install one toolchain and CI caches one.
- Good, because other agent-authorization projects written in Rust, such as SECBLOK/belay, eqtylab/cupcake, and agentgateway, show the ecosystem suits this domain (verified 2026-09-14).
- Bad, because compile times are long, for CI and for first-time contributors.
- Bad, because fewer contributors know Rust than Python, TypeScript, or Go.
- Bad, because small repository tasks take more code as
xtaskcommands than as scripts. - Bad, because agent builders in other languages wait for bindings instead of getting a native library from the start.
Confirmation
- The documentation workflow described in Documentation tooling and CI installs only the Rust toolchain and Cargo-installed tools (planned: M0).
cargo xtaskis the single entry point for repository checks.- Review rule: a pull request that adds a Python, Node.js, or other non-Rust dependency to building, testing, documenting, or releasing Shiin is not merged without an RFC and a superseding ADR.
Pros and cons of the options
Rust for everything
- Good, because it satisfies every decision driver.
- Bad, because of the compile-time and contributor-pool costs listed above.
Rust product with scripting-language tooling
- Good, because scripting languages are quick for small repository tasks.
- Bad, because contributors and CI must install and secure a second toolchain and package ecosystem.
- Bad, because the Rust supply-chain controls do not cover that ecosystem.
Go
- Good, because Go is memory safe, compiles quickly, and produces self-contained binaries.
- Bad, because garbage collection adds latency variance on the hook path.
- Bad, because Cedar would be used through a separate implementation or a foreign-function boundary instead of the reference crate.
TypeScript on Node.js
- Good, because TypeScript is widely known, and some peers, such as Adirdabush1/cerberus, use it (verified 2026-09-14).
- Bad, because users must install a Node.js runtime, and every hook invocation pays its start-up cost.
- Bad, because a security component would depend on a second large package ecosystem.
More information
Revisit this decision only if a required capability has no viable Rust implementation. Demand for other languages is handled by ADR-0042, not by changing the implementation language.
ADR-0003: Project name: Shiin
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Documentation style guide, Overview
Context and problem statement
The project began under the working name “latch.” As it moved from a private sketch to a public specification, a permanent name was needed that conveys the project’s role: it stops an agent, briefly, to ask whether an action is permitted. The name must be pronounceable, memorable, and free of collisions on crates.io and standard domain names.
Decision drivers
- The name should evoke stopping or gating, without suggesting isolation or a blunt refusal of everything.
- It must not collide with an existing high-profile crate or domain.
- It should be language-neutral enough for an agent-agnostic tool.
Considered options
- Keep “latch” as the permanent name.
- “Gate” — conveys the gating function directly.
- “Shiin” — the Japanese word for the conceptual barrier that blocks or permits.
Decision outcome
Chosen option: “Shiin”, because it conveys the gating role in an understated, memorable way and is free of known collisions at the time of writing.
- The binary is
shiin, the daemon isshiind, matching the convention described in the glossary. - The former name “latch” may appear only in this record and in the landscape, as enforced by
cargo xtask docs-check.
Consequences
- Good, because the project has a distinctive, non-colliding name.
- Bad, because existing references to the old name in discussions need updating.
Confirmation
cargo xtask docs-checkrejects the former working name outside the two allowed pages.
Pros and cons of the options
Keep “latch”
- Good, because no migration of references is needed.
- Bad, because the name does not convey the gating role, and it is already used as a GitHub organization name (latchagent/latch, verified 2026-09-14).
“Gate”
- Good, because it is short and directly descriptive.
- Bad, because it is a common word with many existing packages and domains, and it implies a single point of control rather than layers.
“Shiin”
- Good, because it is distinctive and pronounceable.
- Bad, because readers outside Japanese-speaking contexts may not know the meaning.
More information
The name change does not affect the technical design. See ADR-0004 for the licensing implications of the rename.
ADR-0004: Apache-2.0 license and DCO
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Documentation style guide, README
Context and problem statement
Shiin is open-source software intended for adoption by developers and teams. It needed a license that is permissive enough for commercial and personal use, includes a patent grant, and is well understood by the ecosystem. Contributions needed a lightweight sign-off mechanism, not a separate contributor license agreement that creates friction for first-time contributors.
Decision drivers
- Permissive enough for broad adoption, including by agent vendors.
- Patent protection for contributors and users.
- Minimal contribution overhead.
- Compatibility with the Cedar dependency’s license.
Considered options
- Apache License 2.0 with Developer Certificate of Origin.
- MIT License.
- BSD 3-clause.
- A contributor license agreement in addition to the license.
Decision outcome
Chosen option: “Apache License 2.0 with Developer Certificate of Origin”, because Apache 2.0 provides an explicit patent grant that MIT and BSD do not, and the DCO adds no contributor friction.
- The license is Apache-2.0, as set in
[workspace.package].licenseinCargo.toml. - Every commit requires a DCO sign-off (
git commit -s); no contributor license agreement is used. - This choice is recorded in ADR-0004 and referenced from CONTRIBUTING.md.
Consequences
- Good, because contributors and users get a well-understood license with patent protection.
- Good, because the DCO sign-off is a one-line commit flag, not a separate document to sign.
- Bad, because Apache 2.0 is slightly longer than MIT or BSD, though this does not affect usage.
Confirmation
- The
LICENSEfile andNOTICEfile are present at the repository root. - All crates inherit
license = "Apache-2.0"from the workspace.
Pros and cons of the options
Apache 2.0 with DCO
- Good, because of the explicit patent grant.
- Good, because DCO is lightweight.
- Bad, because it is longer than MIT.
MIT
- Good, because it is short and widely recognized.
- Bad, because it has no explicit patent grant.
BSD 3-clause
- Good, because it is short and permissive.
- Bad, because it lacks a patent grant and adds a clause that rarely matters in practice.
Contributor license agreement
- Good, because it gives the project legal flexibility.
- Bad, because it creates friction for contributors and conflicts with the goal of minimal overhead.
More information
The DCO is documented at developercertificate.org. The Apache 2.0 license text is in LICENSE. Attribution notices are in NOTICE.
ADR-0005: Governance model
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, ADR-0001, ADR-0006, Governance overview
Context and problem statement
As an open-source project gains contributors, it needs a clear model for how decisions are made and how disagreements are resolved. Shiin needed a governance structure that scales from the founding maintainer to a community without requiring a corporate sponsor.
Decision drivers
- Simplicity for an early-stage project.
- Clear authority for security and release decisions.
- A path to broader contribution as the project grows.
- Alignment with the DCO-only contribution model.
Considered options
- Benevolent dictator for life (BDFL).
- Lazy consensus with a maintainer group.
- A formal steering committee from day one.
Decision outcome
Chosen option: “Lazy consensus with a maintainer group”, because it is lightweight for a small project and grows naturally into a broader governance model.
- The project works by lazy consensus: proposals that receive no objection after a reasonable review period are considered accepted.
- The founding maintainer is the initial sole maintainer, listed in
MAINTAINERS.md. - Security-sensitive decisions (vulnerability handling, releases) require maintainer approval.
- As the project grows, maintainers are added by consensus of existing maintainers.
- This model is documented in
GOVERNANCE.md.
Consequences
- Good, because it minimizes process overhead while the contributor base is small.
- Good, because it has a clear escalation path if consensus fails.
- Bad, because the founding maintainer is a single point of failure until more maintainers are added.
Confirmation
GOVERNANCE.mdandMAINTAINERS.mdexist at the repository root and describe the model.- ADRs are accepted and superseded by maintainers, as described in ADR-0001.
Pros and cons of the options
Benevolent dictator for life
- Good, because decisions are fast.
- Bad, because it centralizes power and has no clear succession plan.
Lazy consensus with a maintainer group
- Good, because it is lightweight and scales.
- Bad, because it can slow down when maintainers disagree.
Formal steering committee from day one
- Good, because it distributes power broadly.
- Bad, because it is heavyweight for a project with no code shipped yet.
More information
The governance model is revisited when the maintainer group grows beyond three members. See ADR-0005 for the full record.
ADR-0006: Vulnerability reporting
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, ADR-0005, Threat model, Vulnerability disclosure
Context and problem statement
Security vulnerabilities in Shiin could allow actions to bypass authorization, which would defeat the entire purpose of the project. The project needed a process for receiving, triaging, and fixing vulnerability reports that keeps details private until a fix is available.
Decision drivers
- Private reporting for active vulnerabilities.
- Clear response expectations for reporters.
- Coordination with downstream users and integrators.
- No requirement for reporters to create a GitHub account.
Considered options
- A
SECURITY.mdfile with a private email address and a PGP key, handled under embargo. - A public GitHub issue tracker only.
- A third-party vulnerability disclosure platform (for example, HackerOne).
Decision outcome
Chosen option: “A SECURITY.md file with private reporting”, because it is standard for Rust projects and requires no external service.
SECURITY.mdat the repository root documents the reporting process.- Reports are sent to a maintainer-controlled email address.
- Maintainers triage reports within 72 hours and respond within 7 days.
- Fixes are coordinated with an embargo and released with a security advisory.
- The threat model classifies bypass vulnerabilities as in-scope.
Consequences
- Good, because the process is standard and accessible.
- Good, because it does not depend on a third-party platform.
- Bad, because it relies on maintainer availability for initial response.
Confirmation
SECURITY.mdexists at the repository root and links to the vulnerability disclosure page.- The README links to
SECURITY.mdfor security-relevant reports.
Pros and cons of the options
SECURITY.md with email
- Good, because it follows Rust ecosystem conventions.
- Good, because it requires no account or platform.
- Bad, because it depends on maintainer email availability.
Public issue tracker only
- Good, because all discussion is public.
- Bad, because it discloses vulnerabilities before a fix exists.
Third-party platform
- Good, because it provides tooling for coordination.
- Bad, because it adds a dependency on an external service and may require accounts.
More information
The vulnerability disclosure page describes the full process. The threat model treats any bypass of a deny decision as in-scope.
ADR-0007: No telemetry
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Vision, Privacy and telemetry, ADR-0022
Context and problem statement
Shiin intercepts actions on the developer’s machine and records decisions about them. An authorization tool that reports usage back to a server creates a new attack surface and a conflict of interest: the tool’s incentives align with collecting data, not with keeping that data private. The project needed to decide whether any data leaves the machine.
Decision drivers
- The machine running an agent already has the operator’s secrets, so any network call is a potential leak.
- Trust must be earned before it can be required.
- A tool that collects no data does not need a data-retention or privacy-compliance program.
Considered options
- No telemetry or data collection by default; opt-in for any remote feature.
- Anonymous usage statistics with an opt-out.
- Mandatory telemetry for security and improvement.
Decision outcome
Chosen option: “No telemetry or data collection by default”, because Shiin handles security-sensitive actions and cannot ethically collect data without an explicit, informed opt-in.
- No events, metrics, crash reports, or usage data are sent anywhere by default.
- No HTTP client is a dependency of the engine crates.
- Any network feature (remote approvals, remote policy bundles) is explicitly opt-in and uses a separate transport crate.
- The daemon never phones home, even to check for updates.
- This decision is referenced from ADR-0022.
Consequences
- Good, because operators can run Shiin in sensitive environments without concern about data leakage.
- Good, because the absence of telemetry simplifies the supply chain and threat model.
- Bad, because the maintainers receive no usage signals about which features matter.
Confirmation
- Privacy and telemetry documents that no telemetry is collected.
- Use-case UC-08 requires the MCP proxy to be controllable without a remote service.
Pros and cons of the options
No telemetry, opt-in for remote features
- Good, because it earns trust and avoids a data-handling compliance burden.
- Bad, because maintainers have no passive usage data.
Anonymous usage statistics
- Good, because it provides development signals.
- Bad, because even “anonymous” data can be re-identified, and it requires an HTTP client in the default install.
Mandatory telemetry
- Good, because it maximizes data quality.
- Bad, because it is unacceptable for a security-adjacent tool.
More information
The threat model treats any outbound connection that is not explicitly opt-in as a security concern. See ADR-0022 for the deployment model that enables this.
ADR-0008: mdBook documentation
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, ADR-0002, ADR-0001, Documentation tooling and CI
Context and problem statement
Shiin’s documentation is written before its implementation, and it must render both on GitHub and as a dedicated site. The project needed a documentation generator that reads plain Markdown, supports cross-references and diagrams, and produces a searchable site without requiring a Node.js toolchain.
Decision drivers
- Renders the same Markdown on GitHub and as a site.
- Supports Mermaid diagrams (used throughout the design).
- No JavaScript runtime required to build.
- Integrates into the Rust toolchain that the project already uses.
Considered options
- mdBook with
mdbook-mermaid, vendored JavaScript. - A static-site generator written in Python or Node.js.
- Plain GitHub Pages with no build step.
Decision outcome
Chosen option: “mdBook with mdbook-mermaid”, because it is written in Rust, renders GitHub-compatible Markdown, and has a Mermaid preprocessor.
- The source lives in
docs/withSUMMARY.mdas the table of contents. - ADR-0002 requires the documentation toolchain to be installable with
cargo install --locked. - Mermaid renderer files are vendored (
docs/mermaid.min.js,docs/mermaid-init.js) so the built site works offline. - The built site output goes to
target/book/.
Consequences
- Good, because contributors install only Rust tooling.
- Good, because the same Markdown files render correctly on GitHub.
- Bad, because mdBook’s rendering of admonitions depends on GitHub-flavored Markdown extensions, which mdBook supports but not every Markdown viewer does.
Confirmation
docs/book.tomlconfigures the build.mdbook build docssucceeds, as enforced by Documentation tooling and CI.
Pros and cons of the options
mdBook
- Good, because it is Rust-native and matches the toolchain decision.
- Good, because it supports Mermaid via
mdbook-mermaid. - Bad, because it requires vendored JavaScript for Mermaid rendering.
Python or Node.js generator
- Good, because those ecosystems have many themes.
- Bad, because they require a second toolchain.
Plain GitHub Pages
- Good, because it requires no build step.
- Bad, because it cannot render Mermaid diagrams without JavaScript, and it offers no cross-reference validation.
More information
The documentation style guide defines the Markdown conventions the book relies on. Upgrading mdbook-mermaid requires rerunning mdbook-mermaid install docs to regenerate vendored files.
ADR-0009: Deterministic enforcement
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Vision, Evaluation, ADR-0010, ADR-0011
Context and problem statement
An authorization layer that sometimes allows and sometimes denies the same action depending on hidden state is worse than useless: it trains the operator not to trust it. The engine needed to guarantee that identical inputs produce identical outputs, with no dependence on timing, network state, or language-model output.
Decision drivers
- Replay must reproduce decisions exactly.
- Security auditors must be able to reason about the engine’s behavior from a specification.
- No input to any decision may come from a language model or machine-learning model.
Considered options
- Deterministic evaluation: the same action request and policy snapshot always produce the same decision.
- Probabilistic risk scoring: a numeric risk score determines whether an action is allowed.
- Model-in-the-loop: a language model judges each action.
Decision outcome
Chosen option: “Deterministic evaluation”, because it makes the engine’s behavior reproducible, auditable, and testable without statistical uncertainty.
- The engine is a pure function of the action request and the policy snapshot.
- No language-model output, machine-learning model, or timing-dependent input influences any stage.
- Risk signals are discrete properties (such as
bulk_delete) that escalate a decision topending, never numeric scores. - Determinism is a design commitment referenced from the vision.
Consequences
- Good, because replay and verification are meaningful.
- Good, because attackers cannot influence decisions through timing or state manipulation.
- Bad, because the engine cannot adapt to context that a model might detect, such as subtle prompt-injection signals.
Confirmation
- ADR-0021 requires risk signals to be deterministic properties.
- ADR-0010 makes the engine pure, which is a prerequisite for determinism.
Pros and cons of the options
Deterministic evaluation
- Good, because decisions are reproducible and auditable.
- Bad, because it cannot adapt to statistical signals.
Probabilistic risk scoring
- Good, because numeric scores can capture nuanced risk.
- Bad, because non-determinism makes replay impossible and models can be manipulated.
Model-in-the-loop
- Good, because models can understand intent.
- Bad, because no language model is reliably safe, and the output is non-deterministic.
More information
ADR-0009 is the foundation of the evaluation specification. The risk signals specification defines which signals exist and how they escalate decisions.
ADR-0010: Pure synchronous engine
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Glossary, Evaluation, ADR-0009, Workspace and crates
Context and problem statement
The engine evaluates action requests against policy. If the engine performs I/O, opens network connections, or uses asynchronous runtimes, it becomes harder to test, harder to reason about, and harder to embed in different hosts. The project needed to decide what the engine is allowed to do.
Decision drivers
- Replay and verification require the engine to be a pure function.
- CI builds
shiin-engineforwasm32-unknown-unknownas a purity guard. - The engine must be fast and predictable on the hook path.
Considered options
- A pure, synchronous engine that performs no I/O and depends on no async runtime.
- An async engine with I/O for loading policy and grants on demand.
- A hybrid engine that is synchronous for policy rules but async for lookups.
Decision outcome
Chosen option: “A pure, synchronous engine”, because purity is what makes determinism and replay possible, and it keeps the dependency graph small.
shiin-engineperforms no I/O of any kind.- It takes an action request and a policy snapshot and returns a decision.
- It depends on
shiin-policyandshiin-schemaonly. - It never depends on
shiin-classify(classification happens in the adapter before the engine is called). - From v0.4, CI builds
shiin-engineforwasm32-unknown-unknownto catch I/O dependencies.
Consequences
- Good, because the engine is fully testable with deterministic unit tests.
- Good, because the engine can be embedded in any runtime, including WebAssembly.
- Bad, because the engine cannot fetch remote policy or grants; that is the adapter’s job, which provides the snapshot.
Confirmation
shiin-classifyandshiin-policymay read the local file system where their job requires it.- Workspace and crates forbids networking in the engine crates.
- CI builds for
wasm32-unknown-unknown(planned: v0.4).
Pros and cons of the options
Pure, synchronous engine
- Good, because it is deterministic, testable, and embeddable.
- Bad, because it cannot do on-demand lookups.
Async engine with I/O
- Good, because it can fetch policy and data on demand.
- Bad, because I/O breaks determinism and complicates testing.
Hybrid engine
- Good, because it balances purity with flexibility.
- Bad, because the boundary between synchronous rules and async lookups is a source of bugs.
More information
The decision pipeline describes what data the snapshot contains. The adapter contract specifies how the adapter assembles the snapshot from policy layers, grants, and tasks.
ADR-0011: Decision values and combining
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Decision, Evaluation, ADR-0009, Glossary
Context and problem statement
When multiple stages of evaluation each produce a result, the engine must combine them into a single decision. The combining logic must be simple enough that an operator can predict the outcome, and it must ensure that a restrictive stage is never overridden by a permissive one.
Decision drivers
- A
denymust always win, from any source. pendingmust propagate unless a grant already covers it.- The default when nothing matches must be the safest option.
Considered options
- Three values (
allow,deny,pending) with deny-wins and default-deny combining. - A five-level combining scheme with
permit,permit-pending,abstain,deny-pending,deny. - Numeric scoring where higher values win.
Decision outcome
Chosen option: “Three values with deny-wins and default-deny”, because it is the simplest scheme that satisfies every principle in the vision.
- The decision values are
allow,deny, andpending. - A
denyfrom any stage wins over every other result, including grants. - A
pendingwins overallowunless an existing grant covers the request. - When no rule matches the action, the default decision is
deny. - Only policy rules and grants can produce
allow; gates (identity, capability, intent, risk) can onlydeny,pending, or pass.
Consequences
- Good, because the three-value scheme is easy to explain and reason about.
- Good, because deny-wins prevents a permissive rule from overriding a restrictive gate.
- Bad, because operators sometimes want a “soft deny” that asks rather than hard-stops; this is expressed as
pendingwith a reason code.
Confirmation
- Evaluation specifies the exact combining algorithm.
- The decision specification defines the reason codes for each value.
Pros and cons of the options
Three values, deny-wins, default-deny
- Good, because it is simple and matches operator intuition.
- Bad, because it offers no middle ground between
allowanddenyother thanpending.
Five-level combining
- Good, because it distinguishes soft and hard denials.
- Bad, because it is harder to explain and more error-prone to implement.
Numeric scoring
- Good, because it can capture nuanced risk.
- Bad, because it conflicts with the determinism commitment in ADR-0009.
More information
The glossary defines each decision value and the reason code format. The default-decision behavior is configurable to pending but can never be allow.
ADR-0012: Evaluation stage order
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Evaluation, ADR-0010, ADR-0011, Glossary
Context and problem statement
The engine evaluates an action request in stages. Each stage can only make the result stricter, and the order determines which checks happen first. The project needed to fix the order so that the same inputs always produce the same stages, and so identity and trust are established before expensive policy evaluation.
Decision drivers
- Identity must be established before anything else, because trust depends on who is asking.
- Capability is a fast pre-filter before policy evaluation.
- Policy is the core decision and must run after identity and capability.
- Intent binding must run after policy, so a task constraint can only restrict, never relax.
- Risk signals must run last, so they can escalate any earlier result.
Considered options
- The fixed order: identity, capability, policy, intent, risk.
- A configurable order where operators choose which stages run first.
- Parallel evaluation of all stages.
Decision outcome
Chosen option: “The fixed order: identity, capability, policy, intent, risk”, because it matches the principle that later stages can only restrict earlier results, and it ensures identity and capability are checked before the expensive policy evaluation.
- Stages run in exactly this order: identity, capability, policy, intent, and risk.
- The identity and capability stages are gates: they can
deny,pending, or pass, but neverallow. - The policy stage can produce
allow,deny, orpending. - The intent stage checks task constraints and can only make the result stricter.
- The risk stage escalates risk signals and can only make the result stricter.
Consequences
- Good, because the order is deterministic and easy to reason about.
- Good, because identity and capability short-circuit before policy evaluation, saving time.
- Bad, because a fixed order may not be optimal for all workloads; the policy can model most variation within its rules.
Confirmation
- Evaluation specifies the combining rules across stages.
- The five-stages table in How Shiin works documents the order for readers.
Pros and cons of the options
Fixed order
- Good, because it is predictable and deterministic.
- Bad, because it is not configurable.
Configurable order
- Good, because operators can optimize for their workload.
- Bad, because it introduces combinatorial complexity into testing and reasoning.
Parallel evaluation
- Good, because it can be faster.
- Bad, because it breaks the “later stages can only restrict” invariant and complicates the trace.
More information
The decision pipeline describes what data each stage consumes and produces.
ADR-0013: Fail-closed failure semantics
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Security model, Error handling, ADR-0009, ADR-0011, Glossary
Context and problem statement
When the engine, adapter, or policy itself has a problem — an error, a timeout, an unreadable policy file — there is no decision to return. The alternative is either to let the action through (fail open) or to refuse it (fail closed). An authorization layer that fails open defeats its own purpose.
Decision drivers
- A crash or misconfiguration must never let an unauthorized action proceed.
- Operators must notice failures, not silently accept them.
- The decision must be fast: the host’s hook timeout is measured in seconds.
Considered options
- Fail closed: any failure produces a
denydecision. - Fail open: any failure lets the action proceed.
- Fail to
pending: any failure waits for human approval.
Decision outcome
Chosen option: “Fail closed”, because an authorization layer that lets actions through on its own failure is worse than useless.
- Every operational error in the hook binary, the daemon, or the engine is routed to a single conversion function that produces a
denydecision. - Errors and timeouts produce
deny, as stated in How Shiin works. - A panic in the hook binary installs a panic hook that writes the host’s deny response before unwinding.
- A failed audit write for a non-read action also produces
deny, as specified in ADR-0030.
Consequences
- Good, because a failure can never authorize an action.
- Good, because failures are visible and diagnosable.
- Bad, because a misconfigured policy can stop all work; the operator must be able to read the logs to diagnose it.
Confirmation
- Every hook, daemon, and MCP proxy entry point routes
Errto the single conversion function. - A fault-injection test makes the hook panic and time out and asserts the result is
deny(exit criterion: v0.1).
Pros and cons of the options
Fail closed
- Good, because it preserves the security guarantee under failure.
- Bad, because it can block legitimate work when misconfigured.
Fail open
- Good, because it keeps work flowing.
- Bad, because it defeats the purpose of the authorization layer.
Fail to pending
- Good, because a human can override.
- Bad, because in CI or unattended mode there is no human, and
pendingbecomes a hang or a silent denial.
More information
The security model lists the failure modes and their resulting decisions. The error-handling page describes the code-level implementation.
ADR-0014: Cedar policy engine with TOML front-end
Note
Architecture decision: accepted. Confirmed on 2026-10-12. The conformance test in
shiin-enginereproduces every test vector.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Policy model, Policy syntax, Vision, Landscape, ADR-0002
Context and problem statement
Shiin needs an authorization engine that evaluates policy rules against action requests. Operators need a syntax that is easy to read and write, while the engine needs an underlying formal model that is testable, explainable, and reasoned about. Two layers are needed: a human-facing syntax and a machine-verifiable evaluation model.
Decision drivers
- Policy must be analyzable: testable, explainable, and formally reasoned about.
- The front-end syntax must be approachable for operators writing their first policy.
- The engine implementation must be in Rust (see ADR-0002).
- Cedar’s reference implementation,
cedar-policy, is a Rust crate.
Considered options
- Compile TOML policy to Cedar, with Cedar as the evaluation engine.
- Write a native Rust policy evaluator behind a TOML front-end.
Decision outcome
Chosen option: “Compile TOML policy to Cedar”, because Cedar is formally verified and provides explainability and testability that a custom evaluator would need to reimplement.
- Operators write policy in TOML, as shown in How Shiin works.
- The
shiin-policycrate compiles TOML to Cedar at policy load time. - Cedar’s
cedar-policyRust crate performs evaluation. - Advanced users can bypass the TOML front-end and write Cedar directly.
- The M0 spike confirmed Cedar, so the fallback of a native evaluator is not needed.
Consequences
- Good, because policy can be tested, explained, and reasoned about using Cedar’s tooling.
- Good, because a formally verified engine reduces the risk of authorization bugs.
- Bad, because it adds the
cedar-policydependency, which increases compile time and supply-chain surface. - Bad, because if the spike finds Cedar unsuitable, the TOML-to-Cedar compilation work is thrown away.
Confirmation
Confirmed. A spike compiled each test vector’s policy into Cedar and reproduced every expected
decision and reason code. The spike has since been retired, as its own disposition note required,
because shiin-policy and shiin-engine now do the same work and the conformance test in
shiin-engine runs the whole corpus on every change.
It established three things that shape the implementation:
- Cedar’s
forbidoverridingpermitis exactly “deny always wins”, and its diagnostics name the determining policies, which is how reason codes and matched rule identifiers are produced. - Cedar has no third outcome, so
pendingis carried as a@decision("pending")annotation on apermitand downgraded by the caller. The annotation is part of the compilation contract. - Cedar never sees a path. Globs are precomputed in Rust and passed as
context.matched_rules, so policy analysis over paths is outside Cedar’s reach.
The spike also found four specification defects, which were fixed and the spike re-run:
the missing workspace root, the missing risk threshold, the missing deny.default reason code,
and two test vectors that could not be decided from their own inputs.
Ongoing confirmation is the conformance corpus: the test in crates/shiin-engine/tests/conformance.rs
reproduces every vector in docs/spec/test-vectors/, and continuous integration runs it.
Pros and cons of the options
Compile TOML to Cedar
- Good, because Cedar is formally verified and analyzable.
- Good, because the Cedar ecosystem provides explainability and testing tools.
- Bad, because it depends on a third-party crate.
Native Rust evaluator
- Good, because it has no external policy-engine dependency.
- Bad, because it is not formally verified and must reimplement Cedar’s analysis features.
More information
The spike is scoped in the roadmap. The landscape page compares Cedar and other policy engines. If this ADR is superseded, the replacement records the fallback evaluator’s design.
ADR-0015: Policy layering and trust
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Policy model, Glossary, ADR-0014
Context and problem statement
Policy comes from multiple sources: built-in defaults, the operator, the workspace, and eventually an organization. These sources have different trust levels: the operator’s policy is more trusted than a workspace’s. The project needed a layering model that allows workspace policy to only restrict, never to grant new allowances, unless the operator explicitly trusts it.
Decision drivers
- Built-in rules (forbid reading secrets, forbid modifying Shiin state) must always apply.
- Operator policy must take precedence over workspace policy.
- Workspace policy must be able to deny or narrow, but not broaden, what the operator allows.
- The layering must be explicit and auditable.
Considered options
- Three layers (built-in, user, workspace) where later layers can only restrict.
- A flat policy where all sources merge into one set of rules.
- A plugin system where each layer is loaded by a different trusted party.
Decision outcome
Chosen option: “Three layers where later layers can only restrict”, because it gives operators control without requiring per-workspace review.
- Layers, in order: built-in, user, and workspace. An organization layer is reserved for the future.
- Built-in rules define protected resources and the default decision.
- User (operator) rules are loaded from the operator’s configuration.
- Workspace rules are loaded from the project directory and can only restrict unless the operator trusts the workspace by hash.
- A workspace layer can only restrict unless the operator trusts it by hash.
Consequences
- Good, because an untrusted workspace cannot grant access the operator denied.
- Good, because the layers are explicit and auditable.
- Bad, because operators who want workspace policy to grant new allowances must explicitly trust each workspace.
Confirmation
- The policy model specifies the layering and the trust rule.
- The built-in protected resources specification defines what built-in rules cover.
Pros and cons of the options
Three layers, workspace can only restrict
- Good, because it is safe by default.
- Bad, because it requires an explicit trust step for full workspace flexibility.
Flat merged policy
- Good, because it is simple.
- Bad, because a malicious or careless workspace can override operator policy.
Plugin system per layer
- Good, because it is flexible.
- Bad, because it adds complexity and a trusted-loader component.
More information
The built-in protected resources ADR defines what built-in rules cover. The policy model specification defines the evaluation order of layers.
ADR-0016: Built-in protected resources
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Policy model, Glossary, ADR-0015
Context and problem statement
An agent might try to modify Shiin’s own configuration, delete its audit log, or exfiltrate its signing keys. If policy can allow these actions, an operator’s mistake or a compromised workspace policy could disable Shiin entirely. The project needed a mechanism that makes certain resources unallowable by any policy.
Decision drivers
- Shiin’s state, keys, and audit log must not be modifiable by any agent.
- Host hook configuration that disables interception must be protected.
- Approval commands must not be callable by an agent.
- These protections must be in the built-in layer and cannot be overridden.
Considered options
- Built-in rules that deny access to protected resources, unallowable by any policy layer.
- No built-in rules; rely entirely on operator policy.
- OS-level file permissions as the sole protection.
Decision outcome
Chosen option: “Built-in rules that deny access to protected resources”, because policy that can be overridden is not a guarantee.
- Built-in protected resources include: Shiin’s own state directory, signing keys, audit log, host hook configuration files, and approval commands.
- These resources are unallowable: no policy, no layer, and no grant can permit an action that accesses them.
- The built-in layer is evaluated first and produces
denyimmediately for any matching resource.
Consequences
- Good, because Shiin cannot disable itself through its own policy system.
- Good, because the protection is automatic and does not require operator configuration.
- Bad, because if the built-in resource list is wrong, legitimate actions are blocked; the list must be carefully maintained.
Confirmation
- The policy model lists the built-in resource patterns.
- The glossary defines built-in protected resources.
Pros and cons of the options
Built-in unallowable rules
- Good, because the protection is absolute within Shiin’s authority.
- Bad, because the resource list must be complete and correct.
Operator policy only
- Good, because it is flexible.
- Bad, because a misconfiguration can disable Shiin.
OS-level permissions only
- Good, because the OS enforces them.
- Bad, because OS permissions are coarser and do not understand action effects.
More information
See ADR-0015 for the layering model that places built-in rules at the bottom. The threat model treats tampering with audit logs as an in-scope threat.
ADR-0017: JSON wire format and schemas
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Evaluation, JSON Schemas, ADR-0002, ADR-0023, ADR-0042
Context and problem statement
Shiin communicates with adapters, agents, and other tools. The wire format must be language-neutral, versioned, and validated so that integrators in other languages can interoperate. Schemas are needed so that implementations can be tested against the specification.
Decision drivers
- Must be language-neutral.
- Must be versioned so specifications can evolve.
- Must be independently implementable without a Rust dependency.
- Must be machine-validated so conformance can be tested.
Considered options
- JSON with JSON Schema, identified by URN identifiers.
- Protocol Buffers with generated code.
- Cap’n Proto with a schema compiler.
Decision outcome
Chosen option: “JSON with JSON Schema, identified by URN identifiers”, because it is the most interoperable and requires no code generation.
- Action requests, decisions, approval messages, and audit events are serialized as JSON.
- Each message type has a JSON Schema under
docs/spec/schemas/. - Schemas are identified by URNs of the form
urn:shiin:schema:<name>:v<major>. - URN identifiers keep schema identity independent of any web domain.
- Shared definitions live in
common.v1.schema.jsonand are referenced by$id.
Consequences
- Good, because any language with a JSON parser can interoperate.
- Good, because schemas enable conformance testing.
- Bad, because JSON is more verbose than binary formats, but Shiin’s messages are small.
Confirmation
cargo xtask docs-checkcompiles and validates all schemas.- Every JSON example in specifications carries a validation marker and is checked against its schema.
Pros and cons of the options
JSON with JSON Schema
- Good, because it is universally supported.
- Good, because schemas are testable.
- Bad, because it is verbose compared to binary.
Protocol Buffers
- Good, because it is compact and has code generation for many languages.
- Bad, because it requires a code generator and is not human-readable.
Cap’n Proto
- Good, because it supports zero-copy reads.
- Bad, because it has limited language support and is not human-readable.
More information
The specification README lists all schema files. Test vectors encoding the golden use cases are in docs/spec/test-vectors/.
ADR-0018: Conservative action classification
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Classifier, Action request, Risk signals, Use cases
Context and problem statement
When an agent runs a command like python scripts/cleanup.py, the adapter cannot know what the script will do.
Classifying it as safe would be a security risk; classifying it as dangerous would block too much.
The project needed a classification strategy that errs on the side of caution.
Decision drivers
- Unknown actions must not be silently allowed.
- Classification must be deterministic and explainable.
- Operators must be able to see why a classification was made.
Considered options
- Conservative classification: unknown effects are marked
opaque, which escalates topending. - Heuristic classification: unknown effects are guessed and allowed if low-risk.
- Static analysis of all script contents.
Decision outcome
Chosen option: “Conservative classification”, because the agent is untrusted and cannot be allowed to hide unknown effects.
- The classifier derives action types and effects from what a host reports.
- Anything the classifier cannot classify becomes an opaque effect.
- Opaque effects raise the
opaque_commandrisk signal and producepending. - Classifiers only ever make decisions stricter: anything they cannot classify becomes opaque.
Consequences
- Good, because unknown commands are held for approval, not silently allowed.
- Good, because the classification is deterministic and auditable.
- Bad, because some scripts that are genuinely safe are held for approval, increasing prompt fatigue.
Confirmation
- The risk signals specification defines
opaque_commandand other signals. - Use-case UC-02 requires shell commands like
cat .envto be classified as reads of the same canonical path as file-read tools.
Pros and cons of the options
Conservative classification
- Good, because it prevents unknown effects from being allowed.
- Bad, because it increases
pendingfor legitimate unknown scripts.
Heuristic classification
- Good, because it reduces friction.
- Bad, because heuristics can be fooled by prompt injection or obfuscated scripts.
Static analysis
- Good, because it can determine actual effects.
- Bad, because it is fragile, slow, and cannot analyze all languages.
More information
The glossary defines opaque effect. The path canonicalization ADR describes how paths are normalized before classification.
ADR-0019: Path canonicalization
Note
Architecture decision: accepted. The rules below are settled. Windows-specific details are deferred to v0.6.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, ADR-0018, Action request, Use case UC-02
Context and problem statement
An agent might read a secret file through a file tool, or through a command like cat .env, or through a symlink, or through a relative path.
For policy to be effective, the classifier must map all of these to the same canonical path so that a single deny rule covers every form.
The project needed a canonicalization strategy that handles symlinks, parent-directory traversal, and environment-variable expansion.
Decision drivers
- Policy must match on canonical paths, not on how the agent spelled the path.
- Shell commands and file tools must map to the same resource.
- The canonicalization must be deterministic and platform-consistent.
Considered options
- Canonicalize by resolving symlinks and
.., then normalizing to an absolute path. - Match on the path string as the agent provides it, without canonicalization.
- Resolve paths at the host level before handing them to the adapter.
Decision outcome
Chosen option: “Canonicalize by resolving symlinks and ..”, because string matching allows bypasses through symlinks and traversal sequences.
Canonicalization runs in the adapter’s classifier, before policy is consulted, and follows these rules in order:
- Make absolute. A relative path resolves against the request’s workspace root (AR-008). A path with no workspace context that is not absolute is unclassifiable, which makes its effect opaque.
- Expand
~when policy is compiled, not when a request is classified. The operator’s home directory is a property of the machine, so a policy glob such as~/.ssh/**becomes an absolute pattern at load time. Requests never carry~. - Resolve symlinks on the longest existing prefix. Shiin resolves the deepest part of the path that exists, then appends the remaining segments lexically. This keeps
fs.createclassifiable for a file that does not exist yet. - Resolve
.and..lexically, after symlink resolution, so that a symlink cannot be stepped over by a traversal sequence. - Normalize Unicode to NFC, so that two spellings of the same name cannot be treated as different paths.
- Preserve case, and compare case-insensitively only where the filesystem is case-insensitive. Comparison is case-sensitive on Linux, and case-insensitive on macOS and Windows defaults. The Windows specifics (
\\?\prefixes, 8.3 short names, junctions, and reparse points) are settled when Windows moves toward Tier 1 in v0.6. - Fail closed. If canonicalization errors, the effect is opaque and the request can never be allowed by a path rule.
- A workspace-relative pattern can never match outside the workspace (AR-009), even if a symlink inside the workspace points out of it, because matching happens after resolution.
Both the file-read tool and cat .env therefore produce an fs.read effect on the same canonical path, which is what makes a single deny rule cover both.
Consequences
- Good, because policy can match on canonical paths regardless of how the agent reached them.
- Good, because a single deny rule covers file tools, shell commands, and scripts that read the same file.
- Bad, because resolving symlinks requires file system access in the classifier, which adds latency and failure modes.
Confirmation
- Use-case UC-02 requires the classifier to map both the file-read tool and
cat .envto the same canonical path. - The
shiin-classifycrate owns rules 1 to 7, and property tests assert that canonicalization is idempotent and that no traversal or symlink sequence escapes a workspace-relative pattern (testing strategy). - A regression test vector is added for every path-aliasing bypass that is reported.
- The time-of-check to time-of-use gap is a residual risk, not a defect: a path can be replaced between the decision and the action. It is recorded as out of scope in the threat model and is mitigated by a sandbox, not by Shiin.
Pros and cons of the options
Canonicalize with symlink resolution
- Good, because it prevents path-spoofing bypasses.
- Bad, because it requires file system access in the classifier.
String match without canonicalization
- Good, because it is fast and has no I/O.
- Bad, because it allows bypasses through symlinks and traversal.
Resolve at host level
- Good, because the host already has the canonical path.
- Bad, because not all hosts expose it, and the agent’s path may differ from the canonical one.
More information
The conservative action classification ADR describes how effects are derived from classified actions. The classification approach is a key part of UC-02.
ADR-0020: Structured intent binding
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Intent binding, Glossary, Glossary, Glossary, Use cases
Context and problem statement
A developer tells an agent: “Fix the login bug. Do not change the database schema.” The agent may be allowed to run database commands, so the only way to enforce the “do not change the schema” instruction is to check each action against the task’s constraints. But natural-language instructions cannot be enforced directly; they must be translated into machine-checkable form.
Decision drivers
- Task constraints must be explicit and machine-checkable, not natural language.
- A human must confirm the constraints before they apply.
- Natural-language descriptions are recorded for context but never evaluated.
Considered options
- Structured task declarations with a closed set of constraint kinds.
- Free-text task descriptions parsed by an LLM.
- No intent binding; rely on standing policy alone.
Decision outcome
Chosen option: “Structured task declarations with a closed set of constraint kinds”, because only machine-checkable constraints can be deterministically enforced.
- A task declaration contains a description, constraints, and an expiry.
- Constraints are from a closed set:
scope.paths,forbid.action_types,allow.hosts,limits,expires_at, andrequire_pending_for. - Intent binding checks each action against the active task’s constraints.
- Natural-language descriptions of intent are recorded but never evaluated.
- Task declarations are experimental and marked as such (partial source implementation in v0.2; host validation and measurements remain open).
Consequences
- Good, because the constraints are deterministic and auditable.
- Good, because a human confirms the constraints, so a model cannot expand the task scope.
- Bad, because the closed constraint set may not cover every task the operator wants to express.
Confirmation
- The intent binding specification defines the constraint schema.
- Use-case UC-04 demonstrates the design.
Pros and cons of the options
Structured declarations with closed constraints
- Good, because constraints are enforceable and deterministic.
- Bad, because the closed set may be too narrow.
Free-text parsed by an LLM
- Good, because it accepts any description.
- Bad, because it is non-deterministic and attackable by prompt injection.
No intent binding
- Good, because it is simple.
- Bad, because standing policy cannot distinguish an in-task action from an out-of-task one.
More information
The glossary defines intent binding, task declaration, and constraint. See ADR-0009 for why natural-language intent is never evaluated.
ADR-0021: Discrete risk signals
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Risk signals, Glossary, How Shiin works, ADR-0009
Context and problem statement
Some actions are riskier than others: a bulk deletion, reading a secret path, or running an opaque script.
The engine must escalate these to pending for human review.
But a numeric risk score is non-deterministic and can be gamed.
The project needed a risk model that is deterministic and explainable.
Decision drivers
- Risk assessment must be deterministic (see ADR-0009).
- Operators must be able to understand why a signal fired.
- Risk must never be used to allow an action that would otherwise be denied.
Considered options
- Discrete, deterministic risk signals (named properties) that escalate to
pending. - A numeric risk score where higher scores escalate.
- A machine-learning model that classifies risk.
Decision outcome
Chosen option: “Discrete, deterministic risk signals”, because a named property is reproducible and explainable.
- Risk signals are discrete, deterministic properties of an action request, such as
bulk_deleteorsecret_path. - A signal escalates the decision to
pendingunless a matching grant already covers it. - Signals never allow; they can only restrict or escalate.
- There are no numeric risk scores.
Consequences
- Good, because each signal is reproducible and auditable.
- Good, because operators can see exactly which signal fired and why.
- Bad, because discrete signals cannot capture subtle gradations of risk.
Confirmation
- The risk signals specification defines the full set of signals, their triggering conditions, and their reason codes.
- Use-cases UC-01, UC-02, and UC-03 define the core signals.
Pros and cons of the options
Discrete risk signals
- Good, because they are deterministic and explainable.
- Bad, because they cannot express gradations.
Numeric risk scoring
- Good, because it can capture nuance.
- Bad, because it conflicts with determinism and can be gamed.
Machine-learning risk model
- Good, because it can detect subtle patterns.
- Bad, because it is non-deterministic, unexplainable, and trainable by attackers.
More information
Each risk signal produces a reason code starting with pending. (for example, pending.opaque in How Shiin works). The full catalog is in the risk signals specification.
ADR-0022: Local-first, daemonless deployment
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Vision, Glossary, ADR-0007, ADR-0023, ADR-0010
Context and problem statement
Shiin intercepts agent actions on the developer’s machine. The v0.1 adapter runs as a Claude Code hook: a new process for each invocation, with no long-lived daemon. This must remain the zero-dependency path: no service, no network, no daemon, no telemetry. The daemon (v0.3) is an addition for approvals, not a requirement.
Decision drivers
- The hook path must start fast, because it runs on every action.
- No service should be required for basic authorization.
- No data should leave the machine.
Considered options
- Daemonless hooks for v0.1, with an optional daemon added later for approvals.
- A daemon from v0.1 that all hooks connect to.
- A cloud service that all agents connect to.
Decision outcome
Chosen option: “Daemonless hooks for v0.1, optional daemon later”, because the daemonless path has lower latency and zero setup cost.
- v0.1 runs a new
shiin hookprocess for each host invocation. - The engine is pure and synchronous (ADR-0010), so a hook process can evaluate and exit.
- The daemon (
shiind) is introduced in v0.3 for approvals and the local API, but is optional. - The daemonless path remains supported after the daemon ships.
Consequences
- Good, because the barrier to entry is zero: no daemon to start or configure.
- Good, because each hook invocation is independent and fast.
- Bad, because approvals in v0.1 use the host’s native prompt; the real approval protocol comes in v0.3.
Confirmation
- The roadmap lists
shiindand the approval protocol in v0.3. - The use cases for unattended CI (UC-07) require deterministic behavior without a daemon.
Pros and cons of the options
Daemonless first, daemon later
- Good, because it has the lowest barrier to entry.
- Bad, because approvals need the daemon or host prompts.
Daemon from v0.1
- Good, because approvals are centralized.
- Bad, because it adds latency and setup cost to the basic path.
Cloud service
- Good, because it is always available.
- Bad, because it contradicts the local-first principle (ADR-0007) and adds network dependency.
More information
The local API over the daemon is specified in ADR-0023. The deployment models page describes local-first and team-server options.
ADR-0023: JSON-RPC local API
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Local API, ADR-0022, ADR-0017, ADR-0042
Context and problem statement
The daemon (shiind, v0.3) needs an API for clients: the CLI, approver applications, and embedded libraries.
The API must be request-response over a local transport, language-neutral, and versionable.
It must not require HTTP or a network stack for the local-first path.
Decision drivers
- Must be local-only (no network required).
- Must be language-neutral for future bindings.
- Must handle error cases, not just success.
- Must be versionable over time.
Considered options
- JSON-RPC 2.0 over a Unix domain socket (and named pipes on Windows).
- A REST/HTTP API over localhost.
- A custom binary protocol.
Decision outcome
Chosen option: “JSON-RPC 2.0 over a Unix domain socket”, because it is language-neutral, request-response, and does not require an HTTP server or port.
- The local API is JSON-RPC 2.0, as specified in the local API specification.
- Transport is a Unix domain socket on Unix-like systems and a named pipe on Windows.
- All messages use the JSON wire format (ADR-0017).
- The
shiin-clientlibrary implements the client side for Rust.
Consequences
- Good, because JSON-RPC is simple, well-understood, and widely implemented.
- Good, because Unix domain sockets have no port to configure and no firewall rules.
- Bad, because Windows requires named pipes, which is a separate code path.
- Bad, because JSON-RPC over sockets is not directly browser-accessible (not needed for local-first).
Confirmation
- The local API specification defines the methods, parameters, and error codes.
- CI tests the client library against the daemon.
Pros and cons of the options
JSON-RPC over local sockets
- Good, because it is language-neutral and local-only.
- Bad, because it is not HTTP, so standard HTTP tools cannot inspect it easily.
REST/HTTP
- Good, because it is universally understood.
- Bad, because it requires a port, an HTTP stack, and TLS for any remote future.
Custom binary protocol
- Good, because it is efficient.
- Bad, because it is not self-describing and requires code generation for bindings.
More information
The shiin-ipc crate implements the Unix transport in source; Windows named-pipe validation remains open. The deployment models page describes when the daemon is needed.
ADR-0024: Tokio, confined
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Workspace and crates, Async and concurrency, ADR-0010, ADR-0002
Context and problem statement
The daemon (shiind, v0.3) and the MCP proxy (v0.5) are long-lived, I/O-bound processes that need asynchronous runtimes.
But the engine (ADR-0010) is pure and synchronous.
The project needed an async runtime that is confined to the parts that need it, without leaking into the engine.
Decision drivers
- Only the daemon and MCP proxy need async.
- The engine must remain synchronous and panic-safe.
- Contributors should use one well-supported async runtime.
Considered options
- Tokio, used only in binaries and the IPC crate, never in the engine crates.
- async-std as a lighter alternative.
- No async runtime; use blocking I/O everywhere.
Decision outcome
Chosen option: “Tokio, confined to binaries and IPC”, because Tokio is the most mature Rust async runtime and integrates with the crates the project already considers.
- Tokio is a dependency of
shiindandshiin-ipconly. - The engine crates (
shiin-schema,shiin-classify,shiin-policy,shiin-engine) depend on no async runtime. - The MCP proxy uses Tokio through its MCP framework dependency.
- A planned
xtaskcheck inspects the non-dev dependency tree of engine crates and fails if a forbidden crate appears.
Consequences
- Good, because it gives the daemon and proxy good async I/O performance.
- Good, because the engine stays pure and synchronous.
- Bad, because Tokio adds compile-time and binary-size weight to the daemon and proxy.
Confirmation
- Workspace and crates forbids networking and async in the engine crates.
- CI runs a planned
xtaskcheck for the dependency-tree rule.
Pros and cons of the options
Tokio, confined
- Good, because it is mature and well-supported.
- Bad, because compile times are longer for async crates.
async-std
- Good, because it is lighter.
- Bad, because it has smaller community and fewer integrations.
No async runtime
- Good, because it avoids runtime dependency.
- Bad, because the daemon needs to handle many concurrent connections efficiently.
More information
The async and concurrency engineering page describes the confinement rules. The wasm32 build of the engine is a purity guard against runtime leakage.
ADR-0025: Adapter roadmap
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Adapter contract, Glossary, Glossary, Landscape, Roadmap
Context and problem statement
Shiin needs adapters for each AI coding agent host. Each host has a different interception mechanism (hooks) and different response protocols. The project needed a roadmap that covers the most-used hosts first, while keeping the adapter contract general enough that anyone can write a new adapter.
Decision drivers
- The first adapter must cover the most-used agent today.
- The contract must be host-agnostic so future adapters are straightforward.
- Adapter coverage must be documented with enforcement levels and blind spots.
Considered options
- Start with Claude Code (the most popular host), then add Codex CLI and Cursor.
- Support all hosts simultaneously with a generic adapter.
- Start with a single open protocol and ignore host-specific hooks.
Decision outcome
Chosen option: “Claude Code first, then Codex and Cursor, then MCP”, because Claude Code has the most adoption and the clearest hook documentation at the time of writing.
- v0.1 delivers the Claude Code hook adapter, running daemonless.
- v0.2 adds Codex CLI and Cursor hook adapters.
- v0.6 may add a shell wrapper for hosts without hooks.
- Every adapter states its enforcement level (L1/L2/L3) and what it cannot see.
- The contract is specified in the adapter contract, not in host-specific documentation.
Consequences
- Good, because the first adapter covers the largest user base.
- Good, because the contract is general and third parties can write adapters.
- Bad, because users of other hosts must wait for their adapter.
Confirmation
- The enforcement levels page documents what L1 cooperative means for hook adapters.
- Each host’s hook behavior is verified and documented on its integration page under
guides/integrations/.
Pros and cons of the options
Claude Code first, then others
- Good, because it covers the largest immediate user base.
- Bad, because it delays support for other hosts.
All hosts simultaneously
- Good, because it is comprehensive.
- Bad, because hook interfaces differ enough that parallel implementation is error-prone.
Single open protocol only
- Good, because it is simplest.
- Bad, because most agents do not support a single open protocol; hooks are host-specific.
More information
The landscape documents each host’s hook capabilities with verification dates. See ADR-0026 for how adapters are loaded.
ADR-0026: Adapter extensibility
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Adapter contract, ADR-0025, Glossary, Roadmap
Context and problem statement
Adapters connect hosts to the Shiin engine. One option is to allow third parties to load custom adapters as dynamic libraries at runtime. Another is to compile adapters statically and ship them as part of the CLI or daemon. The project needed to decide which model to use and whether dynamic loading is supported.
Decision drivers
- Third parties must be able to integrate, but the attack surface must be minimized.
- Dynamic loading adds complexity and a code-loading trust boundary.
- Compiled adapters are simpler and auditable.
Considered options
- Static adapters only: compile all adapters into the relevant binary, no dynamic loading.
- Dynamic-library plugins loaded at runtime.
- A plugin server that adapters connect to remotely.
Decision outcome
Chosen option: “Static adapters only”, because dynamic code loading is an unnecessary attack surface for an authorization tool.
- Adapters are compiled into the
shiinbinary or theshiinddaemon, not loaded at runtime. - New adapters are added by contributing them to the project, reviewed like any other code.
- This is listed under “Not planned” in the roadmap.
- Third-party integrators implement adapters in other languages through the local API and adapter contract.
Consequences
- Good, because there is no dynamic code-loading trust boundary.
- Good, because all adapter code is auditable in the same build and test pipeline.
- Bad, because organizations cannot ship their own private adapters without forking or using the API.
Confirmation
- The roadmap not-planned section lists “Dynamic-library plugins for adapters” as deliberately out of scope.
- The adapter contract specification defines how external integrations work through the local API instead.
Pros and cons of the options
Static adapters only
- Good, because it removes the dynamic-loading attack surface.
- Bad, because it requires upstream contribution for new adapters.
Dynamic-library plugins
- Good, because it allows private adapters.
- Bad, because it adds a code-loading trust boundary.
Plugin server
- Good, because it isolates adapter failures.
- Bad, because it adds a process boundary and IPC complexity.
More information
Third parties that need a custom integration should use the adapter contract and the local API. Language bindings are tracked in ADR-0042.
ADR-0027: Approval resolutions and grants
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Approval protocol, Glossary, Glossary, Use case UC-05, ADR-0028
Context and problem statement
When a decision is pending, a human must resolve it.
The human’s answer is a resolution, which may produce a stored grant that satisfies future similar requests.
The project needed to define what resolutions are possible and how grants are scoped, so that an approver’s intent is captured precisely and cannot be abused.
Decision drivers
- Approvals must be precise: one-time, scoped, or denied.
- Grants must never override a
deny. - The approval format must be versioned and implementable by any channel.
Considered options
- Three resolutions:
allow_once,allow_always,deny, with single-use and scoped grants. - A free-form approval with arbitrary parameters.
- Binary only: allow or deny, no stored grants.
Decision outcome
Chosen option: “Three resolutions with single-use and scoped grants”, because it gives approvers the precision they need without unbounded complexity.
- A resolution is
allow_once,allow_always, ordeny. - An
allow_onceresolution creates a single-use grant bound to the digest of one action request. - An
allow_alwaysresolution creates a scoped grant covering an agent, action types, a resource pattern, and a workspace, with an expiry. - A grant can satisfy only a grantable
pendingresult. It never overridesdeny. - Approvers approve; the resulting decision is
allow— never the reverse.
Consequences
- Good, because approvers can choose the minimum scope they intend.
- Good, because grants are typed and bounded in time and scope.
- Bad, because scoped grants add complexity to grant management and revocation.
Confirmation
- The approval protocol specification defines the message formats and state machine.
- A property test suite verifies the approval state machine (exit criterion: v0.3).
Pros and cons of the options
Three resolutions, single-use and scoped grants
- Good, because it is precise and auditable.
- Bad, because it is more complex than binary.
Free-form parameters
- Good, because it is flexible.
- Bad, because it is hard to audit and easy to over-grant.
Binary, no grants
- Good, because it is simple.
- Bad, because it forces the human to approve every action, causing fatigue.
More information
The glossary defines grant and resolution. Deny-with-ticket for non-waiting hosts is in ADR-0028.
ADR-0028: Deny-with-ticket for hosts that cannot wait
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Approval protocol, Glossary, Glossary, ADR-0027
Context and problem statement
Some host hooks have strict timeouts and cannot wait for a human to approve a pending decision.
If the adapter simply denies the action, the agent loses the opportunity to proceed even after approval.
The project needed a pattern that lets the adapter deny immediately but still allow the action later if approved.
Decision drivers
- The adapter must respond within the host’s hook timeout.
- An approved action that was initially denied must be retryable.
- The approval must be bound to the original action request, not to anything the agent says.
Considered options
- Deny with a ticket: deny now, return a ticket, allow retry after approval consumes a single-use grant.
- Block the hook until approval (waiting past the host timeout).
- Skip the check entirely for risky actions (fail open).
Decision outcome
Chosen option: “Deny with a ticket”, because it respects the host’s timeout while preserving the ability to allow after approval.
- When a host cannot wait, the adapter denies the action and returns an approval ticket.
- The ticket encodes the action request digest.
- If the agent retries the same action after an approver allows it, the retry consumes a single-use grant and is allowed.
- The adapter never trusts the agent’s replay of the action; it recomputes the digest and checks for a matching grant.
Consequences
- Good, because the host’s timeout is respected.
- Good, because the action can still proceed after approval.
- Bad, because the agent must retry the exact same action; slight variations produce a new request with no matching grant.
Confirmation
- The approval protocol specification defines the ticket format and the retry flow.
- Integration pages document each host’s timeout and retry behavior.
Pros and cons of the options
Deny with a ticket
- Good, because it fits within host timeouts.
- Bad, because it requires the agent to retry.
Block until approval
- Good, because the agent does not need to retry.
- Bad, because it exceeds host timeouts and may hang the agent.
Skip the check
- Good, because it never blocks.
- Bad, because it fails open, violating fail-closed (ADR-0013).
More information
The glossary defines deny-with-ticket and grant. The approval protocol specification defines the ticket’s cryptographic binding.
ADR-0029: Hash-chained JSON Lines audit log
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Audit events, Glossary, Glossary, Use case UC-06
Context and problem statement
Every decision must leave a record that an operator or auditor can review afterward. The audit log must be append-only and tamper-evident: if an event is removed or altered, the tampering must be detectable. The format must be simple, streamable, and compatible with existing log-processing tools.
Decision drivers
- Tamper evidence: altering past events must be detectable.
- Streamable: events can be appended and read incrementally.
- Tool-compatible: existing JSON Lines tools can process the log.
Considered options
- JSON Lines where each event includes the hash of the previous event.
- A binary append-only log with fixed-size records.
- A database-backed log with transactional guarantees.
Decision outcome
Chosen option: “JSON Lines with hash chaining”, because it provides tamper evidence while remaining streamable and compatible with standard tooling.
- The audit log is an append-only JSON Lines file.
- Each audit event includes the hash of the previous event’s serialization, forming a chain.
- Removing or altering an event breaks the chain and is detectable by recomputing hashes.
- The log is tamper-evident, not tamper-proof: an attacker with write access can replace the whole file.
Consequences
- Good, because the format is human-readable and streamable.
- Good, because standard JSON tools can process it.
- Bad, because hash chaining provides no protection against an attacker who can rewrite the whole file; that requires signed checkpoints (ADR-0031).
Confirmation
- The audit events specification defines the event schema and the hash-chain fields.
shiin verifychecks the hash chain and reports breakage.
Pros and cons of the options
JSON Lines with hash chaining
- Good, because it is streamable, readable, and tamper-evident.
- Bad, because it does not prevent full-file replacement.
Binary fixed-size records
- Good, because it is efficient.
- Bad, because it is not human-readable and requires custom tools.
Database-backed log
- Good, because it has transactional guarantees.
- Bad, because it adds a database dependency and is harder to audit independently.
More information
Signed checkpoints and receipts are in ADR-0031. Audit durability (flushing, fsync) is in ADR-0030.
ADR-0030: Audit durability
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Error handling, Audit events, ADR-0013, ADR-0029
Context and problem statement
When an action is allowed, the audit event must be written before the action proceeds. If the write fails or the process crashes before flushing, the audit log has a gap. The project needed a durability model that handles write failures as a security event, not a silent skip.
Decision drivers
- Audit gaps must not be silent.
- The host must not proceed if the audit write fails for a non-read action.
- Performance matters: fsync on every event is expensive.
Considered options
- Write the audit event and fsync before the adapter returns
allow; a write failure producesdeny. - Buffer audit events in memory and flush periodically.
- Best-effort writes with no failure handling.
Decision outcome
Chosen option: “Write and fsync before returning allow; failure produces deny”, because an audit gap on an allowed action is a security-relevant failure.
- For every non-read action that the engine decides to
allow, the adapter writes the audit event to the log and flushes it before returningallowto the host. - If the audit write fails, the failure is routed through the same fail-closed conversion function (ADR-0013) and the decision becomes
deny. - Read actions that are allowed do not require an audit write before returning, but the decision is still recorded.
- The audit log uses buffered writes with periodic flushes for performance, but an explicit flush follows every event that gates a non-read action.
Consequences
- Good, because audit gaps on non-read actions are impossible without a
deny. - Good, because the failure mode is visible and logged.
- Bad, because fsync on every non-read action adds latency to the hook path.
Confirmation
- The error handling page routes audit-write failures through the single conversion function.
- The adapter contract specifies the pre-allowment audit ordering.
Pros and cons of the options
Fsync before allow
- Good, because audit events are durable before actions proceed.
- Bad, because fsync adds latency.
Periodic flush
- Good, because it is fast.
- Bad, because a crash can lose recent events.
Best-effort only
- Good, because it never blocks.
- Bad, because audit gaps are silent and undetectable.
More information
Audit events are hash-chained per ADR-0029. Signed checkpoints and receipts are in ADR-0031.
ADR-0031: DSSE and Ed25519 receipts
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Receipts, Glossary, Glossary, Glossary, Use case UC-06, ADR-0029
Context and problem statement
Hash chaining makes the audit log tamper-evident, but an attacker who can rewrite the whole file can also rewrite the hashes. The project needed a way to produce signed attestations that a decision was made, so that an independent observer can verify the log’s integrity without trusting the host.
Decision drivers
- Signatures must be verifiable by anyone with the operator’s public key.
- The format must be standard, not a custom invention.
- The signing key must be stored securely (see ADR-0032).
Considered options
- DSSE envelopes with Ed25519 signatures for checkpoints and receipts.
- A custom signature format wrapping the decision JSON.
- X.509 certificates with RSA signatures.
Decision outcome
Chosen option: “DSSE envelopes with Ed25519 signatures”, because DSSE is an emerging standard for software supply-chain attestations and Ed25519 is fast and widely supported.
- Checkpoints are signed DSSE envelopes containing the audit log’s head hash and position.
- Receipts are signed DSSE envelopes attesting that a specific decision was made.
- Signatures use Ed25519.
shiin verifychecks signatures and the hash chain offline.shiin replayre-evaluates recorded requests against a policy snapshot, including a different policy, to show what would have changed.
Consequences
- Good, because DSSE and Ed25519 are standards with libraries in many languages.
- Good, because offline verification does not require network access.
- Bad, because it adds a signing-key management requirement.
Confirmation
- The receipts specification defines the DSSE envelope structure.
- Receipts verify offline on every Tier 1 platform (exit criterion: v0.4).
Pros and cons of the options
DSSE with Ed25519
- Good, because it uses standard, audited formats.
- Good, because Ed25519 is fast and compact.
- Bad, because it requires key management.
Custom signature format
- Good, because it avoids external dependencies.
- Bad, because custom crypto is dangerous and not independently verifiable.
X.509 with RSA
- Good, because enterprises standardize on X.509.
- Bad, because it is heavier and not offline-friendly.
More information
The signing key storage ADR defines where keys live. The cryptography page describes the hash functions used in the chain.
ADR-0032: Signing key storage
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, ADR-0031, Receipts, Vision, ADR-0016
Context and problem statement
Shiin signs checkpoints and receipts with Ed25519 keys. Those keys must be stored in a way that is available to the signing process but not readable by agents. The project needed a storage strategy that works on the developer’s machine without requiring external infrastructure.
Decision drivers
- Keys must not be accessible to agents or untrusted code.
- The storage must work on Linux, macOS, and Windows.
- Key rotation must be supported.
Considered options
- Store keys in the OS keyring (keychain on macOS, Credential Manager on Windows, Secret Service on Linux).
- Store keys in a file protected by OS file permissions.
- Generate keys on demand per session with no persistent storage.
Decision outcome
Chosen option: “Store keys in the OS keyring”, because it is the most portable way to protect secrets at rest without configuration.
- Ed25519 signing keys live in the OS keyring under the service name
shiin. shiinreads the key for signing; agents cannot access the keyring through the adapter.- The built-in protected resources (ADR-0016) prevent policy from allowing agent access to the keyring.
- Key rotation is supported by
shiin key rotate. - If no key exists,
shiin initgenerates one.
Consequences
- Good, because the OS keyring is available on all Tier 1 platforms and protects keys at rest.
- Good, because no additional configuration is needed.
- Bad, because the keyring is per-user, so a team server scenario requires a different store (planned: after 1.0).
Confirmation
shiin initandshiin key rotatemanage the key.- The receipts specification references the key by its identifier, not its content.
Pros and cons of the options
OS keyring
- Good, because it is portable and protects keys at rest.
- Bad, because it is per-user and not suitable for a shared team server.
File with OS permissions
- Good, because it is simple and works everywhere.
- Bad, because file permissions are coarser and can be bypassed by a process running as the same user.
On-demand generation
- Good, because it needs no storage.
- Bad, because it cannot verify receipts from previous sessions.
More information
The cryptography page lists the hash functions and signature algorithms. The threat model covers key-extraction attacks.
ADR-0033: Enforcement levels
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Enforcement levels, Glossary, Glossary, Landscape, ADR-0025
Context and problem statement
Different integration methods provide different strengths of enforcement. A hook-based adapter can be disabled by the host; a proxy in the data path is harder to bypass; an OS-level control is strongest but hardest to deploy. The project needed a labeling scheme that honestly communicates how strong each integration is, so operators do not overestimate their protection.
Decision drivers
- Operators must know how strong their protection is.
- The scheme must be simple and honest.
- Labels must not overpromise.
Considered options
- Three labeled levels: L1 cooperative, L2 mediated, L3 OS-enforced.
- A numeric score (1–10) for each adapter.
- No labels; let operators infer the strength.
Decision outcome
Chosen option: “Three labeled levels”, because three levels are easy to remember and honest about what each integration can and cannot do.
- L1 Cooperative: The host honors the decision through its hook mechanism, which can be disabled (for example,
claude code --bareordisableAllHooks). - L2 Mediated: Shiin sits in the data path, as the MCP proxy does; the agent cannot route around it without disconnecting.
- L3 OS-enforced (future): The operating system or a sandbox enforces the decision, regardless of agent behavior.
- Every adapter states its enforcement level and its blind spots on its integration page.
Consequences
- Good, because operators know exactly how strong their protection is.
- Good, because the labels prevent overstating coverage.
- Bad, because some operators will want a finer-grained score; the three levels are a deliberate simplification.
Confirmation
- Each integration page under
guides/integrations/documents its enforcement level. - The enforcement levels page defines the criteria.
Pros and cons of the options
Three labeled levels
- Good, because it is simple and honest.
- Bad, because it does not capture every nuance.
Numeric score
- Good, because it is granular.
- Bad, because numbers imply precision that does not exist.
No labels
- Good, because it avoids prescriptive language.
- Bad, because operators overestimate their protection.
More information
The landscape page documents each host’s hook strengths and limits with verification dates. The vision states: “Shiin is designed to add to host controls, not to replace them.”
ADR-0034: Edition, MSRV, and toolchain
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Toolchain and MSRV, Workspace and crates, ADR-0002,
rust-toolchain.toml,Cargo.toml
Context and problem statement
Rust evolves quickly. A pinned toolchain ensures reproducible lint and formatting results, but contributors must be able to build with a slightly older compiler. The project needed to pick an edition, a pinned toolchain, and a minimum supported Rust version.
Decision drivers
- CI and contributor builds must be reproducible.
- The MSRV must not be too aggressive, so dependency graphs can support it.
- The edition must be current enough for the features the project needs.
Considered options
- Edition 2024, pinned toolchain 1.98.1, MSRV 1.96.
- Edition 2021, pinned toolchain 1.85, MSRV 1.83.
- Track latest nightly for cutting-edge features.
Decision outcome
Chosen option: “Edition 2024, pinned toolchain 1.98.1, MSRV 1.96”, because Edition 2024 is the current edition and the pinned/nearby versions are stable and widely supported.
- Edition 2024, set in
[workspace.package].editioninCargo.toml. - Pinned toolchain 1.98.1, set in
rust-toolchain.toml. - MSRV 1.96, set in
[workspace.package].rust-versioninCargo.toml(pinned stable minus 2 minor versions). rustfmtstyle edition is 2024, set inrustfmt.toml.- Dependency resolver 3, which is MSRV-aware.
Consequences
- Good, because the toolchain is reproducible across contributors and CI.
- Good, because the MSRV is achievable by most current Rust installations.
- Bad, because newer compiler features are unavailable until the MSRV is raised.
Confirmation
rust-toolchain.tomlandCargo.tomldeclare the versions.- The toolchain and MSRV page documents the policy for bumping them.
Pros and cons of the options
Edition 2024, toolchain 1.98.1, MSRV 1.96
- Good, because it uses the current edition and recent stable.
- Bad, because contributors on older Rust must upgrade.
Edition 2021, older toolchain
- Good, because it supports more legacy environments.
- Bad, because it forgoes Edition 2024 features and is less future-proof.
Track nightly
- Good, because it enables cutting-edge features.
- Bad, because nightly is unstable and breaks reproducibility.
More information
Changing the edition, MSRV, or toolchain rules requires a superseding ADR. The unsafe code policy ADR sets the baseline on which these rules build.
ADR-0035: Workspace layout
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Workspace and crates, ADR-0034, ADR-0037,
Cargo.toml,rust-toolchain.toml
Context and problem statement
As the project grows from one crate to many, the repository layout must keep the dependency graph acyclic, the crate boundaries clear, and the build efficient. A virtual workspace root with product crates in subdirectories is the standard Rust layout, but conventions vary.
Decision drivers
- No crate at the root should be privileged.
- Product crates should be grouped and clearly named.
- Repository tooling should be separate and never published.
- Fuzzing should not pollute the main workspace.
Considered options
- Virtual workspace root,
crates/<name>/for products,xtask/for tooling,fuzz/for fuzzing. - Root package with all crates as sub-crates.
- A monorepo with flat crate directories at the root.
Decision outcome
Chosen option: “Virtual workspace root, crates/ for products, xtask/ for tooling, fuzz/ for fuzzing”, because it avoids privileging a root crate and groups concerns clearly.
- The root
Cargo.tomlis a virtual manifest with no[package]section. - Product crates live in
crates/<name>/, where the directory name is the crate name. xtask/provides repository automation (cargo xtask docs-check) and setspublish = false.fuzz/is a separate workspace with its own nightly toolchain for fuzz targets.- Every member inherits
edition,rust-version,license,repository, andauthorsfrom[workspace.package]. - Every member sets
[lints] workspace = true, exceptshiin-platform.
Consequences
- Good, because no crate is privileged by living at the root.
- Good, because crates are grouped logically.
- Good, because
xtaskandfuzzare isolated from the published crates. - Bad, because
crates/<name>/adds a directory level; contributors must learn the layout.
Confirmation
Cargo.tomland.cargo/config.tomlimplement this layout.- The workspace and crates page documents the full crate list.
Pros and cons of the options
Virtual workspace root
- Good, because no crate is privileged.
- Good, because the layout is the Rust community standard.
- Bad, because it adds a directory level for product crates.
Root package
- Good, because it is simple for a single-crate project.
- Bad, because it privileges one crate and does not scale to many crates.
Flat directories
- Good, because it avoids nested directories.
- Bad, because it mixes product and tooling crates without clear boundaries.
More information
The workspace and crates page is the single source of truth for the crate list and their responsibilities.
ADR-0036: Error handling
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Error handling, ADR-0013, ADR-0034, ADR-0002
Context and problem statement
Shiin’s code handles untrusted input from agents and hosts, and its error handling must never let a failure let an action through. At the same time, operators need clear diagnostics to fix configuration errors, and internal bugs must be caught during testing. The project needed a taxonomy of failure that separates authorization decisions from operational errors and diagnostics.
Decision drivers
- Decisions (
allow,deny,pending) are not errors and never becomeErr. - Operational errors must produce fail-closed
deny. - Diagnostics must be actionable for operators.
- Bugs must be caught in tests, not silently handled.
Considered options
- A four-category taxonomy: decisions, operational errors, user diagnostics, and bugs.
- A single
Result<Decision, Error>with errors propagated everywhere. - Panic on any unexpected condition and let the host handle it.
Decision outcome
Chosen option: “Four-category taxonomy”, because it keeps concerns separated and ensures failures always produce deny.
- Decisions (
Ok(Decision)):allow,deny,pending. NeverErr. - Operational errors (per-crate
thiserrorenums,#[non_exhaustive]): unreadable files, IPC timeouts, malformed payloads. Mapped to fail-closeddenyby one conversion function. - User diagnostics (with source spans, rendered by
miette): syntax errors in policy files. Shown to the operator, never to the agent. - Bugs (
debug_assert!, invariant violations): fail during testing; production failures producedenyvia the panic hook.
Consequences
- Good, because the single conversion function means there is one place to review and test.
- Good, because diagnostics are separated from operational errors.
- Bad, because the taxonomy adds upfront design cost.
Confirmation
- Every operational error routes through the single
deny_on_failurefunction. - A fault-injection test makes the hook panic and asserts the result is
deny.
Pros and cons of the options
Four-category taxonomy
- Good, because it separates concerns and ensures fail-closed.
- Bad, because it is more structured than a single error type.
Single Result with errors everywhere
- Good, because it is simple.
- Bad, because it conflates decisions with failures and risks letting
?turn adenyinto an error path.
Panic on unexpected conditions
- Good, because it crashes fast.
- Bad, because hosts may treat a crash as permission to proceed.
More information
The error handling engineering page describes the implementation. Each error variant has a stable code listed in error codes.
ADR-0037: Unsafe code policy
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Unsafe code policy, Workspace and crates, ADR-0002, ADR-0038
Context and problem statement
Shiin parses untrusted input from agents and hosts, including shell commands, file paths, and tool payloads.
Unsafe Rust disables compiler checks that the project relies on to guard these paths.
A memory-safety bug in Shiin could change a decision from deny to allow, completely defeating the authorization layer.
Decision drivers
- Unsafe code must be minimized and contained.
- Every unsafe block must be justified and reviewed.
- The workspace lints must not be overridable by individual crates.
Considered options
- Forbid
unsafe_codein all crates except a singleshiin-platformcrate.
unsafe_code = "forbid"in[workspace.lints.rust].- Only
shiin-platformis allowedunsafe, and only where no safe crate provides the needed facility. - Every
unsafeblock has a// SAFETY:comment, one unsafe operation per block, and tests on every supported platform. shiin-platformdeclares its own lint table (copied from the workspace, withunsafe_codechanged todeny).- Soundness bugs are treated as vulnerabilities through ADR-0006, not as ordinary bugs.
Consequences
- Good, because unsafe code is confined to one small, heavily reviewed crate.
- Good, because
forbidcannot be overridden byalloworexpectinside a crate. - Bad, because creating
shiin-platformadds review overhead; it is created only when a concrete need exists.
Confirmation
- The unsafe code policy page and
Cargo.tomlenforce the rule. - Two-maintainer review is required for any unsafe change.
- Miri runs
shiin-platformtests in CI.
Pros and cons of the options
Forbid unsafe, one exception crate
- Good, because unsafe is minimized and contained.
- Bad, because it requires
shiin-platformfor any OS facility not covered by safe crates.
Allow unsafe everywhere with review
- Good, because it is flexible.
- Bad, because it distributes unsafe code across many crates and reviewers.
No unsafe at all
- Good, because it is simplest.
- Bad, because some OS facilities (Windows named pipes, raw syscalls) require it.
More information
The unsafe code policy page is the detailed rulebook. The supply chain policy ADR covers unsafe code in dependencies.
ADR-0038: Supply chain policy
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Workspace and crates, ADR-0037, ADR-0034, ADR-0039
Context and problem statement
Shiin depends on third-party crates, some of which contain unsafe code.
A vulnerable or compromised dependency could let an attacker change a decision or extract data.
The project needed a supply-chain policy that admits dependencies only after review.
Decision drivers
- Dependencies must be declared once at the workspace level with default features off.
- Unsafe usage in dependencies must be weighed against safe alternatives.
- Security advisories in the RustSec database must be monitored.
Considered options
- Declare all dependencies in
[workspace.dependencies]withdefault-features = false, and review each for safety and audit history. - Allow per-crate dependency declarations without central review.
- Vet every dependency through cargo-vet before use.
Decision outcome
Chosen option: “Workspace-level declaration with default features off and review-based admission”, because it gives visibility and control without requiring a vet database from day one.
- Every third-party dependency is declared in
[workspace.dependencies]with a version requirement anddefault-features = false. - Members reference it with
workspace = trueand enable only the features they use. - Admission criteria include: amount of unsafe code, documentation of unsafe blocks, RustSec advisory history, and whether a safe alternative exists.
cargo-denychecks advisories, licenses, bans, and sources in CI.cargo-vetis not selected; its audit database would add maintenance cost without replacing the configuredcargo-denychecks.
Consequences
- Good, because all dependencies are visible in one place.
- Good, because default features are off, reducing the attack surface.
- Bad, because the review process adds latency to adopting new dependencies.
Confirmation
Cargo.tomldeclares dependencies in[workspace.dependencies].- CI runs
cargo deny check.
Pros and cons of the options
Workspace-level declaration with review
- Good, because it provides visibility and control.
- Bad, because it adds review overhead.
Per-crate declarations
- Good, because it is simpler.
- Bad, because dependencies are scattered and hard to audit.
cargo-vet
- Good, because it is thorough.
- Bad, because it requires maintaining a vet database, which is heavy for a pre-alpha project.
More information
The workspace and crates page documents the dependency rules. The unsafe code policy ADR covers unsafe code in dependencies.
ADR-0039: Release tooling
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Release engineering, ADR-0002, ADR-0017, ADR-0031
Context and problem statement
Shiin ships signed release binaries with a software bill of materials. The release process must generate checksums, SBOMs, signatures, and changelog entries, all reproducibly. The project needed a release tooling strategy that works within the Rust ecosystem and the DCO-only contribution model.
Decision drivers
- Release artifacts must be signed and checksummable.
- The changelog must be generated from commits (Conventional Commits).
- SBOMs must be produced for each binary.
Considered options
- A release-tooling crate in
xtask, pluscargo-releaseor a custom script, using GitHub Actions. - Hand-crafted release scripts in shell.
- A third-party release service (for example, Release Please).
Decision outcome
Chosen option: “xtask-managed release with cargo-release or equivalent, GitHub Actions for CI”, because it keeps the release logic in Rust and the workflow configuration standard.
- Release artifacts are built with
cargo build --releasefor all Tier 1 platforms. - Each binary is signed with Ed25519 and has a checksum file.
- An SBOM is generated in SPDX format.
- The changelog is generated from Conventional Commits via the release tooling.
- GitHub Actions runs the release workflow with
contents: writepermission. - This is the only place in CI with write permissions.
Consequences
- Good, because releases are reproducible and the tooling is in Rust.
- Good, because the changelog is derived from commits automatically.
- Bad, because the release workflow has write access and must be carefully guarded.
Confirmation
Partly confirmed on 2026-10-12. dist is configured for the six target platforms with
installers and build provenance, and the workflow it generates is checked in with every
action pinned by commit. Two gaps are recorded rather than hidden: dist rewrites its own
workflow with tag references, so a pin check in ci.yml guards the pins, and dist offers no
option for cargo auditable, so that metadata is not in released binaries. The bill of
materials is produced by a separate workflow whose command has been verified locally.
The decision is only fully confirmed when a real tag produces a verifiable release.
Original confirmation
- The release engineering page documents the full process.
- Release binaries are signed with the signing key from ADR-0032.
Pros and cons of the options
xtask-managed release
- Good, because it keeps release logic in Rust and is reproducible.
- Bad, because writing release code in Rust is more involved than a shell script.
Shell scripts
- Good, because they are quick to write.
- Bad, because they are hard to test and maintain across platforms.
Third-party service
- Good, because it is hands-off.
- Bad, because it adds a dependency on an external service and may not integrate with DCO.
More information
Pre-release binaries use the shiin policy validate TOML checker (planned: v0.1). Post-1.0 releases include an SBOM in SPDX format.
ADR-0040: Platform support tiers
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Platform support, Toolchain and MSRV, Unsafe code policy, Roadmap
Context and problem statement
Shiin runs on the developer’s machine and must decide how many platforms to officially support with CI, prebuilt binaries, and tests. Supporting every platform from day one spreads the project thin. The project needed a tiering scheme that sets expectations for where Shiin works, is tested, and ships binaries.
Decision drivers
- Tier 1 must be the platforms the maintainers use and CI tests.
- Tier 2 is best-effort with no CI guarantee.
- Tier 3 is community-supported or not yet ported.
Considered options
- Three tiers: Tier 1 (tested + binaries), Tier 2 (builds but no CI), Tier 3 (community).
- Binary support only for the latest two stable releases of each OS.
- No tiers; support everything.
Decision outcome
Chosen option: “Three tiers with CI and binaries for Tier 1 only”, because it sets clear expectations and keeps CI focused.
- Tier 1: Linux (x86-64), macOS (aarch64), Windows (x86-64). CI runs on every pull request. Prebuilt binaries shipped.
- Tier 2: Linux (aarch64), macOS (x86-64). Builds in CI but not guaranteed.
- Tier 3: All other platforms. No CI guarantee; community builds welcome.
- Performance budgets and receipts are verified on Tier 1 platforms (exit criterion: v0.4).
- Windows promotion from Tier 2 to Tier 1 is planned for v0.6.
Consequences
- Good, because CI and binaries focus on a manageable set of platforms.
- Good, because tier expectations are clear to users.
- Bad, because some platforms get no prebuilt binaries.
Confirmation
- The platform support page lists all tiers.
- CI runs on Tier 1 platforms only.
Pros and cons of the options
Three tiers
- Good, because it is simple and sets clear expectations.
- Bad, because Tier 3 users get no guarantees.
Latest two stable releases only
- Good, because it limits OS version support.
- Bad, because it does not distinguish architecture or CI coverage.
Support everything
- Good, because it maximizes reach.
- Bad, because testing and binary distribution become intractable.
More information
The roadmap mentions Windows promotion to Tier 1 in v0.6. The performance budgets page specifies latency and memory targets per tier.
ADR-0041: Remote approver transport
Note
Architecture decision: deferred. Deferred to v0.6. Do not implement until confirmed by an RFC.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, Approval protocol, ADR-0027, Roadmap
Context and problem statement
The v0.3 approval protocol supports local approvers (command-line, desktop, editor). For teams or headless environments, approvals may need to reach a remote approver through a webhook or API. The transport mechanism for remote approvals must be secure, authenticated, and replay-resistant.
Decision drivers
- Remote approval must not expose sensitive action data to an untrusted channel.
- Approvals must be non-repudiable.
- The transport must be opt-in, consistent with the no-telemetry principle.
Considered options
- Signed webhooks with Ed25519, delivered over HTTPS with mutual TLS.
- A centralized approval server with its own identity model.
- Polling-based approval via a REST API.
Decision outcome
Chosen option: “Deferred pending RFC”, because the threat model for remote approvals and the exact transport are not yet settled.
- This ADR is deferred to v0.6.
- The approval protocol is designed to be transport-independent so a future RFC can add remote transport without changing the approval message format.
- The local-first principle (ADR-0007) and local-first deployment (ADR-0022) apply: remote approval is opt-in, never default.
Consequences
- Good, because deferring avoids committing to a transport that may be superseded.
- Bad, because teams needing remote approvals today have no path.
Confirmation
- The RFC process will produce a proposal for remote approval transport.
- The approval protocol specification must remain transport-agnostic.
Pros and cons of the options
Signed webhooks over HTTPS
- Good, because it is standard and replay-resistant with timestamps.
- Bad, because it requires HTTPS infrastructure and mutual TLS management.
Centralized server
- Good, because it is a simple model.
- Bad, because it creates a central point of failure and a data-collection risk.
REST polling
- Good, because it works behind firewalls.
- Bad, because it is slower and harder to authenticate.
More information
The roadmap v0.6 section lists remote approvals. This ADR must be accepted by an RFC before implementation begins.
ADR-0042: Language bindings
Note
Architecture decision: accepted.
- Date: 2026-09-14
- Deciders: Founding maintainer
- Supersedes: none
- Superseded by: none
- Related: ADR index, ADR-0002, ADR-0017, ADR-0023, Workspace and crates
Context and problem statement
The Rust client library (shiin-client) is the primary way to embed Shiin in custom agents and tools.
But many agent developers use Python, TypeScript, Java, or Go.
After 1.0, the project needs a path for non-Rust integrators to use Shiin without writing Rust.
Decision drivers
- The stable public API consists of
shiin-schemaandshiin-clientonly. - Bindings must not pull in the full engine or Cedar crate.
- Bindings should be generated or maintained with minimal manual effort.
Considered options
- Ship
shiin-clientas the Rust native library, then provide FFI bindings for other languages after 1.0. - Provide official bindings for Python and TypeScript from the start.
- Rely on third parties to write unofficial bindings using the local API.
Decision outcome
Chosen option: “Rust native first, FFI or language-wrapper bindings after 1.0”, because a single stable Rust API ensures consistency, and bindings can be built on top of it.
- The Rust client library ships in v0.5 (ADR-0002 rationale: one toolchain).
- Non-Rust bindings are added after 1.0 when the Rust API is stable.
- Bindings use the local API (ADR-0023) or FFI over
shiin-client. - The JSON wire format (ADR-0017) enables any language to interoperate even before official bindings.
Consequences
- Good, because the focus is on a correct, stable Rust API first.
- Good, because the JSON wire format allows unofficial integrations from day one.
- Bad, because non-Rust developers must use the raw JSON RPC or write their own adapter until bindings ship.
Confirmation
- The workspace and crates page lists
shiin-clientas the only stable public Rust API (besidesshiin-schema). - The local API specification is designed to be implementable from any language.
Pros and cons of the options
Rust native first, bindings after 1.0
- Good, because the API is correct and stable before binding work.
- Bad, because non-Rust teams wait for bindings.
Python and TypeScript from the start
- Good, because it captures those markets early.
- Bad, because it splits the team across multiple language ecosystems and delays the Rust API.
Third-party bindings only
- Good, because the core team is not responsible.
- Bad, because quality and maintenance vary.
More information
The vision states that after 1.0, teams need shared policy and language bindings. The local API specification provides the contract that any language binding can implement.
ADR template
Note
Architecture decision: proposed. Copy this file to
docs/adr/NNNN-short-title.md, replace every placeholder, and add the new record to the ADR index.
This template follows MADR 4. Keep records short. An ADR explains one decision, the options considered, and the consequences. Detailed rules belong in the specification the ADR links to.
A new record’s title is written as # ADR-NNNN: Title in sentence case.
Its metadata comment uses kind=adr and one of the statuses
proposed, accepted, deferred, superseded, or deprecated.
- Date: YYYY-MM-DD
- Deciders: names or handles
- Supersedes: ADR-NNNN, or “none”
- Superseded by: ADR-NNNN, or “none”
- Related: links to specifications, other ADRs, and issues
Context and problem statement
Describe the situation and the question that needs an answer, in two to five sentences.
Decision drivers
- A force or constraint that shapes the decision
- Another one
Considered options
- Option one
- Option two
- Option three
Decision outcome
Chosen option: “Option one”, because it best satisfies the decision drivers in the following way.
Consequences
- Good, because of a benefit.
- Bad, because of a cost that is accepted.
Confirmation
How compliance with the decision is checked: a test, a CI check, a review rule, or a spike with exit criteria.
Pros and cons of the options
Option one
- Good, because of a strength.
- Bad, because of a weakness.
Option two
- Good, because of a strength.
- Bad, because of a weakness.
More information
Links, prior art, and conditions under which the decision should be revisited.
RFC process
Note
RFC: accepted. This page defines the process for proposing changes to Shiin’s specifications.
When to use an RFC
An RFC is required when a change:
- Changes an accepted specification.
- Adds a new action-type namespace.
- Adds a policy-language feature.
- Adds, removes, or reorders milestone scope.
Process
- Copy the RFC template to
NNNN-short-title.md. - Open a pull request with the proposal.
- Reviewers discuss and the maintainers accept or reject.
- An accepted RFC produces an ADR.
Statuses
| Status | Meaning |
|---|---|
draft | The proposal is written and open for review. |
accepted | The proposal is accepted and produces an ADR. |
rejected | The proposal is not accepted. |
superseded | Replaced by a later RFC. |
Current proposals
- RFC-0001: Remote approver transport proposes opt-in signed polling over outbound HTTPS.
Read next
RFC-0001: Remote approver transport
Note
RFC: draft. This proposal is not accepted. Remote-approval implementation remains blocked on maintainer review and an accepted decision record.
- Date: 2026-10-16
- Status: draft
- Supersedes: none
- Superseded by: none
Summary
This RFC proposes an opt-in outbound HTTPS polling transport for remote approvals. It replaces the v0.6 roadmap’s unspecified “signed webhooks” wording with a transport that does not require an inbound listener on the developer’s machine. Every response is signed by a configured remote approval key and bound to one pending ticket, request binding, nonce, and expiry.
Motivation
The local daemon currently requires an operator terminal to resolve a pending action. Headless and team environments need a path for a human to review that action remotely. The existing ADR-0041 defers implementation until the transport and its threat model are settled.
Proposal
- Remote approval is disabled unless the operator configures an HTTPS endpoint and a trusted Ed25519 response key.
- The local daemon sends only pending approval requests to that endpoint and polls for a response. It does not open a listener or accept an unsolicited remote connection.
- The outbound request includes the existing ticket and normalized action summary, a digest of the bound request, a fresh random nonce, an audience identifier, and a short expiry. It excludes operator credentials, signing keys, replay keys, and unrelated audit history.
- The remote response is a signed, versioned envelope that binds the endpoint audience, ticket, request digest, nonce,
approver_id, resolution, optional grant scope, lifetime, issue time, and expiry. - The daemon verifies the configured key, signature, audience, ticket, request digest, nonce, resolution, and time bounds before passing the response to the existing approval state machine.
The state machine still re-evaluates current policy and task state, and a grant never overrides
deny. - HTTPS certificate validation is mandatory. Redirects are rejected, request and response sizes are bounded, and endpoint configuration is operator-owned rather than workspace-owned.
- Polling failures, invalid signatures, stale responses, missing nonces, unknown fields, and expired requests leave the action
pendingor deny it according to the existing attended/unattended behavior. None may produceallow. - The transport remains opt-in and performs no background network activity when no endpoint is configured.
Considered alternatives
- Inbound signed webhooks: they can deliver decisions promptly but require a remotely reachable callback and enlarge the daemon’s network attack surface.
- Unsigned REST polling: it avoids inbound connections but does not provide a durable, independently verifiable approver statement.
- A centralized Shiin approval service: it moves identity, availability, and data custody into a hosted service, which conflicts with the current local-first deployment model.
Consequences
- A signed response can prove that a configured key approved a specific pending request, but it cannot prove the remote service authenticated a human correctly.
- Operators must protect the remote key and review which action fields leave the machine.
- Polling adds latency and depends on both the remote endpoint and the local network.
- The approval protocol remains transport-independent; this proposal adds a transport profile without changing its existing resolution values.
- Threat-model review must cover endpoint trust, sensitive action disclosure, key rotation, DNS rebinding, response replay, and compromised remote services before implementation.
Confirmation
Before implementation, maintainers must accept or revise this RFC and replace deferred ADR-0041 with an accepted ADR.
Conformance tests must prove that wrong keys, altered payloads, stale tickets, mismatched bindings, reused nonces, redirects, timeouts, and malformed responses never produce allow.
The transport must also pass an independent threat-model review.
RFC template
Note
RFC: accepted. This is the template for proposal records.
RFC-NNNN: Short title
- Date: YYYY-MM-DD
- Status: draft
- Supersedes: none
- Superseded by: none
Summary
In one paragraph, what is proposed.
Motivation
Why is this change needed?
Proposal
What exactly changes?
Considered alternatives
What else was considered and why was it rejected?
Consequences
What does this change affect?
Confirmation
How will this change be confirmed?
Roadmap
Note
Project policy: draft. This roadmap is the project’s plan of record. It changes through pull requests, and larger changes go through the RFC process.
How to read this roadmap
- Milestones are defined by capability, not by date. A milestone is finished when its exit criteria are met.
- Each milestone lists what is out of scope, so that “not yet” is an explicit decision.
- Shipping a milestone includes updating the documentation: every page for that milestone changes its banner from “Design intent: not implemented” to the implemented state.
- Versions before 1.0 may contain breaking changes, as described in Versioning and stability.
Overview
| Milestone | Theme | Headline capability |
|---|---|---|
| M0 | Specification | Specs, threat model, test vectors, and the policy-engine spike |
| v0.1 | Gate | Claude Code adapter with policy, basic risk signals, and an audit log |
| v0.2 | Tasks | Task declarations and intent binding; Codex CLI and Cursor adapters |
| v0.3 | Approve | Daemon, approval protocol, and grants |
| v0.4 | Evidence | Signed receipts, verification, and replay |
| v0.5 | MCP and SDK | MCP proxy, local API, and the Rust client library |
| v0.6 | Reach | Remote approvals, streamable-HTTP MCP, shell wrapper, Windows |
| v1.0 | Stable | Frozen specifications, audited, conformance suite |
| After 1.0 | Teams | Team policy server, more approver apps, language bindings |
M0: Specification
Goal. Settle the design before writing product code.
Scope.
- The documentation set in
docs/, including every specification listed in About the specifications. - JSON Schemas and test vectors that encode the use cases.
- The Cedar spike required by ADR-0014, done: it reproduced every test vector, so ADR-0014 is accepted.
Exit criteria.
- The action request, decision, evaluation, policy model, and adapter contract specifications are
accepted. Done on 2026-10-12. - The threat model has been reviewed by at least one person outside the project. Open: this is the last M0 criterion, and it needs a reviewer who is not the author.
- The core golden use cases have test vectors. The original eight-case corpus has grown to 26 cases, each decidable from its own inputs and checked in continuous integration.
- ADR-0014 is
accepted. Done on 2026-10-12.
v0.1: Gate
Goal. A developer using Claude Code on Linux or macOS can enforce a policy and see a record of every decision.
Scope.
- Claude Code hook adapter, running without a daemon.
- Identity at the
assertedlevel, capability, policy, and thesecret_path,outside_workspace,bulk_delete, andopaque_commandrisk signals. - Built-in protected resources.
- Commands:
shiin init,shiin check,shiin explain,shiin policy validate,shiin hook claude-code,shiin log, andshiin doctor. - Hash-chained audit log.
pendingexpressed through Claude Code’s native permission prompt. This is an interim step, not the approval protocol.- Signed release binaries with a software bill of materials.
Out of scope. Daemon, approval protocol, grants, tasks, receipts, Windows.
Exit criteria.
- All v0.1 test vectors pass.
- The performance budgets for the daemonless hook path are met on Tier 1 platforms.
- A fault-injection test shows that crashes and timeouts in the hook produce
deny.
v0.2: Tasks
Goal. Actions can be bound to the task the developer gave, and the same policy works across three hosts.
Scope.
- Task declarations with the closed constraint set, marked experimental. Done: scope, forbidden action types, allowed hosts, counting limits, ask-for, and expiry.
- Counters for task limits. Done.
- Codex CLI and Cursor hook adapters. Done in source; neither has been tested against a running host, which is the remaining v0.2 work.
shiin taskandshiin policy test. Done.- Unattended mode for CI. Done in v0.1.
Out of scope. Approval protocol, SQL classification.
Exit criteria.
- Intent-binding test vectors pass for all three hosts.
- The intent-binding evaluation plan in Intent binding has produced its first measurements. Done for the nine-case labelled task corpus; independent real-workload evaluation remains open.
v0.3: Approve
Goal. pending decisions go through a real approval protocol instead of host prompts.
Scope.
shiind, the per-user daemon.- Approval protocol v1 with the command-line approver.
- Single-use and scoped grants, and revocation.
- Deny-with-ticket for hosts that cannot wait.
Out of scope. Remote approvers, desktop or editor approvers.
Exit criteria.
- Approval state-machine property tests pass.
- Self-approval through an intercepted channel is shown to be impossible by test.
v0.4: Evidence
Goal. Decisions can be proven and re-examined.
Scope.
- Signed checkpoints and receipts.
shiin verifyandshiin replay, including what-if replay against a different policy.- Key management and rotation.
- Redaction of sensitive values in audit events.
cargo-denyenforced in CI, as selected in ADR-0038.
Exit criteria.
- Receipts verify offline on every Tier 1 platform. Unit tests and native key-store lifecycle checks pass on hosted Linux, macOS, and Windows runners; independent review and live operator-environment qualification remain open.
- Replay reproduces every decision in the conformance corpus. Done for the 26-case signed/encrypted corpus through the CLI evaluation routine, including capability, session/task, what-if, and grant regression tests.
v0.5: MCP and SDK
Goal. Shiin mediates MCP traffic and can be embedded in custom agents.
Scope.
- MCP proxy for spec revision 2026-07-28, stdio transport first.
- Local API over the daemon.
shiin-client, the Rust client library.- Peer-credential identity (
attested-local). - SQL and migration classifiers. Partial classifier primitives exist in source; host integration remains open.
Exit criteria.
- The MCP proxy passes conformance tests against reference MCP servers.
- A custom agent built with
shiin-clientpasses the adapter conformance suite.
v0.6: Reach
Goal. Shiin works in more environments.
Scope.
- Remote approvers through signed webhooks (ADR-0041 must be decided first).
- Streamable-HTTP MCP transport. Downstream HTTP and HTTPS with a provisioned bearer token have process-fixture coverage; the upstream connection remains stdio, and OAuth token acquisition/refresh and external conformance remain open.
- Shell wrapper for hosts without hooks.
shiin execnow has a Unix source implementation; live host routing and platform validation remain open. - Windows promoted toward Tier 1.
- Additional hosts, each with its hook behavior verified and documented.
v1.0: Stable
Goal. Shiin is dependable enough to build on.
Exit criteria.
- The v1 specifications and the policy language are frozen.
- An independent security audit is complete, and its report is published.
- The conformance suite is published.
- All success criteria in the vision are met.
After 1.0
- Team policy server with signed policy bundles and central evidence.
- Desktop and editor approver applications.
- Language bindings (ADR-0042).
Not planned
These are deliberately not on the roadmap. Proposing any of them requires an RFC.
- Machine-learning or language-model decisions.
- Telemetry or any phone-home behavior.
- Dynamic-library plugins for adapters (ADR-0026).
- Compliance certifications.
Changing the roadmap
Small corrections are ordinary pull requests. Adding, removing, or reordering milestone scope requires an RFC.
Versioning and stability
Note
Project policy: draft. This page is the only home of Shiin’s versioning and stability rules.
Versions before 1.0
Versions before 1.0 may contain breaking changes.
A breaking change adds ! after the scope in the commit subject and a BREAKING CHANGE: footer.
Versions after 1.0
- Patch (x.y.Z): Backward-compatible fixes only.
- Minor (x.Y.z): Backward-compatible features. May raise the MSRV.
- Major (X.y.z): Breaking changes.
Specifications
The v1 specifications and the policy language are frozen under the versioning policy.
Before 1.0, specifications are versioned as v1-draft.N and may change.
Library crates
shiin-schema and shiin-client are the only crates intended to become stable public APIs.
After 1.0 they follow the guarantees in this page.
Read next
Privacy and telemetry
Note
Project policy: accepted. This page is the only home of Shiin’s privacy and telemetry rules (ADR-0007).
No telemetry
Shiin collects no telemetry and has no phone-home behavior. It needs no service and sends no data off the developer’s machine.
What stays local
All policy, audit logs, grants, tasks, and signing keys stay on the developer’s machine.
Network access
Every network feature is opt-in. Shiin’s engine performs no networking at all.
Remote approvals
Remote approvals are planned for v0.6 and require an explicit opt-in (ADR-0041).
Read next
Documentation style guide
Note
Project policy: accepted. Every page under
docs/follows this guide. Rules marked (checked) are enforced bycargo xtask docs-check.
Shiin’s documentation is written before its implementation. This guide keeps a large set of pages consistent, honest, and verifiable so that readers, contributors, and security reviewers can trust what they read.
The honesty rule
A reader must never mistake a design for a working feature.
- Never claim that something exists today (that it can be installed, runs, is tested, or is supported)
unless the page’s
implementationfield ispartialorcomplete. - Pages that describe designed behavior may use the present tense (“the engine evaluates stages in order”) because their banner states that no implementation exists.
- When a page mixes built and planned behavior, mark each planned item inline, for example
(planned: v0.3). - The pull request that ships a feature updates the affected pages’
implementationandmilestonefields and their banners. (checked: the banner must match the metadata)
Document kinds
Each page has exactly one kind. Kinds follow the Diátaxis framework, extended with project-specific kinds.
| Kind | Purpose | Usual location |
|---|---|---|
index | Landing page that orients readers | docs/README.md |
explanation | Why the design is the way it is | overview/, concepts/, architecture/ |
spec | Precise, normative contracts that implementations are checked against | spec/, security/security-model.md |
reference | Facts to look up | reference/, glossary.md |
how-to | Steps to accomplish a goal | guides/ |
tutorial | A guided first experience | getting-started/ |
adr | One architecture decision | adr/ |
rfc | The proposal process and its template | rfcs/ |
policy | Rules the project and its contributors follow | project/, engineering/, security/vulnerability-disclosure.md |
Page metadata (checked)
Line 1 of every page under docs/, except SUMMARY.md, is exactly one HTML comment:
<!-- shiin-doc: kind=<kind> status=<status> implementation=<implementation> milestone=<milestone> reviewed=<YYYY-MM-DD> -->
The comment is invisible on GitHub and in the built book, so no front-matter preprocessor is needed. Fields appear in this order, separated by single spaces.
| Field | Allowed values | Meaning |
|---|---|---|
kind | see Document kinds | What sort of page this is |
status | draft, proposed, accepted, deferred, superseded, deprecated | Where the content is in its review lifecycle |
implementation | none, partial, complete, n/a | How much of the described behavior exists in released code; n/a for process pages |
milestone | m0, v0.1 through v0.6, v1.0, post-1.0, n/a | The roadmap milestone the content targets; see Roadmap |
reviewed | ISO 8601 date | When a maintainer last confirmed the page is accurate |
spec, tutorial, and how-to pages must not use implementation=n/a.
Title and banner (checked)
After the metadata comment and a blank line comes the page title as a level-1 heading, then a blank line, then a banner written as a GitHub-style admonition. mdBook and GitHub both render these admonitions.
<!-- shiin-doc: kind=spec status=draft implementation=none milestone=v0.1 reviewed=2026-09-14 -->
# Action request
> [!NOTE]
> **Specification: draft.**
> Normative design. No implementation exists yet.
The first banner line after the admonition marker must begin with the label shown below. The rest of the banner is free text.
| Metadata | Admonition | Banner label |
|---|---|---|
kind=index | [!NOTE] | **Project status: pre-alpha.** |
kind=spec | [!NOTE] | **Specification: <status>.** |
kind=explanation | [!NOTE] | **Design document: <status>.** |
kind=adr | [!NOTE] | **Architecture decision: <status>.** |
kind=rfc | [!NOTE] | **RFC: <status>.** |
kind=policy | [!NOTE] | **Project policy: <status>.** |
kind is tutorial, how-to, or reference, with implementation=none | [!WARNING] | **Design intent: not implemented.** |
kind is tutorial or how-to, implemented | [!NOTE] | **Guide: <status>.** |
kind=reference, implemented or n/a | [!NOTE] | **Reference: <status>.** |
Recommended wording for design-intent pages: “Planned for v0.1. Nothing on this page works yet. It describes the intended behavior so it can be reviewed before it is built.”
Files, folders, and headings
- File and folder names are lowercase
kebab-casewith the.mdextension. - Every page appears in
SUMMARY.md, and everySUMMARY.mdentry points to an existing page. (checked) - Headings use sentence case: “Decision pipeline”, not “Decision Pipeline”.
- A page has one level-1 heading. Headings must be unique within a page, because anchors are derived from heading text.
- Do not use heading ID attributes (
{#id}) or definition lists; GitHub renders neither.
Writing style
- Use American English and the serial (Oxford) comma.
- Write one sentence per line (semantic line breaks). Rendering is unaffected, and diffs show exactly which sentence changed.
- Prefer short, direct sentences. Put the conclusion first.
- Address the reader as “you” in tutorials and how-to guides. Specifications describe roles (“the adapter”, “the approver”) and never “you”.
- Link a term to the glossary the first time it appears on a page.
- Never use humor, marketing superlatives, or unqualified security claims (“unbreakable”, “fully secure”).
Terminology (checked in part)
glossary.md is the only place terms are defined.
It also contains the table of avoided terms and their replacements.
The most important rules:
- Decision values are
allow,deny, andpending. Use “ask” only when naming a host’s native mechanism, such as Claude Code’saskpermission decision. (checked:blockin code formatting is rejected) - A human approves; the resulting decision is allow.
- Shiin’s unit is the action. “Tool call” is the host’s term and appears only on host-specific pages.
- Say capability or rule, not “permission”, for Shiin concepts.
- Shiin is an authorization layer. It is never described as a firewall, guardrail, or sandbox, except when contrasting it with those tools. (checked: “Shiin is a firewall” and similar phrasings are rejected)
- Write “allowlist” and “denylist”. (checked)
- “Shiin” is the project.
shiinis the command-line binary.shiindis the daemon. - The former working name of the project appears only in ADR-0003 and the landscape. (checked)
Specifications
Specifications are the contracts that code, tests, and conformance suites are checked against.
- Only
kind=specpages use requirement keywords. (checked) Each specification includes this sentence in its conventions section: “The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.” - Every normative requirement starts with a bold, bracketed requirement ID:
**[AR-001]**. IDs are unique across all documentation, are defined only onkind=specpages, and are never renumbered or reused. A withdrawn requirement keeps its ID with the text “Withdrawn.” (checked) - Any mention of a requirement ID elsewhere must refer to a defined ID. (checked)
- Each specification states its version (for example
shiin.action-request/v1-draft.1) near the top and ends with a “Revision history” section. - Every JSON example that claims to be valid is validated against its schema (see Validated examples).
Requirement ID prefixes
| Prefix | Owning specification |
|---|---|
AT | Action types |
AR | Action request |
DEC | Decision |
EVAL | Evaluation |
ID | Identity |
POL | Policy model |
SYN | Policy syntax |
INT | Intent binding |
RISK | Risk signals |
APR | Approval protocol |
AUD | Audit events |
RCPT | Receipts |
ADP | Adapter contract |
API | Local API |
SEC | Security model |
Requirement IDs have three digits after the prefix (EVAL-007).
Threat IDs
Threats are defined in the threat model as **[ST-<category>-<nn>]**,
where the category is a STRIDE letter (S, T, R, I, D, or E) and nn has two digits,
for example **[ST-T-03]**.
Any mention of a threat ID must refer to a defined threat. (checked)
JSON Schemas
- Schemas live in
docs/spec/schemas/and are named<name>.v<major>.schema.json. - Every schema declares
"$schema": "https://json-schema.org/draft/2020-12/schema". (checked) - Every schema has an
$idof the formurn:shiin:schema:<name>:v<major>, for exampleurn:shiin:schema:action-request:v1. URNs keep schema identity independent of any web domain. (checked) - Shared definitions live in
common.v1.schema.jsonand are referenced by$id, for example"$ref": "urn:shiin:schema:common:v1#/$defs/uuid". - Test vectors live in
docs/spec/test-vectors/and are validated againsttest-vector.v1.schema.json. (checked)
Validated examples
To have a JSON example validated, put a marker comment on the line directly before its code fence.
The path is relative to docs/.
<!-- validate: spec/schemas/decision.v1.schema.json -->
```json
{ "...": "..." }
```
Use <!-- validate-fail: <schema path> --> for an example that is deliberately invalid;
the check fails if such an example validates. (checked)
Code blocks and example data
- Always tag fenced code blocks with a language:
toml,json,rust,console(a shell session with$prompts),text,mermaid, orcedar. - Use domains reserved by RFC 2606 (
example.com,example.org,*.example) for hosts. - Use fake secrets that contain the word
EXAMPLE, such assk-EXAMPLE-0000000000000000. Never paste a real-looking credential, even an expired one. - Use
/home/dev/project(Linux),/Users/dev/project(macOS), andC:\Users\dev\project(Windows) as example paths.
Diagrams
- Use Mermaid in fenced
mermaidblocks only. GitHub renders them natively, and the book renders them throughmdbook-mermaid. - Label nodes with glossary terms.
- Do not set custom colors or themes; they break dark mode.
- Put a one-sentence summary of the diagram in the text before it, so the page makes sense without the image.
Links and cross-references
- Link between pages under
docs/with relative paths that include the.mdextension. - Link to files outside
docs/(such asSECURITY.md) with absolute GitHub URLs, because the built book does not contain them. - Refer to decisions as
ADR-NNNNwith a link, for example ADR-0014. EveryADR-NNNNmention must refer to an existing ADR. (checked) - Every statement about a third-party product (a host’s hook format, a competitor’s features) links its source and gives the date it was verified: “(verified 2026-09-14)”.
One home per fact
Each fact below is defined in exactly one place. Other pages link to that place instead of restating the fact, so the fact cannot drift out of sync.
| Fact | Home |
|---|---|
| Term definitions | Glossary |
| Action types | Action types |
| Reason codes | Decision |
| Risk signals | Risk signals |
| Failure semantics | Security model |
| Enforcement level of each adapter | Enforcement levels |
| Host hook formats | the host’s page under guides/integrations/ |
| Latency and memory budgets | Performance budgets |
| Platform tiers | Platform support |
| Crates and their responsibilities | Workspace and crates |
| MSRV and toolchain | Toolchain and MSRV |
| Error codes | Error codes |
| Milestones | Roadmap |
| Decisions | ADR index |
Placeholders
Information only a maintainer can supply, such as a contact address, is written as TODO(maintainer): <what is needed>.
Placeholders are allowed during the specification phase and must be resolved before the first release.
Checklist for documentation changes
- Metadata and banner are correct, and
reviewedis updated. - The page is listed in
SUMMARY.md. - New terms are added to the glossary, and existing terms are used as defined.
- Each fact is stated in its home, and other pages link to it.
- JSON examples carry validation markers.
- Claims about third-party products link a source and a verification date.
-
cargo xtask docs-check,mdbook build docs,lychee,typos, andrumdlpass locally.
Documentation tooling and CI
Note
Project policy: accepted. The tools that build and check the documentation, how to run them locally, and what CI enforces.
The documentation is checked as strictly as code. Every tool used here is written in Rust (ADR-0002), and every check that runs in CI can be run locally with the same command.
Tools
| Tool | Version | Purpose | Configuration |
|---|---|---|---|
cargo xtask docs-check | in repository | Project-specific rules from the style guide | xtask/ |
| mdBook | 0.5.4 | Builds the documentation site from docs/ | docs/book.toml, docs/SUMMARY.md |
| mdbook-mermaid | 0.17.1 | Renders Mermaid diagrams in the built site | [preprocessor.mermaid] in docs/book.toml |
| lychee | 0.24.2 | Checks links and heading anchors | lychee.toml |
| typos | 1.50.1 | Finds spelling mistakes | typos.toml |
| rumdl | 0.2.73 | Lints Markdown structure | .rumdl.toml |
Versions are pinned in .github/workflows/docs.yml.
Upgrading a tool is a pull request that changes the pin and this table together.
Setting up locally
-
Install Rust with rustup. The repository’s
rust-toolchain.tomlselects the pinned toolchain automatically; see Toolchain and MSRV. -
Install the prebuilt tools:
$ cargo install cargo-binstall $ cargo binstall --locked mdbook@0.5.4 mdbook-mermaid@0.17.1 lychee@0.24.2 typos-cli@1.50.1 rumdl@0.2.73
Running the checks
Run these from the repository root. They are the same commands CI runs, in the same order.
$ cargo fmt --all --check
$ cargo clippy --workspace --all-targets -- -D warnings
$ cargo xtask docs-check
$ rumdl check .
$ typos
$ lychee --config lychee.toml './*.md' docs
$ mdbook build docs
The built site is written to target/book/.
To preview it with live reload, run mdbook serve docs.
What cargo xtask docs-check enforces
| Check | Rule |
|---|---|
| Metadata | Line 1 of every page is a valid shiin-doc comment with known values |
| Title and banner | Lines 3 to 6 hold the title and the banner that matches the metadata |
| Table of contents | SUMMARY.md and the pages on disk match in both directions, with no duplicates |
| Requirement IDs | Defined once, only in their owning kind=spec page; every mention refers to a defined ID |
| Threat IDs | Defined once, only in the threat model; every mention refers to a defined threat |
| ADRs | Unique numbers, ADR-NNNN: titles, listed in the index; every ADR-NNNN mention exists |
| Terminology | Requirement keywords only on specifications; deny as the decision value rather than the word it replaces; no exclusionary terms; no mischaracterization of Shiin; the former working name only where allowed |
| JSON Schemas | Draft 2020-12, urn:shiin:schema: identifiers matching file names, valid against the metaschema, and compilable with cross-schema references |
| Examples | JSON blocks after validate markers match their schema; blocks after validate-fail markers do not |
| Test vectors | Every file in spec/test-vectors/ matches the test-vector schema |
The implementation lives in xtask/src/docs_check/, with one module per group of checks.
What the other tools enforce
- rumdl applies its default Markdown rules, with line length disabled because pages use one sentence per line.
- typos checks spelling across the repository, excluding build output and vendored files.
- lychee runs offline on pull requests and checks that every relative link and heading anchor resolves.
- mdBook fails the build if
SUMMARY.mdreferences a missing page or a preprocessor fails.
Vendored files
docs/mermaid.min.js and docs/mermaid-init.js are the browser-side Mermaid renderer used by the built site.
They are generated by mdbook-mermaid install docs and are not edited by hand.
When mdbook-mermaid is upgraded, rerun that command and commit the regenerated files.
Linters exclude these files.
CI
The docs workflow in .github/workflows/docs.yml runs every check above on each pull request and on pushes to main.
- The workflow has read-only
contentspermission and does not persist checkout credentials. - Third-party actions are pinned by commit SHA, with the release tag in a comment.
- A failing check blocks merging once branch protection is configured; see the maintainer handbook.
Planned additions
The weekly external-link workflow, 180-day specification-review warning, and main book-publication workflow are now configured for the first alpha.
cargo xtask link-health checks public links; --report-issue explicitly enables the CI bot’s deduplicated issue report.
Pull-request link checks remain offline.
| Addition | When |
|---|---|
Validating TOML policy examples with shiin policy validate | v0.1 |
Comparing reference/cli.md with the CLI’s generated help | v0.1 |
| Versioned documentation per release | After 1.0 |
Adding a check
- Write the rule in the style guide and mark it (checked).
- Implement it in a module under
xtask/src/docs_check/, reporting problems with file and line. - Fix every existing page so the check passes.
- Update the table in What
cargo xtask docs-checkenforces.
Maintainer handbook
Note
Project policy: draft. This page is the only home of Shiin’s maintainer rules.
Responsibilities
Maintainers:
- Review and merge pull requests.
- Accept or reject RFCs.
- Cut releases.
- Respond to vulnerability reports.
Merging
A pull request merges when:
- CI passes.
- It has a DCO sign-off.
- It has at least one maintainer review.
- A change to an accepted ADR adds a superseding ADR.
Releases
A release is cut from main when the milestone’s exit criteria are met.
Branch protection
A failing check blocks merging once branch protection is configured.
Read next
Glossary
Note
Reference: draft. This is the only place Shiin terms are defined. Every other page links here instead of redefining a term.
Terms are listed alphabetically. Each entry gives a definition, what the term is easily confused with, and where its precise rules live. To propose a new term or change a definition, open a pull request that edits this page and every page that uses the term.
Action
An operation an agent attempts that has an effect outside the agent itself, such as reading a file, running a command, or contacting a host. The action is Shiin’s unit of authorization.
- Not to be confused with: a tool call, which is a host’s name for how an agent invokes a capability. One tool call can contain several actions.
- Rules: Action types.
Action request
The normalized, host-independent description of one proposed action. An adapter builds it and the engine evaluates it.
- Rules: Action request.
Action type
A dotted identifier that classifies an action, such as fs.write or shell.exec.
Policies refer to action types, never to host tool names.
- Rules: Action types.
Adapter
The component that connects a host to Shiin. An adapter intercepts proposed actions, builds action requests, and enforces the resulting decision using the host’s own protocol. Adapters never make decisions.
- Rules: Adapter contract.
Agent
Software that uses a model to propose actions on a person’s behalf, such as Claude Code or Codex CLI. Shiin treats every agent as untrusted.
- Not to be confused with: the host, which is the application the agent runs in. For many products the agent and host ship together.
Agent identity
The identifier of the agent that proposed an action, together with its assurance level.
- Not to be confused with: approver identity, which identifies the human resolving an approval request.
- Rules: Identity.
Allow
The decision value meaning the action may proceed.
Only policy rules and grants can produce allow.
- Rules: Decision, Evaluation.
Approval channel
A route by which approval requests reach approvers, such as the command-line approver or a webhook.
- Rules: Approval protocol.
Approval request
A pending decision waiting for an approver to resolve it.
- Rules: Approval protocol.
Approver
A human authorized to resolve approval requests.
An approver approves or denies; the resulting decision is allow or deny.
Assurance level
How strongly an agent identity is established.
The levels, weakest first, are asserted, attested-local, attested-os-user, and attested-crypto (reserved for the future).
- Rules: Identity.
Audit event
One record in the audit log, such as a received request, a decision, or an approval resolution.
- Rules: Audit events.
Audit log
An append-only JSON Lines file of audit events linked by a hash chain. The audit log is tamper-evident, not tamper-proof.
- Rules: Audit events.
Built-in protected resource
A resource that no policy can make allowable for agents, such as Shiin’s own state, keys, and audit log, host hook configuration, and approval commands.
- Rules: Policy model, ADR-0016.
Capability
A coarse permission for an agent to attempt a whole class of actions at all, for example “may run shell commands”. The capability stage is a gate.
- Not to be confused with: object capabilities (unforgeable tokens), which Shiin does not use.
Checkpoint
A signed statement of the audit log’s head hash at a given position.
- Rules: Audit events, Cryptography.
Classifier
Deterministic code that derives action types and effects from what a host reports. Classifiers only ever make decisions stricter: anything they cannot classify becomes an opaque effect.
- Rules: Action request, ADR-0018.
Constraint
A machine-checkable limit inside a task declaration.
The closed set of constraint kinds is scope.paths, forbid.action_types, allow.hosts, limits, expires_at, and require_pending_for.
- Rules: Intent binding.
Decision
The engine’s answer to an action request:
allow, deny, or pending, with a reason code.
- Rules: Decision.
Default decision
The decision made when no rule matches.
It is deny, can be configured to pending, and can never be allow.
- Rules: Evaluation.
Deny
The decision value meaning the action must not proceed.
A deny from any stage wins over every other result, including grants.
Deny-with-ticket
How pending is expressed to a host that cannot wait for an approver:
the adapter denies the action now and returns an approval ticket.
If the agent retries after an approver approves, the retry consumes a single-use grant.
- Rules: Approval protocol, ADR-0028.
Developer
The person using an agent to do work. Often the same person as the operator and the approver, but the roles are distinct.
Effect
A concrete consequence of an action, such as a path written or a host contacted,
recorded with a confidence of exact, inferred, or opaque.
- Rules: Action request.
Enforcement level
How strongly a Shiin decision is enforced for a given adapter. At L1 Cooperative, the host honors the decision through its hook mechanism, which can be disabled. At L2 Mediated, Shiin sits in the data path, as the MCP proxy does. At L3 OS-enforced (future), the operating system or a sandbox enforces the decision.
- Rules: Enforcement levels, ADR-0033.
Engine
The pure, synchronous component that evaluates an action request against a policy snapshot and returns a decision. It performs no I/O.
- Rules: Evaluation, ADR-0010.
Fail-closed
The rule that any failure (an error, a timeout, an unreadable policy) results in deny.
- Rules: Security model, ADR-0013.
Gate
An evaluation stage that can return deny, pending, or pass, but never allow.
The identity, capability, intent, and risk stages are gates.
- Rules: Evaluation.
Grant
A stored authorization created when an approver resolves an approval request.
- A single-use grant is bound to the digest of one action request.
- A scoped grant covers an agent, action types, a resource pattern, and a workspace, and expires.
A grant can satisfy only a grantable pending result. It never overrides deny.
- Rules: Approval protocol, ADR-0027.
Hash chain
The construction in which each audit event includes the hash of the event before it, so removing or altering an event is detectable.
Hook
A host mechanism that runs an external program before or after the agent uses a tool, and that can accept a decision back. Hooks are the interception point for L1 adapters.
Host
The application that runs an agent and exposes an interception mechanism, such as Claude Code, Codex CLI, Cursor, or an MCP client.
Intent binding
Checking an action against the constraints of the active task declaration. Natural-language descriptions of intent are recorded but never evaluated.
- Rules: Intent binding, ADR-0020.
Opaque effect
An effect Shiin cannot determine before the action runs,
such as what python cleanup.py will do.
Opaque effects lead to pending by default.
Operator
The person who installs and configures Shiin and writes its policy.
Outcome verification
A reserved concept: checking after an action runs that its real effects matched what was authorized. It is not yet specified.
- Not to be confused with: receipt verification.
Pending
The decision value meaning the action must wait for an approver.
A pending decision that is never resolved expires as deny; it never becomes allow on its own.
Policy
The complete set of rules, capabilities, defaults, and risk configuration that applies to an evaluation, drawn from every policy layer.
- Rules: Policy model.
Policy layer
One source of policy. Layers, in order, are built-in, user, and workspace. An organization layer is reserved for the future. A workspace layer can only restrict unless the operator trusts it by hash.
- Rules: Policy model, ADR-0015.
Policy snapshot
The immutable input to one evaluation: compiled policy, grants, active tasks, and counters, identified by a snapshot hash.
- Rules: Evaluation.
Reason code
A stable, machine-readable code that explains a decision, such as deny.policy.
- Rules: Decision.
Receipt
A signed statement that attests a decision was made, using a DSSE envelope and an Ed25519 signature.
Receipt verification
Checking that a receipt’s signature and contents are valid. It proves who signed a statement about a decision, not that the action’s real outcome was correct.
Replay
Re-evaluating recorded action requests against their recorded policy snapshots, or against a different policy to answer “what if”.
Requirement ID
A stable identifier, such as EVAL-007, for one normative requirement in a specification.
Resolution
An approver’s answer to an approval request: allow_once, allow_always, or deny.
Risk signal
A discrete, deterministic property of an action request, such as bulk_delete or secret_path,
that can escalate a decision. Shiin does not use numeric risk scores.
- Rules: Risk signals.
Rule
A policy element that matches action requests and produces allow, deny, or pending.
Session
One run or conversation of a host, identified by the host’s session ID.
Shiin
The project. shiin is the command-line binary and shiind is the daemon.
Stage
One step of evaluation. The stages, in order, are identity, capability, policy, intent, and risk. Later stages can only restrict the result of earlier ones.
- Rules: Evaluation, ADR-0012.
Task declaration
A structured, human-confirmed statement of what an agent is working on: a description, constraints, and an expiry. Also called a task.
- Rules: Intent binding.
Trace
The record of each stage’s result for one decision, produced in explain mode and used by replay.
Workspace
The project directory an agent works in, and the root of the workspace policy layer.
Avoided terms
| Avoid | Use instead | Why |
|---|---|---|
| block, blocked | deny, denied | One word for one decision value |
| ask (for Shiin behavior) | pending | “Ask” is reserved for hosts’ native mechanisms |
| tool call (for Shiin behavior) | action | A tool call can contain several actions |
| permission (for Shiin concepts) | capability, rule | “Permission” is ambiguous with operating-system permissions |
| firewall, guardrail, sandbox (describing Shiin) | authorization layer | Shiin is none of these; see Vision |
| whitelist, blacklist | allowlist, denylist | Clearer and inclusive |
| user (in specifications) | developer, operator, approver | Specifications need the exact role |
| intent (meaning constraints) | task declaration, constraint | Natural-language intent is never enforced |
| verification (alone) | receipt verification, outcome verification | They prove different things |