Shell arbitration for coding agents

Stop trusting
the command.
Start checking the intent.

The agent proposes the command. sino checks it against the request you actually made, then allows or denies it before a process starts.

The failure mode

The agent wasn't hacked.
It was trying to help.

Every incident below had a permission system. The agent was allowed to run commands.

The problem is that allowed never meant asked for.

The distinction

Permissions ask who.
sino asks why.

A deny-list knows what a command looks like. It does not know what you asked for. So it blocks the cleanup you wanted, waves through the destructive command you didn't, and trains you to turn it off.

EXISTING PERMISSION LAYER static rules
# deny dangerous-looking strings
"deny": [
"Bash(rm -rf:*)",
"Bash(git reset --hard:*)",
"Bash(*DROP TABLE*)"
]
Blocks the shape. Not the intent.

rm -rf * in /tmp is blocked. rm -rf * in your home directory is also blocked. One is a cleanup; the other is an incident. The rule cannot tell.

When the rule is wrong, you disable it. Then it blocks nothing.
SINO intent is an input
# the judge receives all four
request: "clear the temp build output"
command: "cd /tmp && rm -rf *"
cwd: "/Users/you/app"
flags: ["none"]
Same bytes. Different verdict.

The bounded delete in /tmp serves the request. The unbounded delete in your home directory does not. The decision is about the task, not the spelling.

Bounded cleanup: allow. Unbounded delete: deny.
Live arbitration

Change the request.
Watch the verdict flip.

Pick a preset to see the same command flip. Then edit either field and make the example yours.

PRESET COMMANDS scroll or drag to browse
arbitration console
local demo / deterministic judge ready
YOUR OWN EXAMPLE

Edit either field, then run. Editing detaches the preset.

cwd /Users/you/app platform darwin
No process is spawned.
WHAT THE JUDGE SEES structured input

                
PARSED SCOPE 0 flags
Audit record JSONL · includes the command

                  
                
binary verdict fail-closed cache per request Production: decision model over /v1/decisions
The pipeline

One line in.
One verdict out.

No shell is spawned until the decision is made. The judge sees the whole line, the parsed subcommands, the effective working directory, and the request that started the task.

01

Capture intent

The plugin remembers the user's request for the task. That request is the only trusted statement of intent. A command can never nominate itself.

trusted "clear the temp build output"
02

Sanitize & parse

Comments and heredoc bodies come out. Substitutions and pipes stay intact. `cd` updates the effective cwd. Tripwire content is flagged and never reaches the model.

sanitized cd /tmp && rm -rf *
03

Judge

A decision model scores Allow or Deny against the request. The command is untrusted data: instructions, fake scores, and hidden characters are not evidence.

verdict Allow 0.94 / Deny 0.03
04

Decide & record

One binary effect. Any timeout, transport error, or malformed answer is a deny. Every record includes the command, parsed scope, reason, probability, and latency.

effect allow · logged
The landscape

Where sino fits.

Deny-lists, sandboxes, scoped tokens, and audit logs all matter. None of them answers the question this layer answers: did the user ask for this?

Swipe to compare columns

Capability Static ruleshooks · deny-lists · Blast Radius Sandboxescontainers · microVMs · Seatbelt Agent IAMscoped tokens · OAuth Audit logstraces · SIEM · outputs sinointent arbitration
Reads the user's request — — — — ✓
Blocks known-dangerous shapes ✓ ◐ — — ✓
Distinguishes /tmp from $HOME ◐ ✓ — — ✓
Understands pipes, $(…), heredocs, cwd ◐ — — ◐ ✓
Resists instructions inside the command ◐ — — — ✓
Allows requested destructive work on real targets ✕ ✕ — — ✓
Fails closed on timeout or error varies ✓ — — ✓
Explains the verdict in a sentence ◐ — — ◐ ✓
Logs the command, scope, reason, probability ◐ — ◐ ◐ ✓
STATIC RULES

Fast, cheap, and easy to bypass. The rule sees the string, not the task. It blocks the cleanup you asked for and misses the command that only becomes destructive because of where it runs.

SANDBOXES

Contain the blast radius. They do not decide whether the blast was intended. A sandbox that blocks the work is just a slower “no”, and most teams turn it down to get the work done.

AGENT IAM

Scoped tokens and short-lived credentials are necessary. They would not have stopped the Cursor agent in the PocketOS incident from using an over-privileged Railway token. Identity is not intent.

OBSERVABILITY

A log after the fact is a post-mortem, not a control. It tells you what happened after the volume and its backups are gone.

Install

Drop it in front
of the shell.

sino is an OpenCode plugin. Point it at any OpenAI-compatible /decisions endpoint, or use OpenCode's decision-model transport. No static deny rules required.

1

Install with opencode plugin add github:bsalbilla06/sino.

2

Set enforce: true and a 2s timeout.

3

Run the tests, then observe a session in enforce: false if you want a dry run.

opencode.json
// opencode.json
{
  "plugins": [{
    "package": "github:bsalbilla06/sino",
    "options": {
      "enforce": true,
      "transport": "fetch",
      "fetch": {
        "baseURL": "https://api.openai.com/v1",
        "path": "/decisions",
        "model": "gpt-6-luna",
        "apiKeyEnv": "OPENAI_API_KEY"
      },
      "timeoutMs": 2000,
      "allowThreshold": 0.55,
      "cache": true,
      "denyOnTripwire": true,
      "log": {
        "enabled": true,
        "file": ".opencode/sino/verdicts.jsonl"
      }
    }
  }]
}
$ bun test # 72 tests, no credentials
$ bun run test/eval.ts # live calibration
$ bun run audit # audit table