Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

StateWhat it covers
Exists todayThe 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
SpecifiedThe action request, decision, evaluation, policy model, intent binding, approval protocol, audit events, receipts, adapter contract, and local API
OpenIndependent 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 areRead, in order
New to ShiinVision, How Shiin works, Use cases, Limitations
Evaluating Shiin’s securityThreat model, Security model, Enforcement levels, Limitations
Comparing Shiin with other toolsLandscape, FAQ
Building an adapter or integrationAdapter contract, Action request, Decision, Local API
Planning to write policyPolicies, Policy model, Writing policies
Contributing codeWorkspace and crates, Coding standards, Testing strategy, CONTRIBUTING.md
Proposing a changeADR 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:

  1. Mistakes. A model misreads a request and deletes the wrong directory, drops the wrong database, or force-pushes over a colleague’s work.
  2. 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.
  3. 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.

  1. 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.
  2. 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).
  3. 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.
  4. Deny wins, and the default is deny. A deny from any stage overrides everything else. When nothing matches, the answer is deny. Rules out: “allowed because no rule matched” (ADR-0011).
  5. Fail closed. Errors, timeouts, and unreadable policy produce deny. Rules out: silently letting actions through when Shiin itself has a problem (ADR-0013).
  6. 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).
  7. 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.
  8. Evidence by default. Every decision produces an audit event, and the audit log is tamper-evident. Rules out: decisions that leave no record.
  9. 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).
  10. 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).
  11. 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:

  1. Task-intent binding. Actions are checked against the constraints of the task the developer confirmed, not only against standing policy.
  2. 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.
  3. Analyzable policy. Policies compile to Cedar, a formally verified authorization language, so they can be tested, explained, and reasoned about (ADR-0014).
  4. Explicit enforcement levels. Every integration is labeled cooperative, mediated, or OS-enforced, so its protection is never overstated.

Non-goals

Shiin is notUse instead
A sandbox or isolation boundaryAn OS sandbox, container, or virtual machine, or the host’s own sandbox, alongside Shiin
A prompt-injection detectorA dedicated scanner. Shiin limits what a manipulated agent can do; it does not detect the manipulation
An observability or tracing dashboardTracing tools. Shiin’s audit log can be exported to them
A blocklist of dangerous commandsShiin 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 contentContent-safety tooling for the model’s inputs and outputs
A replacement for operating-system permissionsRun agents as a less-privileged user where possible; see Hardening
Data loss prevention for every channelAn 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.

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

PersonaSituationWhat they need from Shiin
Solo developerUses two or three agents on a laptop, often with broad permissions to keep work flowingProtection from costly mistakes without constant prompts
Security-minded leadResponsible for a team adopting agents, needs to explain the risk to othersClear, reviewable policy and evidence of what agents did
Platform engineerRuns agents in CI or other unattended environmentsDeterministic behavior with no human in the loop
Agent builderMaintains an agent, host, or MCP serverA 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

GoalBetter fit
Run untrusted code safelyAn OS sandbox, container, or virtual machine
Stop data leaving through channels that are allowedEgress controls or a DLP proxy
Detect prompt injection in contentA prompt-injection scanner
Prove compliance with a regulationShiin can supply evidence, but it makes no compliance claims

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, and pending, 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:

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.

CategoryWhere it actsQuestion it answers
Host-native controlsInside the host, before a tool runsWhat does this host let the agent do?
OS sandboxes and isolationAround a running processWhat can this process reach, whatever it does?
Network egress controlsOn outbound trafficWhere may traffic go?
MCP and agent gatewaysBetween an agent and the tool servers or other agents it callsWhich tools and agents may be called?
Policy engines as building blocksInside other toolsHow are rules written and evaluated?
Open-source agent action gatesWhere an agent proposes an actionShould 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 PreToolUse and PermissionRequest hooks that can return allow, deny, or ask decisions (hooks, permissions, verified 2026-09-14). Hooks can be disabled with --bare or the disableAllHooks setting, 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 PreToolUse hooks 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, and preToolUse, with allow, deny, or ask responses and a failClosed option (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.

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-policy Rust 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.

DimensionQuestion to ask of any toolShiin’s design
Agent-agnosticDoes 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 levelWhere 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 determinismDo 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 modelHow 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)
EvidenceWhat 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 portabilityCan policy be reviewed, tested, and reused outside one host?TOML compiled to Cedar (ADR-0014, accepted); direct Cedar input is planned
Task-intent bindingCan 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-firstDoes 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

ControlWhat 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 processesHooks 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 toolsHooks 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 optionnot 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 toolDoes 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 BoundariesNetwork egress only

Open-source projects

ProjectWhat it does (per its documentation)Interception pointDecision outcomes (project’s own labels)EvidencePolicy languageLanguage and license
SECBLOK/belay (verified 2026-09-14)Local-first security layer at the tool-call boundary for Claude Code, Codex, Cursor, and MCPNative hooks or an MCP proxy“deny”, “ask”, “allow”Tamper-evident audit trailnot documentedRust; AGPL-3.0 with a commercial option
eqtylab/cupcake (verified 2026-09-14)Policies for Claude Code, Cursor, and other agentsHost hooksnot documentednot documentedOPA/Rego compiled to WebAssemblyRust; Apache-2.0
dgenio/agentfence (verified 2026-09-14)Deny-by-default action decisionsnot documented“allow”, “deny”, “ask”Ed25519-signed, hash-chained logsnot documentedGo; Apache-2.0
Adirdabush1/cerberus (verified 2026-09-14)Risk-scores actions from Claude Code, Codex, Cursor, and ClineLocal gateway“ALLOW”, “AUDIT”, “HITL”, “BLOCK”not documentednot documentedTypeScript; 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 scoringnot documentednot documentednot documentednot documentedKotlin; Apache-2.0
ToolJet/ActionRail (verified 2026-09-14)Checks consequential tool-call arguments against live systems of record; public betanot documented“allow”, “hold”, “block”not documentednot documentedPython; Apache-2.0
latchagent/latch (verified 2026-09-14)“Control layer for autonomous AI agents” in which risky actions wait for approvalnot documentednot documentednot documentednot documentedTypeScript; MIT
microsoft/agent-governance-toolkit (verified 2026-09-14)Policy enforcement, zero-trust identity, sandboxing, and audit for AI agentsnot documentednot documentedAuditnot documentedPython; MIT
luckyPipewrench/pipelock (verified 2026-09-14)Agent firewall with DLP, SSRF, prompt-injection, and tool-poisoning detectionHTTP, MCP, A2A, and WebSocket trafficnot documentedSigned action receiptsnot documentedGo; Apache-2.0 core, Elastic-2.0 enterprise features
agentgateway/agentgateway (verified 2026-09-14)Linux Foundation proxy for MCP and A2AMCP and A2A proxynot documentednot documentedRBAC rules in CELRust; Apache-2.0
invariantlabs-ai/invariant (verified 2026-09-14)Rule-based guardrails for LLM and MCP applicationsnot documentednot documentednot documentednot documentedPython; 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.

  1. 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.
  2. 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.
  3. 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).
  4. 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 needConsiderRelationship to Shiin
