MCP server
Put Shotoku between your agent and the tools it can reach, so every call is checked against your policy before it runs — and the agent cannot opt out.
Two ways to use it
Decide this first. It changes the whole setup, and only one of the two is actually enforcement.
| Wrapping | Advisory | |
|---|---|---|
| What it does | Shotoku re-exposes your other MCP servers and gates every call | The agent calls authorize_action when it decides to |
| Can the agent skip it | No — it has no other route to the tools | Yes — nothing compels the call |
| What it is good for | Real enforcement over existing MCP servers | Actions with no MCP server behind them: purchases, your own API calls, code execution |
| Setup | Set SHOTOKU_MCP_UPSTREAMS and move your servers behind Shotoku | Add Shotoku alongside your other servers |
They are not exclusive. Wrapping covers everything reachable through MCP; advisory covers everything else. Most setups end up using both.
Install
Nothing to clone or build. The server runs straight from npm:
It speaks MCP over stdin, so running it by hand just sits there waiting — that is correct. Your agent host is what launches it. You will also want the CLI, which is how you review and approve what the agent asked for:
Setup: wrapping
Shotoku connects to your other MCP servers, re-exposes each of their tools under its own name, and gates every call. The agent talks to Shotoku and nothing else.
1. Set up a home for Shotoku
Your agent host launches the server with a working directory it chooses — often the filesystem root. Give Shotoku a fixed place to keep its policy and its ledger so that never matters:
2. List the servers to put behind Shotoku
The format is the same mcpServers shape your agent host already uses, so entries usually move across unchanged.
3. Point your agent host at Shotoku
Replace the entries you just moved. This is the whole config — Shotoku is now the only MCP server your host knows about:
On macOS the file is at ~/Library/Application Support/Claude/claude_desktop_config.json; on Windows, %APPDATA%\Claude\claude_desktop_config.json. Restart the app afterwards.
4. Write rules for the wrapped tools
A wrapped tool is named <server>__<tool>, and that name is the resource in your policy. This is the single most useful thing to know about wrapping.
Keep defaultVerdict: pending_approval. A new tool appearing in an upstream server after an update is then held for review instead of silently allowed.
5. Check it worked
In Claude Desktop, open the tools icon in a new conversation. You should see Shotoku’s own tools plus one entry per wrapped tool, prefixed: obsidian__search_vault, reddit__browse_subreddit, and so on. Then, in a terminal:
The ledger it prints must be the same file the server writes to. If they differ, the agent’s decisions are going somewhere you are not looking — the most common cause of an approval queue that appears empty.
Setup: advisory
For actions with no MCP server behind them — a purchase, a paid API call, running code — the agent asks Shotoku directly. Same install, no upstreams file, and Shotoku sits alongside your other servers rather than in front of them:
Be clear-eyed about what this is: the agent decides whether to ask. It is a guardrail and an audit trail, not a barrier. Use wrapping wherever a tool server exists.
The tools
A default install exposes three:
| Tool | What it does |
|---|---|
| authorize_action | Ask whether an action is allowed. Returns the full decision with reasons |
| get_decision | Look up a past decision by ID |
| get_pending_approvals | List decisions waiting for a human |
Three, not five. approve_decision and deny_decision exist but are withheld and refused if called: an agent that can approve its own pending decisions has not been gated, it has been given a form to fill in.
authorize_action
| Field | Required | What it means |
|---|---|---|
| action | yes | One of purchase, api_call, execute_code, send_email, mcp_tool, custom |
| resource | yes | What it is acting on — a domain, service name, or endpoint |
| amount | no | Cost in USD, if any |
| context | no | Extra detail recorded with the decision (JSON, max 16 KB) |
There is deliberately no actor field — see below.
Who the decision is attributed to
Every decision is recorded against an actor. Shotoku sets it; the agent does not.
An MCP client announces its own name during the initialize handshake, before it can make a single tool call, and the model never sees that value. A connection from Claude Desktop is recorded as claude-desktop, one from Cursor as cursor, with no configuration.
To name a server yourself — one entry per agent, each with its own budget — set SHOTOKU_MCP_ACTOR in that entry’s env, then write rules that apply to it alone:
Approving what the agent asked for
Approval happens where the agent cannot reach it — your terminal:
Or in the TUI (shotoku tui), which is the same queue with keyboard navigation.
What happens after you approve
An approval is redeemable once, by the next call matching it exactly — same agent, same tool, same arguments, same amount. The agent retries, the call goes through, and the approval is spent.
That last line is the design, not a bug. Approving an action is not approving it forever — standing permission is what policy.yaml is for. Approvals also expire, after fifteen minutes by default (grantTtlSeconds).
shotoku status lists approvals waiting to be used, so one never disappears into nothing:
What the agent is told when blocked
A denial says so plainly and tells the agent not to retry.
What ends up in the ledger
Every gated call is recorded: the agent, the server, the tool, the verdict, the reasons, and a hash linking it to the record before it. For wrapped calls the names of the arguments are recorded — never their values. Tool arguments routinely carry API keys, file contents and personal data, and an audit log is not a place to accumulate them.
The values are still bound to the approval, through a digest. That is what stops an approval for one file from being redeemed against another.
Environment variables
| Variable | Default | What it controls |
|---|---|---|
| SHOTOKU_HOME | ~/.shotoku | Where policy and ledger live when nothing else says otherwise |
| SHOTOKU_MCP_UPSTREAMS | unset | Path to the upstreams file. Setting it turns on wrapping |
| SHOTOKU_MCP_ACTOR | the client name | Overrides the agent name recorded in the ledger |
| SHOTOKU_MCP_ALLOW_APPROVALS | unset | 1 hands the approval tools to the agent. It removes the separation the product exists to provide |
| SHOTOKU_POLICY | from the home | Path to the policy file |
| SHOTOKU_LEDGER | from the home | Path to the decision ledger |
| SHOTOKU_CONFIG | shotoku.config.json | Path to the Shotoku config file |
| SHOTOKU_RECEIPT_SECRET | unset | When set, approved decisions carry a signed receipt |
Which files the server actually uses
Every Shotoku process resolves the same way, highest priority first:
SHOTOKU_POLICY/SHOTOKU_LEDGER- a
shotoku.config.json, searched for from the working directory upward - an existing
./data/decisions.jsonlbeside the working directory $SHOTOKU_HOME, otherwise~/.shotoku
shotoku where prints what a directory resolves to and what decided it. Run it in your terminal and compare with your host config — if the two disagree, the server is writing decisions the CLI cannot see.
When something is wrong
| Symptom | Almost always |
|---|---|
| The host says the server failed to start | Check its MCP logs — Shotoku names the reason on stderr. Usually the upstreams file: invalid JSON, or a path that is not absolute. |
| No prefixed tools appear | Wrapping is off or matched nothing. SHOTOKU_MCP_UPSTREAMS must point at a file with an mcpServers object, and each entry needs a command. Shotoku says which entry it rejected. |
| The agent still reaches a wrapped server directly | Its entry is still in your host config. Wrapping only enforces anything when Shotoku is the only route. |
| shotoku status is empty but the agent says it is blocked | The CLI and the server are reading different ledgers. Run shotoku where and compare with the host config. |
| Everything is denied, including allowed things | The policy file failed to load and Shotoku fails closed. Run any shotoku authorize; the reason names the problem. |
| An approval does not let the retry through | Something in the call changed — a different argument or amount — or more than fifteen minutes passed. An approval binds to the exact call. |