Shotoku is a local-first governance layer that enforces budget caps and approval workflows before agents execute paid actions. It intercepts tool calls, checks policy rules stored in YAML, and writes decisions to a local JSONL audit log. No cloud sync, no custody handoff, no external approval service.
The core primitive is a synchronous authorization check that runs before any action that costs money, calls a paid API, or mutates external state. If the action exceeds a per-transaction or daily budget, Shotoku denies it or queues it for human approval. The decision is recorded locally, and the agent only proceeds if the status is approved.
Architecture
Shotoku sits between your agent orchestration code and the tool execution layer. The integration point is a single function call:
import { authorize } from "@shotoku/core"; const decision = await authorize({ actor: "shopping-agent", action: "purchase", resource: "api.openai.com", amount: 24.99, }); if (decision.status === "approved") { // execute the tool call } else if (decision.status === "pending_approval") { // queue for human review } else { // denied, log and skip } The authorize function reads a local policy file (policy.yaml) and appends a decision record to decisions.jsonl. The policy file defines per-resource rules:
rules: - resource: openai.com actions: [purchase, api_call] maxAmount: 50 maxDailyAmount: 200 verdict: approved - resource: stripe.com actions: [purchase] verdict: pending_approval - resource: "*" verdict: denied The decision log is append-only JSONL. Each line is a JSON object with an ID, status, amount, and timestamp. Approval events reference the original decision ID:
{"id":"dec_01","status":"approved","amount":"12.00","timestamp":"2026-11-14T14:32:01Z"} {"id":"dec_02","status":"pending_approval","amount":"89.99","timestamp":"2026-11-14T14:33:12Z"} {"id":"apr_01","type":"approval","decision":"dec_02","status":"approved","timestamp":"2026-11-14T14:45:00Z"} This structure supports offline reconciliation and tamper detection. If a decision record is missing or out of sequence, you know something was deleted or reordered.
State Management and Distributed Execution
The local-first design assumes a single process or a shared filesystem. If you run multiple agent processes on different machines, each maintains its own decision log. There is no built-in reconciliation across nodes.
For distributed deployments, you have three options:
- Shared filesystem: Mount a network volume so all processes write to the same
decisions.jsonl. File locking prevents concurrent writes from corrupting the log. - Centralized aggregator: Each agent writes to a local log, and a separate process tails all logs and merges them into a central audit store. This adds latency but preserves the local-first guarantee during network partitions.
- Single orchestrator: Run one agent process that dispatches work to stateless workers. Only the orchestrator calls
authorize, so budget tracking stays consistent.
The policy file can be version-controlled and deployed to all nodes. Changes take effect on the next authorize call, so there is no cache invalidation problem.
Approval Workflow Implementation
When a decision status is pending_approval, the agent must block or queue the action. Shotoku does not provide a built-in UI or notification system. You implement the approval loop yourself:
- Blocking queue: The agent polls
decisions.jsonlfor approval events. When a human approves the decision (by appending an approval record), the agent retries the tool call. - Callback registration: The agent registers a webhook or callback function that fires when an approval event is written. This requires a file watcher or event stream.
- Human-in-the-loop UI: A separate dashboard reads
decisions.jsonl, displays pending decisions, and writes approval records when a human clicks "Approve."
The approval record is just another JSONL line. The agent code checks for it:
function waitForApproval(decisionId) { return new Promise((resolve) => { const watcher = fs.watch("decisions.jsonl", () => { const log = fs.readFileSync("decisions.jsonl", "utf8"); const lines = log.split("\n").filter(Boolean); const approval = lines.find( (line) => JSON.parse(line).decision === decisionId ); if (approval) { watcher.close(); resolve(JSON.parse(approval)); } }); }); } This is naive but functional. For production, use a proper event stream or message queue.
Budget Reconciliation and Rolling Windows
The maxDailyAmount rule enforces a rolling 24-hour budget. Shotoku scans the decision log for all approved transactions in the last 24 hours and sums the amounts. If the new request would exceed the limit, it is denied or queued.
This scan happens on every authorize call. For high-throughput agents, this becomes a performance bottleneck. You can optimize by caching the rolling sum and invalidating it every minute, or by indexing the decision log with a time-series database.
The policy file does not support budget rollover or monthly caps. If you need that, you write custom logic that reads the decision log and adjusts the maxDailyAmount dynamically.
Security Boundaries
The policy file and decision log are plaintext. If an attacker gains filesystem access, they can delete decisions, modify the policy, or forge approval records. Shotoku does not sign or encrypt the log.
To harden the setup:
- Immutable log: Write
decisions.jsonlto an append-only file or object storage bucket with versioning enabled. - Separate approval process: Run the approval UI on a different machine with read-only access to the decision log. Approvals are written to a separate file that the agent polls.
- Cryptographic signatures: Hash each decision record and include the hash of the previous record, creating a Merkle chain. This detects tampering but does not prevent it.
The local-first design means there is no authentication layer. Any process that can read the policy file can call authorize. If you run untrusted code alongside your agent, it can bypass Shotoku by calling the tool directly.
Observability and Failure Modes
The decision log is your primary observability surface. You can tail it in real time, export it to a monitoring tool, or parse it with jq:
cat decisions.jsonl | jq 'select(.status == "denied")' Common failure modes:
| Failure | Symptom | Mitigation |
|---|---|---|
| Policy file missing | All requests denied | Validate policy file exists on startup |
| Decision log corrupted | JSON parse errors | Write each line atomically, use file locking |
| Approval never arrives | Agent hangs indefinitely | Add timeout to approval polling loop |
| Budget calculation drift | Incorrect daily totals | Periodically reconcile log against external ledger |
| Concurrent writes | Interleaved JSON lines | Use advisory file locks or single-writer pattern |
Shotoku does not retry failed authorize calls. If the policy file is unreadable or the log write fails, the function throws an error. Your agent code must handle this and decide whether to fail open (allow the action) or fail closed (deny it).
Deployment Shape
Shotoku is a library, not a service. You install it via npm and import it into your agent code. The CLI (shotoku-cli) provides commands to inspect the decision log and validate the policy file, but it does not run a daemon or server.
For a single-machine deployment:
- Install
@shotoku/corein your agent project. - Write a
policy.yamlfile. - Call
authorizebefore every paid action. - Tail
decisions.jsonlfor audit and debugging.
For a multi-machine deployment, add a log aggregator or shared filesystem. The policy file can be deployed via configuration management (Ansible, Chef) or read from a Git repository.
Code Example: Intercepting Tool Calls
If you use LangChain or a similar framework, wrap the tool execution function:
import { authorize } from "@shotoku/core"; async function executeToolWithBudget(tool, args, cost) { const decision = await authorize({ actor: "my-agent", action: tool.name, resource: tool.endpoint, amount: cost, }); if (decision.status !== "approved") { throw new Error(`Tool call blocked: ${decision.reasons.join(", ")}`); } return tool.execute(args); } This pattern works for any tool that has a known cost. For tools with variable costs (like LLM API calls), you estimate the cost based on token count or use a conservative upper bound.
Technical Verdict
Use Shotoku when:
- You run agents on your own infrastructure and need budget enforcement without cloud dependencies.
- You want a tamper-evident audit log for compliance or debugging.
- You need human-in-the-loop approval for high-value actions.
- You can tolerate the operational overhead of managing local state and approval workflows.
Avoid Shotoku when:
- You run agents in a distributed, multi-tenant environment where local state is impractical.
- You need real-time budget tracking across hundreds of agents (the log scan becomes a bottleneck).
- You require cryptographic guarantees or non-repudiation (the log is not signed).
- You want a turnkey approval UI or notification system (you must build it yourself).
Shotoku is a control primitive, not a complete governance platform. It gives you the building blocks to enforce budgets and approvals, but you are responsible for the approval loop, log aggregation, and security hardening. For teams that already run local-first infrastructure and want fine-grained cost control, it is a clean, minimal solution. For teams that need cloud-native governance or multi-region consistency, it is too narrow.