A tool you can use todayOne of the open-source agent action gatesShiin also offers experimental CLI and Unix-daemon archives; see its limitations before use
Controls for one host, where its built-in settings meet your needsHost-native controlsInstead or alongside: keep them enabled even if you adopt Shiin
To limit what an allowed command, or the code it runs, can reachAn OS sandbox, container, or virtual machineAlongside: Shiin does not isolate processes
To restrict where an agent’s traffic can go, or to inspect that trafficNetwork egress controlsAlongside: Shiin does not inspect traffic and cannot see data leaving through an allowed action
Access control for MCP or A2A traffic shared across many agentsMCP and agent gatewaysInstead 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 previewsmikepenz/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 recordToolJet/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 Regoeqtylab/cupcake, or microsoft/regorus to build your own (verified 2026-09-14)Instead: Shiin proposes Cedar
A broad agent-governance toolkit in Pythonmicrosoft/agent-governance-toolkit (verified 2026-09-14)Instead or alongside
An authorization engine inside your own application, without agent-specific featuresCedar 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 reviewed date 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.

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:

  1. Task-intent binding. Actions are checked against the constraints of the task the developer confirmed.
  2. An open, agent-neutral contract. The action request, decision, and approval protocol are versioned specifications with schemas and test vectors.
  3. Analyzable policy. Policies compile to Cedar, a formally verified authorization language.
  4. 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.

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

  1. 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.
  2. An adapter intercepts it. Before the host executes the action, the host hands it to a Shiin adapter, for example through a hook.
  3. 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.
  4. The engine evaluates it. The engine takes the action request and a policy snapshot and runs five stages in a fixed order.
  5. A decision comes back. The decision is allow, deny, or pending, with a reason code and a message for the agent.
  6. 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.
  7. 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.

StageQuestion it answersCan return
IdentityDo we know which agent is asking, and how sure are we?deny, pending, or pass
CapabilityMay this agent attempt this class of action at all?deny, pending, or pass
PolicyWhat do the rules say about this action on these resources?allow, deny, or pending
IntentDoes the action fit the constraints of the active task?deny, pending, or pass
RiskDoes 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 is deny.
  • Otherwise, if any stage returns pending, the decision is pending, 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.write and one effect: an exact write to /home/dev/project/src/login.rs.
  • Identity and capability pass. The edit-source rule 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.read on /home/dev/project/.env.
  • The no-secrets rule matches and returns deny. The edit-source rule does not matter, because deny wins.
  • The decision is deny with reason code deny.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_command risk signal.
  • The decision is pending with reason code pending.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-source rule 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.paths constraint. The path is outside it.
  • The decision is deny with reason code deny.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.

PartMilestone
Claude Code adapter, engine, policy, audit logv0.1 (source; host validation open)
Task declarations and intent bindingv0.2 (experimental source; measurements open)
Approval protocol and grantsv0.3 (Unix source; platform validation open)
Receipts and replayv0.4 (source; external review open)
MCP proxy, Rust client, and SQL classifiersv0.5 (early source; conformance/integration validation open)

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:

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.

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

ValueMeaning
allowThe action may proceed.
denyThe action must not proceed.
pendingThe 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.

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.

LevelMeaning
assertedThe host reports the agent id. Nothing verifies it.
attested-localThe agent id is attested by a local trust root.
attested-os-userThe agent id is attested by the operating system user.
attested-cryptoThe 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.

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.

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.

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:

ConstraintMeaning
scope.pathsPaths the agent may touch.
forbid.action_typesAction types the agent may not perform.
allow.hostsHosts the agent may contact.
limitsCounters for actions such as deletions.
expires_atWhen the task ends.
require_pending_forAction types that always need approval.

Expiry

When a task expires, its constraints and any grants tied to it end.

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

SignalTriggered by
secret_pathAn effect touches a path known to hold secrets.
outside_workspaceAn effect touches a path outside the workspace.
bulk_deleteA single action would delete more files than a threshold.
opaque_commandAn action carries an opaque effect.
destructive_vcsA 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.

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.

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:

LevelMeaning
L1 CooperativeThe host honors the decision through its hook mechanism.
L2 MediatedShiin sits in the data path.
L3 OS-enforcedThe operating system or a sandbox enforces the decision.

Hosts

HostLevelInterceptionMilestone
Claude CodeL1hooksv0.1
Codex CLIL1hooksv0.2
CursorL1hooksv0.2
MCP proxyL2data pathv0.5

Honest labeling

Every adapter states its enforcement level and what it cannot see.

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.

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.

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.

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.

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-secrets wins over read-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_command turns a command whose effects cannot be read, such as python 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

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.read or shell.exec. A trailing * is a wildcard, so fs.* 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* covers cargo 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

  1. A matching deny wins over everything, so rule order in the file never matters.
  2. Otherwise a matching pending holds the action for a person.
  3. Otherwise a matching allow permits it.
  4. 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:

SignalFires when
opaque_commandAn action carries an effect Shiin cannot read, such as an interpreter or a shell substitution
bulk_deleteA single action would delete more files than the threshold, which defaults to 20
destructive_vcsA command rewrites or discards version control history, such as a force push
privilege_escalationA command asks for higher privileges, such as sudo
secret_pathAn effect touches a path known to hold secrets
outside_workspaceA 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

FeatureArrives
Separately trusted user/workspace layers and attested identitystill open
Writing rules directly in Cedarafter the policy language is stable

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.

  1. Enable Shiin’s adapter for your host.
  2. Enable the host’s sandbox for Bash commands.
  3. Keep host permission rules enabled alongside Shiin.

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.

OptionEffect
--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).

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 expect pending.opaque rather than a denial.
  • Each risk signal you configured, with a case just over the threshold.
  • Your task constraints, by including a task and, for a limit, a usage showing what has been spent already.
  • Anything you have ever had to fix. A case is cheaper than remembering.

Fields a case may carry

FieldPurpose
action_requestThe action to decide. Required
expectedThe decision and reason code. Required
policyA policy for this case alone. Omit to use the workspace’s
taskAn active task declaration
usageWhat the task has already spent, for limit cases
modeunattended to check what happens with no approver
nowThe evaluation time, for expiry cases

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.

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.

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

HostEnforcement levelStatus
Claude CodeL1 cooperativeImplemented in source
Codex CLIL1 cooperativeImplemented; live-host validation open
CursorL1 cooperativeImplemented; live-host validation open
MCP proxyL2 mediatedPartial; one downstream stdio or Streamable HTTP server and tool calls
Custom agentsL1/L2 via APIPartial; Unix API and Rust client exist
Aider in a sandboxL1 cooperativePlanned 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.

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

  1. Install Shiin.
  2. Run shiin init.
  3. Configure the host using the quickstart. shiin hook claude-code reads one JSON payload from standard input; it is not an interactive setup command.

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 toolAction typeNotes
Bash, shell, local_shellshell.execClassified like any command, so cat .env records the read it performs
apply_patchfs.writeThe path is used when the payload names one
Readfs.read
Anything elsex-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-trust exists.
  • 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.

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 eventAction typeNotes
beforeShellExecutionshell.execClassified like any command, so a read it performs is recorded
beforeReadFilefs.read
beforeMCPExecutionx-cursor.mcp.<tool>Opaque: a tool server describes its own behavior, and Shiin cannot verify it
Anything elsex-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 deny for both engine deny and pending decisions. 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.

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.

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.

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.

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

CommandWhat it doesStatus
shiin initWrites a starter policy to .shiin/policy.toml, refusing to overwrite oneworks today
shiin policy validate [path]Parses and compiles a policy, reporting rule and signal counts or a diagnosticworks today
shiin check <request.json>Decides one action request and prints the decision and reason codeworks today
shiin explain <request.json>Decides, and also prints the stage that decided and the rules that matchedworks today
shiin hook claude-codeServes one Claude Code PreToolUse hook call, reading the payload on standard inputworks today
shiin log [count]Prints recent audit events, most recent lastworks today
shiin verifyChecks the audit log’s hash chainworks today
shiin verify <evidence.json> <public.json>Verifies a signed receipt or checkpoint; checkpoints also require matching local audit historyimplemented in source; platform validation incomplete
shiin replay <encrypted-replay.json> [policy.toml]Replays captured inputs with the original or alternate policyimplemented in source; requires the originating OS keyring
shiin key init, shiin key rotate, shiin key publicInitializes or rotates the evidence key and prints its public keyimplemented in source on supported keyring platforms
shiin doctorReports this workspace’s policy, audit log, mode, and enforcement levelworks today
shiin policy test [dir]Runs policy cases from .shiin/tests, using the same runner as the conformance corpusworks today
shiin task start | show | endDeclares what the agent is working on, shows it with its usage, or ends itworks today
shiin hook codex, shiin hook cursorThe same hook service for those hostsworks today, untested against a running host
shiin exec --session ID -- /ABSOLUTE/PROGRAM [ARG ...]Authorizes and audits one command, then starts it only after allowsource implementation on Unix; host routing remains unvalidated
shiin pending, shiin approveLists and resolves approval requests using a separate operator credential and terminal confirmationimplemented in source on Unix; validation in progress
shiin grants, shiin revokeInspects and revokes grants or unresolved ticketsimplemented in source on Unix; validation in progress
shiind --operatorRuns the local approval daemon and displays its in-memory operator credential in a dedicated terminalimplemented 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.

CodeMeaning
0The action is allowed
1The action is denied, or the command failed
2The 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.

Configuration

Note

Reference: draft. This describes what a source build reads today, and marks what is planned.

Files

FilePurposeStatus
.shiin/policy.tomlThe policy for this workspaceworks today
.shiin/audit.jsonlThe append-only audit logworks today
.shiin/tests/Policy cases run by shiin policy testworks today
.shiin/config.tomlSettings that are not policy, such as the audit log’s locationplanned, v0.3
.shiin/tasks/The active task declaration and its countersworks 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

VariableDescriptionStatus
SHIIN_WORKSPACEOverrides the workspace root. Useful when a host reports no working directoryworks today
SHIIN_UNATTENDEDIf set to any value, a pending decision becomes a denial, because no approver can be reachedworks today
CITreated the same as SHIIN_UNATTENDED, since continuous integration has no approverworks today
SHIIN_DAEMONSet to 1 to require local daemon evaluation; an unavailable configured service denies the actionsource implementation on Unix
SHIIN_SOCKETAbsolute Unix socket override; setting it requires the daemon even without SHIIN_DAEMON=1source implementation on Unix
SHIIN_EVIDENCESet to 1 when starting the daemon to enable signed receipts/checkpoints and encrypted replay records; unavailable keys prevent startupsource implementation; OS key-store validation remains open
SHIIN_LOGLog level filterplanned, 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.

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

CodeMeaningStatus
SHIIN-POL-0001The policy file could not be read.emitted
SHIIN-POL-0002The policy file is not valid TOML.emitted
SHIIN-POL-0003The policy uses an unknown key.emitted
SHIIN-POL-0004A rule has a duplicate id.emitted
SHIIN-POL-0005A rule has an invalid glob pattern.emitted
SHIIN-POL-0006A 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

CodeMeaningStatus
SHIIN-CLS-0001A 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

CodeMeaningStatus
SHIIN-AUD-0001The audit log could not be written.emitted
SHIIN-AUD-0002The 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

CodeMeaningStatus
SHIIN-GEN-0001An internal error occurred, including a request that could not be evaluated.emitted
SHIIN-GEN-0002The 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.

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

TierPlatformsSupport
Tier 1Linux x86_64, macOS 13+ (x86_64 and arm64)Tested in CI.
Tier 2Windows 10+ (x86_64)Builds; promoted toward Tier 1 in v0.6.
Tier 3Other architecturesNot 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.

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

StatusMeaning
draftThe specification is under active revision. Field names, IDs, and schemas may change.
proposedThe specification has stabilized enough to send to implementers for review.
acceptedThe 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.
supersededA 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 $id URN (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 $id in 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=spec page.
  • The prefix indicates which specification owns the requirement:

Schemas and test vectors

  • JSON Schemas live in schemas/ and are named <name>.v<major>.schema.json.
  • Shared definitions live in common.v1.schema.json and are referenced by $id.
  • Test vectors live in test-vectors/ and are validated against test-vector.v1.schema.json.

Specification list

Reading the specifications

  1. Start with How Shiin works for the end-to-end flow.
  2. Read the action request and decision for the core messages.
  3. Read evaluation for how decisions are combined.
  4. Read the adapter contract for integration.
  5. Consult the test vectors for concrete examples.

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 typeMeaning
fs.readRead a file or directory entry.
fs.listList directory entries or search for matching paths.
fs.writeWrite or overwrite a file.
fs.createCreate a new file, directory, or symlink.
fs.deleteDelete a file, directory, or symlink.
fs.moveMove or rename a file or directory.
fs.chmodChange 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 typeMeaning
shell.execExecute 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 typeMeaning
net.connectOpen a connection to a remote host.
net.fetchFetch 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 typeMeaning
db.queryRun a read query against a database.
db.mutateRun a write query against a database.
db.schema_changeChange 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:

FieldTypeRequiredDescription
schemastringyesThe schema identifier, shiin.action-request/v1-draft.1.
idstring (UUID)yesA unique identifier for this request.
sessionstringyesThe host’s session identifier.
agentobjectyesThe agent identity.
action_typestringyesAn action type, such as fs.write.
effectsarray of effectyesThe effects of the action.
taskstring or nullnoThe active task declaration id, or null.
policy_snapshotstringyesThe digest of the policy snapshot used.
hostobjectyesHost-specific context.

[AR-002] id MUST be a version 4 UUID.

Agent identity

[AR-003] The agent object MUST contain:

FieldTypeRequiredDescription
idstringyesThe agent identifier.
assurancestringyesThe assurance level: asserted, attested-local, attested-os-user, or attested-crypto.

Effects

[AR-004] Each element of effects MUST be a JSON object with:

FieldTypeRequiredDescription
kindstringyesA free-form label for the effect, such as path or host.
valuestringyesThe concrete value, such as a canonical path or host name.
confidencestringyesexact, 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:

FieldTypeRequiredDescription
namestringyesThe host name, such as claude-code.
toolstringnoThe host’s tool name, if applicable.
payloadobject or nullnoThe 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 workspace object, 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:

CodeDecisionMeaning
allow.policyallowA policy rule allowed the action.
allow.grantallowA grant covered the action.
deny.identitydenyThe agent identity is not acceptable.
deny.capabilitydenyThe agent lacks a capability for this action class.
deny.policydenyA policy rule denied the action.
deny.intentdenyThe action does not fit the active task.
deny.riskdenyA risk signal denied the action.
deny.failuredenyA failure produced a fail-closed deny.
deny.builtindenyA built-in protected resource was touched.
deny.unattendeddenyA pending result could not be resolved because no approver is available.
deny.defaultdenyNo rule matched, so the default decision applied.
pending.identitypendingThe agent identity needs confirmation.
pending.capabilitypendingThe agent capability needs confirmation.
pending.intentpendingThe action needs confirmation against the task.
pending.riskpendingA risk signal escalated the action.
pending.opaquependingAn opaque effect needs approval.
pending.grantablependingThe 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:

FieldTypeRequiredDescription
schemastringyesshiin.decision/v1-draft.1.
request_idstring (UUID)yesThe id of the action request.
decisionstringyesallow, deny, or pending.
reasonstringyesThe reason code.
messagestringyesThe agent-facing message.
tracearraynoThe trace of stage results, in explain mode.
approvalobject or nullnoAn 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:

LevelMeaning
assertedThe host reports the agent id. Nothing verifies it.
attested-localThe agent id is attested by a local trust root.
attested-os-userThe agent id is attested by the operating system user.
attested-cryptoThe 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:

ConstraintMeaning
scope.pathsPaths the agent may touch.
forbid.action_typesAction types the agent may not perform.
allow.hostsHosts the agent may contact.
limitsCounters for actions such as deletions.
expires_atWhen the task ends.
require_pending_forAction 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, and require_pending_for were 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:

KindMeaning
request.receivedAn action request was received.
decision.madeA decision was produced.
approval.requestedAn approval request was created.
approval.resolvedAn approval was resolved.
grant.usedA grant was consumed.
grant.createdA grant was created.
grant.revokedA grant was revoked.

Required fields

[AUD-005] Every event MUST carry:

FieldTypeDescription
schemastringshiin.audit-event/v1-draft.2.
idstring (UUID)The event id.
timestampstring (ISO 8601)When the event occurred.
kindstringThe event kind.
prevstringThe digest of the previous event; the first uses sha256: followed by 64 zeroes.
selfstringThe digest of this event.
seqintegerPosition in the log, beginning at 1.
payloadobjectAn 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

HostEnforcement levelInterceptionMilestone
Claude CodeL1hooksv0.1
Codex CLIL1hooksv0.2
CursorL1hooksv0.2
MCP proxyL2data pathv0.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-client exposes 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:

MethodDescription
shiin.evaluateEvaluate an action request and return a decision.
shiin.evaluate.unattendedEvaluate without an available approver; pending becomes deny.
shiin.policy.validateValidate a policy and return diagnostics.
shiin.approve.resolveResolve an approval request.
shiin.pendingList pending requests; requires the operator credential.
shiin.grantsList grants; requires the operator credential.
shiin.revokeRevoke 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 source field 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 test execute 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:

FieldMeaning
taskThe active task declaration, for vectors that exercise intent binding.
modeattended or unattended. Absent means attended.
usageWhat the task has used before this action, for vectors that exercise limits.
nowThe evaluation time, for vectors that exercise a task expiry.

Golden use cases

IDUse caseVector file
TV-001An allowed edittv-001-allowed-edit.v1.json
TV-002A denied read of a secrettv-002-denied-secret.v1.json
TV-003A command with unknown effectstv-003-opaque-command.v1.json
TV-004An action outside the tasktv-004-outside-task.v1.json
TV-005A bulk deletiontv-005-bulk-delete.v1.json
TV-006A destructive VCS operationtv-006-destructive-vcs.v1.json
TV-007An unattended denytv-007-unattended-deny.v1.json
TV-008No rule matches, so the default decision appliestv-008-default-deny.v1.json
TV-009A secret read through a shell command, denied by the same rule as a direct readtv-009-secret-via-shell.v1.json
TV-010A task’s write limit is reachedtv-010-task-limit.v1.json
TV-011An expired task no longer constrainstv-011-task-expired.v1.json
TV-012A task asks a person about an action typetv-012-task-asks.v1.json
TV-013A task limits which hosts may be reachedtv-013-task-host.v1.json
TV-014Task scope denial wins over required approvaltv-014-task-deny-before-pending.v1.json
TV-015A task forbids a read performed through a shell commandtv-015-task-forbidden-effect.v1.json
TV-016Workspace-relative task scope excludes another workspacetv-016-task-scope-root.v1.json
TV-017A spent task budget wins over required approvaltv-017-task-budget-before-pending.v1.json
TV-018Outside-workspace risk denies an otherwise allowed writetv-018-outside-workspace-risk.v1.json
TV-019Secret-path risk denies an otherwise allowed readtv-019-secret-path-risk.v1.json
TV-020Missing capability denies an otherwise allowed actiontv-020-missing-capability.v1.json
TV-021Capability denial wins over a broad capability granttv-021-capability-deny-wins.v1.json
TV-022Every effect class requires an explicit capabilitytv-022-capability-effect.v1.json
TV-023A capability does not replace policy authorizationtv-023-capability-not-authority.v1.json
TV-024Session and task rule selectors match togethertv-024-rule-session-task.v1.json
TV-025A different session cannot match a scoped ruletv-025-rule-session-mismatch.v1.json
TV-026An absent task cannot match a task-scoped ruletv-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 usage and now fields they need.
  • v1-draft.3 (2026-10-12): Added TV-009, which pins UC-02: a rule written for fs.read must 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 task and mode fields, 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

CategoryMeaning
SSpoofing identity
TTampering with data or logs
RRepudiation (denying an action)
IInformation disclosure
DDenial of service
EElevation 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.

ThreatChange or evidenceRemaining review
ST-I-01Hook 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-02Audit 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-01Audit 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-01Existing 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-01Hook identities remain asserted and the identity stage passes through.Review the lack of assurance enforcement before claiming identity protection.
ST-T-02Built-in protections cannot be granted by operator rules.Review single-file policy loading against the designed trust layers.
ST-E-02Cursor 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

AdapterLevelCannot see
Claude CodeL1Actions outside hooks; code run by an allowed command.
Codex CLIL1Actions outside hooks; code run by an allowed command.
CursorL1Actions outside hooks; code run by an allowed command.
MCP proxyL2Traffic not routed through the proxy.
Shell wrapperL1Commands 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

UseAlgorithm
Receipt signaturesEd25519
Audit hash chainSHA-256
Receipt envelopeDSSE

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.

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.

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.

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 doctor and 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.

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.

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

  1. Intercept. An adapter receives the proposed action from the host.
  2. Normalize. The adapter builds an action request.
  3. Evaluate. The engine runs five stages in order: identity, capability, policy, intent, and risk.
  4. Decide. The engine produces a decision: allow, deny, or pending.
  5. Record. Request and decision audit events are synced; a write failure denies the action.
  6. 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

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.

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

StateLocationMilestone
Policy.shiin/policy.toml in the workspacev0.1
Audit log.shiin/audit.jsonl in the workspacev0.1
Approval requests and grants~/.shiin/runtime/approvals.json, beside the Unix daemon socketv0.3, source implementation
Signing keysOS keyring, following ADR-0032v0.4, partial source implementation
Tasks.shiin/tasks/ in the workspacev0.2
Receipts and encrypted replay.shiin/evidence/<request-id>.receipt.json and <request-id>.replay.jsonv0.4, when daemon evidence is enabled
Signed checkpoint and public keys.shiin/evidence/checkpoint.json and <key-id>.public.jsonv0.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).

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

PathStatePurpose
Cargo.tomlexistsVirtual workspace manifest with shared package metadata, [workspace.dependencies], and [workspace.lints]
rust-toolchain.tomlexistsPinned toolchain; see Toolchain and MSRV
rustfmt.toml, .editorconfigexistFormatting rules; see Coding standards
.cargo/config.tomlexistsDefines the cargo xtask alias
xtask/existsRepository tooling; today it provides cargo xtask docs-check
crates/existsProduct crates, each created in the milestone that needs it
fuzz/existsWire-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.toml is 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 the cargo xtask convention for repository automation written in Rust. It sets publish = 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, and authors from [workspace.package].
  • Every member sets [lints] workspace = true, except shiin-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.

CrateResponsibilityMilestone
shiin-schemaWire types for action requests, decisions, approval messages, and audit events, matching the JSON Schemas in docs/spec/schemas/v0.1
shiin-classifyNormalizers for paths, shell commands, URLs, and experimental SQL, and the derivation of effects from themv0.1 (SQL: v0.5; host integration open)
shiin-policyThe TOML front-end and its compilation to Cedar, policy layering, and diagnosticsv0.1
shiin-engineThe pure, synchronous evaluation pipelinev0.1
shiin-auditThe hash-chained audit log, verification, replay, and receipts behind a featurev0.1 (receipts: v0.4; external review open)
shiin-hooksHost hook adapters: a claude_code module, then codex and cursor modulesv0.1 (Codex and Cursor: v0.2)
shiin-cliThe shiin binary: init, check, explain, policy validate, policy test, hook, task, pending, approve, log, verify, replay, and doctorv0.1
shiin-approvalThe approval state machine and grantsv0.3 (source; review open)
shiin-ipcLocal transport over Unix domain sockets and Windows named pipesv0.3 (Unix source; Windows validation open)
shiindThe per-user daemonv0.3 (Unix source; platform validation open)
shiin-clientThe Rust client for daemon evaluation and policy validationv0.5 (early source; conformance open)
shiin-mcp-proxyThe MCP proxy with upstream stdio and downstream stdio or Streamable HTTPv0.5 (HTTP: v0.6; external conformance open)
shiin-platformThe only crate allowed to contain unsafe code; created only if neededas needed
xtaskRepository toolingexists
fuzz/Fuzz targets; a separate workspace, not a member cratev0.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-schema and shiin-client are 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.
  • xtask is never published.

Dependencies are declared once, with default features off

  • Every third-party 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.
  • 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-engine performs no I/O of any kind (ADR-0010).
  • shiin-classify and shiin-policy may read the local file system where their job requires it, such as resolving a path or loading a policy file.
  • cargo xtask dependency-check inspects 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_methods and disallowed_types Clippy 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:

  1. A crate never depends on a crate in a group above it, and libraries never depend on binaries.
  2. The engine crates depend only on each other and on third-party crates that satisfy the no-networking rule.
  3. shiin-engine does not depend on shiin-classify. Adapters classify what a host reports into an action request; the engine only evaluates action requests.
  4. shiin-client depends only on shiin-schema, shiin-ipc, and third-party crates, so embedding the public client library never pulls in Cedar or the engine.
  5. shiin-platform depends on no other Shiin crate. shiin-engine and shiin-schema never depend on it.
  6. xtask may depend on product crates, for example to validate examples against real types; no product crate depends on xtask.

Dependencies within a group are allowed when they respect these rules; Cargo rejects cycles.

Adding a crate

  1. Confirm the crate is in the table for the current milestone, or update the table in the same pull request.
  2. Create crates/<name>/Cargo.toml inheriting the [workspace.package] fields and [lints] workspace = true.
  3. Write crate-level documentation stating the crate’s responsibility and, before 1.0, that its API is internal (unless it is shiin-schema or shiin-client).
  4. Admit each new dependency under Dependencies and supply chain.
  5. If the crate is security-sensitive, add it to Security-sensitive changes.

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

SettingValueWhere it is set
Edition2024edition in [workspace.package] of Cargo.toml
Dependency resolver3resolver in [workspace] of Cargo.toml
Pinned toolchain1.98.1channel in rust-toolchain.toml
MSRV1.96rust-version in [workspace.package] of Cargo.toml
Formatter style edition2024style_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 cargo command run inside the repository uses Rust 1.98.1 with rustfmt and Clippy.
  • CI installs the same toolchain from the same file. The documentation workflow in .github/workflows/docs.yml already 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 main stays 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.

PeriodCratesMSRV rule
Before 1.0All cratesPinned stable minus 2 minor versions
After 1.0Library cratesLatest stable minus 6 minor versions, about eight months of releases
After 1.0Binary 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-version in [workspace.package], and inherited by every member.
  • After 1.0, a binary crate that tracks a newer MSRV overrides rust-version in its own manifest.
  • Resolver 3 is MSRV-aware. When Cargo resolves dependency versions, it prefers versions whose declared rust-version is 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 unsafe code.

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

  1. Install rustup by following the instructions at rustup.rs.

  2. 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
    
  3. 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 rustfmt using the repository’s rustfmt.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.

LintLevelWhy
unsafe_codeforbidUnsafe 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_useddenyunwrap() turns an unexpected None or Err into a panic. Errors are propagated with ? and handled as described in Error handling.
clippy::panicdenyAn explicit panic! in a hook or the engine can crash the process that was supposed to answer deny.
clippy::tododenyUnfinished code does not merge. A todo! reached at run time panics.
clippy::dbg_macrodenydbg! writes to standard error, bypasses logging configuration, and can print sensitive values.
clippy::print_stdoutdenyIn a hook, standard output carries the response the host parses, so a stray line can corrupt the decision. Libraries never print.
clippy::print_stderrdenyStandard 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_reasondenyEvery lint suppression states why it is needed, so reviewers can judge it and remove it when the reason is gone.
clippy::undocumented_unsafe_blocksdenyEvery 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:

LintLevelWhy
missing_docswarnPublic items are documented; see Documentation comments.
unreachable_pubwarnItems 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::pedanticwarn, priority -1Enables the pedantic group. The lower priority lets individual entries in the same table override lints from the group.
clippy::expect_usedwarnexpect 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:

LintLevelWhy
clippy::indexing_slicingdenyIndexing with [] panics when out of bounds. These crates use get and handle None.
clippy::arithmetic_side_effectswarnInteger 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]. expect produces 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, because expect would 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, and Grant, not synonyms.
  • Decision values are Allow, Deny, and Pending in Rust, matching allow, deny, and pending on 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 String or u64 values.
  • 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_docs enforces 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 Result have an # Errors section, and functions that can panic have a # Panics section. 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 tracing events 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-engine does not log. It returns a trace as part of its result, which keeps it pure and makes explanations reproducible.
  • Levels: error for failures that lead to a fail-closed deny, warn for degraded operation, info for lifecycle events, and debug or trace for 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 Debug by 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, and chore.
  • 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 a BREAKING CHANGE: footer explaining the migration.
  • Every commit carries a Developer Certificate of Origin sign-off, which git commit -s adds.
$ 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.

CategoryExamplesRepresentationWhat happens
DecisionsA rule denies a write; an action needs approvalA Decision value, returned normallyThe adapter enforces it. It is never an Err.
Operational errorsAn unreadable policy file, a failed audit write, a malformed host payload, an IPC timeoutA per-crate thiserror enum marked #[non_exhaustive]Mapped to a fail-closed deny in one place, logged with its error code
User diagnosticsA syntax error in policy.toml, a shadowed ruleDiagnostic data with source spans, rendered by the CLI with mietteShown to the operator; at run time, an invalid policy still fails closed
BugsA violated internal invariantdebug_assert! in development builds; no panics on the engine hot pathFixed 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, or Box<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-policy exposes diagnostics as plain data: an error code, a message, source spans, and optional help text.
  • shiin-cli renders that data with miette, showing the offending lines of the policy file. Keeping miette in the binary keeps rendering concerns out of the library.
  • Commands such as shiin policy validate report 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 no unwrap, 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, expect is 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 Err to 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.

AudienceWhat it receives
Agent, through the hostThe deny decision and the fixed agent-facing message for failures defined in Decision. No error text, path, error code, or stack trace.
OperatorCLI diagnostics, the error code, and the full error chain in logs
Audit logAn 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 main installs a panic hook with std::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 deny before 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 unsafe Rust is allowed, and what every use of it must include. The workspace-wide forbid already exists in Cargo.toml; shiin-platform does 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 root Cargo.toml. Every crate that inherits the workspace lints, including xtask today, cannot contain unsafe blocks, unsafe fn, or unsafe impl.
  • forbid cannot be overridden by an allow or expect attribute inside the crate.
  • The only exception is shiin-platform.

shiin-platform

  • shiin-platform is 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-engine and shiin-schema never 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:

  1. The Rust standard library.
  2. 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.
  3. 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

  1. A // SAFETY: comment directly above the block, explaining which invariants make it sound and where they are established. clippy::undocumented_unsafe_blocks enforces the presence of the comment; review enforces its accuracy.
  2. One unsafe operation per block, so each comment justifies exactly one thing. shiin-platform enables clippy::multiple_unsafe_ops_per_block for this.
  3. A # Safety section on any private unsafe fn, stating what callers must guarantee. Unsafe operations inside an unsafe fn still need their own unsafe block (unsafe_op_in_unsafe_fn).
  4. The smallest possible scope. Safe code does the preparation and the checking, and the unsafe block contains only the call.
  5. Tests that exercise the safe wrapper on every supported platform the wrapper targets, including error paths.
  6. 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.
  7. 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.

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 deny is enforced in CI, following ADR-0038; cargo vet is 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, and MIT-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.

Testing strategy

Note

Project policy: draft. This page is the only home of Shiin’s testing rules.

Levels

LevelWhat it coversWhere
UnitSingle functions and typesIn-module tests modules.
IntegrationCrate boundaries and the enginecrates/*/tests/.
ConformanceThe test vectorsdocs/spec/test-vectors/.
Fault injectionCrashes, timeouts, damaged state, and failed audit writesCLI unit and integration tests.
PlatformOne supported platform eachCI 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.

Performance budgets

Note

Project policy: draft. This page is the only home of Shiin’s performance budgets.

Latency

PathBudgetPlatform
Hook evaluation50 msTier 1
Hook total including process start200 msTier 1
Policy compilation1 sTier 1

Memory

PathBudget
Engine evaluation10 MB
Policy compilation100 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.

Continuous integration

Note

Project policy: draft. This page is the only home of Shiin’s CI rules.

Workflows

WorkflowWhen it runsWhat it does
ci.ymlPull requests and pushes to mainFormatting, 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.ymlPull requests, pushes to main, and manual dispatchDocumentation checks, stale-specification warnings, intent measurements, dependency checks, and book publication from main
robustness.ymlPull requests and pushes to mainNative key-store lifecycle checks on Linux, macOS, and Windows; bounded parser fuzzing and pure wire-type tests under Miri on a dated nightly
release.ymlPull requests and a version tagBuilds 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.ymlManual dispatchProduces an additional JSON CycloneDX diagnostic as a build artifact
link-health.ymlWeekly and manual dispatchChecks 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.

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:

PieceState
dist-workspace.toml with five Unix targets and shell installersconfigured for CLI and daemon archives
release.yml, generated by dist and repaired by Rust automationcandidate 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-attestationsconfigured for release archives
CycloneDX bills of materialsgenerated and uploaded in the release workflow; sbom.yml is an optional manual JSON diagnostic
cargo auditable dependency metadataconfigured in dist
Publishing crates to a registrydeliberately 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:

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.

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-platform or 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.

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

StatusMeaning
proposedA recommendation is written but not yet ratified, or its confirmation step is still pending
acceptedThe decision is in effect
deferredThe question is recognized but intentionally postponed to a named milestone
supersededReplaced by a later ADR, which is linked
deprecatedNo longer relevant, with no replacement

Process

  1. Copy the template to NNNN-short-title.md using the next free number.
  2. Open a pull request with status proposed, and add the record to the index below.
  3. A maintainer accepts the decision by changing its status when the pull request merges, following GOVERNANCE.md.
  4. To change a decision, write a new ADR that supersedes the old one, and update both records.

Index

Project

ADRTitleStatus
0001Record architecture decisionsaccepted
0002Rust for product and toolingaccepted
0003Project name: Shiinaccepted
0004Apache-2.0 license and DCOaccepted
0005Governance modelaccepted
0006Vulnerability reportingaccepted
0007No telemetryaccepted
0008mdBook documentationaccepted

Engine semantics

Policy, classification, intent, and risk

Deployment, API, adapters, and approvals

Evidence

ADRTitleStatus
0029Hash-chained JSON Lines audit logaccepted
0030Audit durabilityaccepted
0031DSSE and Ed25519 receiptsaccepted
0032Signing key storageaccepted
0033Enforcement levelsaccepted

Rust engineering

ADRTitleStatus
0034Edition, MSRV, and toolchainaccepted
0035Workspace layoutaccepted
0036Error handlingaccepted
0037Unsafe code policyaccepted
0038Supply chain policyaccepted
0039Release toolingaccepted
0040Platform support tiersaccepted

Deferred

ADRTitleStatus
0041Remote approver transportdeferred to v0.6
0042Language bindingsdeferred until after 1.0

ADR-0001: Record architecture decisions

Note

Architecture decision: accepted.

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

  1. MADR 4 records in docs/adr/, superseded but never deleted
  2. A shorter context, decision, and consequences format in docs/adr/
  3. Decisions recorded only in issues, pull requests, and discussions
  4. 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 proposed status 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-check checks that ADR numbers are unique, that every title starts with # ADR-NNNN:, that the index links every record, and that every ADR-NNNN reference 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 xtask documentation tooling is the only Rust crate in the workspace today.

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

  1. Rust for the product and all tooling
  2. Rust for the product, with Python or Node.js scripts for tooling
  3. Go for the product and tooling
  4. 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, and rumdl) are installed with cargo 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 unsafe code 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 unsafe code.
  • 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 xtask commands 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 xtask is 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.

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

  1. Keep “latch” as the permanent name.
  2. “Gate” — conveys the gating function directly.
  3. “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 is shiind, 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-check rejects 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.

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

  1. Apache License 2.0 with Developer Certificate of Origin.
  2. MIT License.
  3. BSD 3-clause.
  4. 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].license in Cargo.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 LICENSE file and NOTICE file 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.

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

  1. Benevolent dictator for life (BDFL).
  2. Lazy consensus with a maintainer group.
  3. 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.md and MAINTAINERS.md exist 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.

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

  1. A SECURITY.md file with a private email address and a PGP key, handled under embargo.
  2. A public GitHub issue tracker only.
  3. 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.md at 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.md exists at the repository root and links to the vulnerability disclosure page.
  • The README links to SECURITY.md for 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.

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

  1. No telemetry or data collection by default; opt-in for any remote feature.
  2. Anonymous usage statistics with an opt-out.
  3. 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.

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

  1. mdBook with mdbook-mermaid, vendored JavaScript.
  2. A static-site generator written in Python or Node.js.
  3. 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/ with SUMMARY.md as 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

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.

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

  1. Deterministic evaluation: the same action request and policy snapshot always produce the same decision.
  2. Probabilistic risk scoring: a numeric risk score determines whether an action is allowed.
  3. 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 to pending, 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.

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-engine for wasm32-unknown-unknown as a purity guard.
  • The engine must be fast and predictable on the hook path.

Considered options

  1. A pure, synchronous engine that performs no I/O and depends on no async runtime.
  2. An async engine with I/O for loading policy and grants on demand.
  3. 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-engine performs no I/O of any kind.
  • It takes an action request and a policy snapshot and returns a decision.
  • It depends on shiin-policy and shiin-schema only.
  • It never depends on shiin-classify (classification happens in the adapter before the engine is called).
  • From v0.4, CI builds shiin-engine for wasm32-unknown-unknown to 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-classify and shiin-policy may 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.

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 deny must always win, from any source.
  • pending must propagate unless a grant already covers it.
  • The default when nothing matches must be the safest option.

Considered options

  1. Three values (allow, deny, pending) with deny-wins and default-deny combining.
  2. A five-level combining scheme with permit, permit-pending, abstain, deny-pending, deny.
  3. 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, and pending.
  • A deny from any stage wins over every other result, including grants.
  • A pending wins over allow unless 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 only deny, 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 pending with a reason code.

Confirmation

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 allow and deny other than pending.

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.

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

  1. The fixed order: identity, capability, policy, intent, risk.
  2. A configurable order where operators choose which stages run first.
  3. 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 never allow.
  • The policy stage can produce allow, deny, or pending.
  • 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.

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

  1. Fail closed: any failure produces a deny decision.
  2. Fail open: any failure lets the action proceed.
  3. 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 deny decision.
  • 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 Err to 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 pending becomes 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-engine reproduces every test vector.

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

  1. Compile TOML policy to Cedar, with Cedar as the evaluation engine.
  2. 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-policy crate compiles TOML to Cedar at policy load time.
  • Cedar’s cedar-policy Rust 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-policy dependency, 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 forbid overriding permit is 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 pending is carried as a @decision("pending") annotation on a permit and 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.

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

  1. Three layers (built-in, user, workspace) where later layers can only restrict.
  2. A flat policy where all sources merge into one set of rules.
  3. 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

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.

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

  1. Built-in rules that deny access to protected resources, unallowable by any policy layer.
  2. No built-in rules; rely entirely on operator policy.
  3. 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 deny immediately 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

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.

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

  1. JSON with JSON Schema, identified by URN identifiers.
  2. Protocol Buffers with generated code.
  3. 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.json and 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-check compiles 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.

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

  1. Conservative classification: unknown effects are marked opaque, which escalates to pending.
  2. Heuristic classification: unknown effects are guessed and allowed if low-risk.
  3. 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_command risk signal and produce pending.
  • 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_command and other signals.
  • Use-case UC-02 requires shell commands like cat .env to 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 pending for 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.

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

  1. Canonicalize by resolving symlinks and .., then normalizing to an absolute path.
  2. Match on the path string as the agent provides it, without canonicalization.
  3. 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:

  1. 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.
  2. 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 ~.
  3. 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.create classifiable for a file that does not exist yet.
  4. Resolve . and .. lexically, after symlink resolution, so that a symlink cannot be stepped over by a traversal sequence.
  5. Normalize Unicode to NFC, so that two spellings of the same name cannot be treated as different paths.
  6. 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.
  7. Fail closed. If canonicalization errors, the effect is opaque and the request can never be allowed by a path rule.
  8. 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 .env to the same canonical path.
  • The shiin-classify crate 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

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

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

  1. Structured task declarations with a closed set of constraint kinds.
  2. Free-text task descriptions parsed by an LLM.
  3. 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, and require_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

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.

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

  1. Discrete, deterministic risk signals (named properties) that escalate to pending.
  2. A numeric risk score where higher scores escalate.
  3. 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_delete or secret_path.
  • A signal escalates the decision to pending unless 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

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.

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

  1. Daemonless hooks for v0.1, with an optional daemon added later for approvals.
  2. A daemon from v0.1 that all hooks connect to.
  3. 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 hook process 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 shiind and 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.

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

  1. JSON-RPC 2.0 over a Unix domain socket (and named pipes on Windows).
  2. A REST/HTTP API over localhost.
  3. 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-client library 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.

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

  1. Tokio, used only in binaries and the IPC crate, never in the engine crates.
  2. async-std as a lighter alternative.
  3. 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 shiind and shiin-ipc only.
  • 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 xtask check 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 xtask check 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.

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

  1. Start with Claude Code (the most popular host), then add Codex CLI and Cursor.
  2. Support all hosts simultaneously with a generic adapter.
  3. 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.

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

  1. Static adapters only: compile all adapters into the relevant binary, no dynamic loading.
  2. Dynamic-library plugins loaded at runtime.
  3. 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 shiin binary or the shiind daemon, 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.

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

  1. Three resolutions: allow_once, allow_always, deny, with single-use and scoped grants.
  2. A free-form approval with arbitrary parameters.
  3. 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, or deny.
  • An allow_once resolution creates a single-use grant bound to the digest of one action request.
  • An allow_always resolution 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 pending result. It never overrides deny.
  • 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.

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

  1. Deny with a ticket: deny now, return a ticket, allow retry after approval consumes a single-use grant.
  2. Block the hook until approval (waiting past the host timeout).
  3. 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

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.

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

  1. JSON Lines where each event includes the hash of the previous event.
  2. A binary append-only log with fixed-size records.
  3. 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 verify checks 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.

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

  1. Write the audit event and fsync before the adapter returns allow; a write failure produces deny.
  2. Buffer audit events in memory and flush periodically.
  3. 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 returning allow to 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

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.

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

  1. DSSE envelopes with Ed25519 signatures for checkpoints and receipts.
  2. A custom signature format wrapping the decision JSON.
  3. 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 verify checks signatures and the hash chain offline.
  • shiin replay re-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.

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

  1. Store keys in the OS keyring (keychain on macOS, Credential Manager on Windows, Secret Service on Linux).
  2. Store keys in a file protected by OS file permissions.
  3. 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.
  • shiin reads 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 init generates 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 init and shiin key rotate manage 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.

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

  1. Three labeled levels: L1 cooperative, L2 mediated, L3 OS-enforced.
  2. A numeric score (1–10) for each adapter.
  3. 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 --bare or disableAllHooks).
  • 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.

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

  1. Edition 2024, pinned toolchain 1.98.1, MSRV 1.96.
  2. Edition 2021, pinned toolchain 1.85, MSRV 1.83.
  3. 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].edition in Cargo.toml.
  • Pinned toolchain 1.98.1, set in rust-toolchain.toml.
  • MSRV 1.96, set in [workspace.package].rust-version in Cargo.toml (pinned stable minus 2 minor versions).
  • rustfmt style edition is 2024, set in rustfmt.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.toml and Cargo.toml declare 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.

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

  1. Virtual workspace root, crates/<name>/ for products, xtask/ for tooling, fuzz/ for fuzzing.
  2. Root package with all crates as sub-crates.
  3. 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.toml is 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 sets publish = false.
  • fuzz/ is a separate workspace with its own nightly toolchain for fuzz targets.
  • Every member inherits edition, rust-version, license, repository, and authors from [workspace.package].
  • Every member sets [lints] workspace = true, except shiin-platform.

Consequences

  • Good, because no crate is privileged by living at the root.
  • Good, because crates are grouped logically.
  • Good, because xtask and fuzz are isolated from the published crates.
  • Bad, because crates/<name>/ adds a directory level; contributors must learn the layout.

Confirmation

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.

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 become Err.
  • Operational errors must produce fail-closed deny.
  • Diagnostics must be actionable for operators.
  • Bugs must be caught in tests, not silently handled.

Considered options

  1. A four-category taxonomy: decisions, operational errors, user diagnostics, and bugs.
  2. A single Result<Decision, Error> with errors propagated everywhere.
  3. 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. Never Err.
  • Operational errors (per-crate thiserror enums, #[non_exhaustive]): unreadable files, IPC timeouts, malformed payloads. Mapped to fail-closed deny by 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 produce deny via 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_failure function.
  • 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 a deny into 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.

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

  1. Forbid unsafe_code in all crates except a single shiin-platform crate.
  • unsafe_code = "forbid" in [workspace.lints.rust].
  • Only shiin-platform is allowed unsafe, and only where no safe crate provides the needed facility.
  • Every unsafe block has a // SAFETY: comment, one unsafe operation per block, and tests on every supported platform.
  • shiin-platform declares its own lint table (copied from the workspace, with unsafe_code changed to deny).
  • 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 forbid cannot be overridden by allow or expect inside a crate.
  • Bad, because creating shiin-platform adds review overhead; it is created only when a concrete need exists.

Confirmation

  • The unsafe code policy page and Cargo.toml enforce the rule.
  • Two-maintainer review is required for any unsafe change.
  • Miri runs shiin-platform tests 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-platform for 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.

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

  1. Declare all dependencies in [workspace.dependencies] with default-features = false, and review each for safety and audit history.
  2. Allow per-crate dependency declarations without central review.
  3. 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 and default-features = false.
  • Members reference it with workspace = true and 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-deny checks advisories, licenses, bans, and sources in CI.
  • cargo-vet is not selected; its audit database would add maintenance cost without replacing the configured cargo-deny checks.

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.toml declares 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.

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

  1. A release-tooling crate in xtask, plus cargo-release or a custom script, using GitHub Actions.
  2. Hand-crafted release scripts in shell.
  3. 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 --release for 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: write permission.
  • 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

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.

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

  1. Three tiers: Tier 1 (tested + binaries), Tier 2 (builds but no CI), Tier 3 (community).
  2. Binary support only for the latest two stable releases of each OS.
  3. 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

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.

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

  1. Signed webhooks with Ed25519, delivered over HTTPS with mutual TLS.
  2. A centralized approval server with its own identity model.
  3. 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.

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-schema and shiin-client only.
  • Bindings must not pull in the full engine or Cedar crate.
  • Bindings should be generated or maintained with minimal manual effort.

Considered options

  1. Ship shiin-client as the Rust native library, then provide FFI bindings for other languages after 1.0.
  2. Provide official bindings for Python and TypeScript from the start.
  3. 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

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

  1. Option one
  2. Option two
  3. 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

  1. Copy the RFC template to NNNN-short-title.md.
  2. Open a pull request with the proposal.
  3. Reviewers discuss and the maintainers accept or reject.
  4. An accepted RFC produces an ADR.

Statuses

StatusMeaning
draftThe proposal is written and open for review.
acceptedThe proposal is accepted and produces an ADR.
rejectedThe proposal is not accepted.
supersededReplaced by a later RFC.

Current proposals

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 pending or deny it according to the existing attended/unattended behavior. None may produce allow.
  • 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

MilestoneThemeHeadline capability
M0SpecificationSpecs, threat model, test vectors, and the policy-engine spike
v0.1GateClaude Code adapter with policy, basic risk signals, and an audit log
v0.2TasksTask declarations and intent binding; Codex CLI and Cursor adapters
v0.3ApproveDaemon, approval protocol, and grants
v0.4EvidenceSigned receipts, verification, and replay
v0.5MCP and SDKMCP proxy, local API, and the Rust client library
v0.6ReachRemote approvals, streamable-HTTP MCP, shell wrapper, Windows
v1.0StableFrozen specifications, audited, conformance suite
After 1.0TeamsTeam 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 asserted level, capability, policy, and the secret_path, outside_workspace, bulk_delete, and opaque_command risk signals.
  • Built-in protected resources.
  • Commands: shiin init, shiin check, shiin explain, shiin policy validate, shiin hook claude-code, shiin log, and shiin doctor.
  • Hash-chained audit log.
  • pending expressed 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 task and shiin 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 verify and shiin replay, including what-if replay against a different policy.
  • Key management and rotation.
  • Redaction of sensitive values in audit events.
  • cargo-deny enforced 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-client passes 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 exec now 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.

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

Documentation style guide

Note

Project policy: accepted. Every page under docs/ follows this guide. Rules marked (checked) are enforced by cargo 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 implementation field is partial or complete.
  • 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’ implementation and milestone fields 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.

KindPurposeUsual location
indexLanding page that orients readersdocs/README.md
explanationWhy the design is the way it isoverview/, concepts/, architecture/
specPrecise, normative contracts that implementations are checked againstspec/, security/security-model.md
referenceFacts to look upreference/, glossary.md
how-toSteps to accomplish a goalguides/
tutorialA guided first experiencegetting-started/
adrOne architecture decisionadr/
rfcThe proposal process and its templaterfcs/
policyRules the project and its contributors followproject/, 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.

FieldAllowed valuesMeaning
kindsee Document kindsWhat sort of page this is
statusdraft, proposed, accepted, deferred, superseded, deprecatedWhere the content is in its review lifecycle
implementationnone, partial, complete, n/aHow much of the described behavior exists in released code; n/a for process pages
milestonem0, v0.1 through v0.6, v1.0, post-1.0, n/aThe roadmap milestone the content targets; see Roadmap
reviewedISO 8601 dateWhen 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.

MetadataAdmonitionBanner 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-case with the .md extension.
  • Every page appears in SUMMARY.md, and every SUMMARY.md entry 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, and pending. Use “ask” only when naming a host’s native mechanism, such as Claude Code’s ask permission decision. (checked: block in 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. shiin is the command-line binary. shiind is 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=spec pages 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 on kind=spec pages, 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

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 $id of the form urn:shiin:schema:<name>:v<major>, for example urn:shiin:schema:action-request:v1. URNs keep schema identity independent of any web domain. (checked)
  • Shared definitions live in common.v1.schema.json and 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 against test-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, or cedar.
  • Use domains reserved by RFC 2606 (example.com, example.org, *.example) for hosts.
  • Use fake secrets that contain the word EXAMPLE, such as sk-EXAMPLE-0000000000000000. Never paste a real-looking credential, even an expired one.
  • Use /home/dev/project (Linux), /Users/dev/project (macOS), and C:\Users\dev\project (Windows) as example paths.

Diagrams

  • Use Mermaid in fenced mermaid blocks only. GitHub renders them natively, and the book renders them through mdbook-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.
  • Link between pages under docs/ with relative paths that include the .md extension.
  • Link to files outside docs/ (such as SECURITY.md) with absolute GitHub URLs, because the built book does not contain them.
  • Refer to decisions as ADR-NNNN with a link, for example ADR-0014. Every ADR-NNNN mention 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.

FactHome
Term definitionsGlossary
Action typesAction types
Reason codesDecision
Risk signalsRisk signals
Failure semanticsSecurity model
Enforcement level of each adapterEnforcement levels
Host hook formatsthe host’s page under guides/integrations/
Latency and memory budgetsPerformance budgets
Platform tiersPlatform support
Crates and their responsibilitiesWorkspace and crates
MSRV and toolchainToolchain and MSRV
Error codesError codes
MilestonesRoadmap
DecisionsADR 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 reviewed is 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, and rumdl pass 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

ToolVersionPurposeConfiguration
cargo xtask docs-checkin repositoryProject-specific rules from the style guidextask/
mdBook0.5.4Builds the documentation site from docs/docs/book.toml, docs/SUMMARY.md
mdbook-mermaid0.17.1Renders Mermaid diagrams in the built site[preprocessor.mermaid] in docs/book.toml
lychee0.24.2Checks links and heading anchorslychee.toml
typos1.50.1Finds spelling mistakestypos.toml
rumdl0.2.73Lints 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

  1. Install Rust with rustup. The repository’s rust-toolchain.toml selects the pinned toolchain automatically; see Toolchain and MSRV.

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

CheckRule
MetadataLine 1 of every page is a valid shiin-doc comment with known values
Title and bannerLines 3 to 6 hold the title and the banner that matches the metadata
Table of contentsSUMMARY.md and the pages on disk match in both directions, with no duplicates
Requirement IDsDefined once, only in their owning kind=spec page; every mention refers to a defined ID
Threat IDsDefined once, only in the threat model; every mention refers to a defined threat
ADRsUnique numbers, ADR-NNNN: titles, listed in the index; every ADR-NNNN mention exists
TerminologyRequirement 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 SchemasDraft 2020-12, urn:shiin:schema: identifiers matching file names, valid against the metaschema, and compilable with cross-schema references
ExamplesJSON blocks after validate markers match their schema; blocks after validate-fail markers do not
Test vectorsEvery 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.md references 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 contents permission 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.

AdditionWhen
Validating TOML policy examples with shiin policy validatev0.1
Comparing reference/cli.md with the CLI’s generated helpv0.1
Versioned documentation per releaseAfter 1.0

Adding a check

  1. Write the rule in the style guide and mark it (checked).
  2. Implement it in a module under xtask/src/docs_check/, reporting problems with file and line.
  3. Fix every existing page so the check passes.
  4. Update the table in What cargo xtask docs-check enforces.

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.

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.

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.

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.

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.

Approval channel

A route by which approval requests reach approvers, such as the command-line approver or a webhook.

Approval request

A pending decision waiting for an approver to resolve it.

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

Audit event

One record in the audit log, such as a received request, a decision, or an approval resolution.

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.

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.

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.

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.

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.

Decision

The engine’s answer to an action request: allow, deny, or pending, with a reason code.

Default decision

The decision made when no rule matches. It is deny, can be configured to pending, and can never be allow.

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.

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.

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.

Engine

The pure, synchronous component that evaluates an action request against a policy snapshot and returns a decision. It performs no I/O.

Fail-closed

The rule that any failure (an error, a timeout, an unreadable policy) results in deny.

Gate

An evaluation stage that can return deny, pending, or pass, but never allow. The identity, capability, intent, and risk stages are gates.

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.

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.

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.

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.

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.

Policy snapshot

The immutable input to one evaluation: compiled policy, grants, active tasks, and counters, identified by a snapshot hash.

Reason code

A stable, machine-readable code that explains a decision, such as deny.policy.

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.

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.

Task declaration

A structured, human-confirmed statement of what an agent is working on: a description, constraints, and an expiry. Also called a task.

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

AvoidUse insteadWhy
block, blockeddeny, deniedOne word for one decision value
ask (for Shiin behavior)pending“Ask” is reserved for hosts’ native mechanisms
tool call (for Shiin behavior)actionA 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 layerShiin is none of these; see Vision
whitelist, blacklistallowlist, denylistClearer and inclusive
user (in specifications)developer, operator, approverSpecifications need the exact role
intent (meaning constraints)task declaration, constraintNatural-language intent is never enforced
verification (alone)receipt verification, outcome verificationThey prove different things