GET /api/sidecars/reviews/<id> on the Control Plane answers, but it takes the Sidecar token, which an agent must not hold. The Sidecar gives the agent three ways that need no credential:
The status path and the MCP server need a Control Plane, because the approval status lives there. None of the three returns the statement, and none approves anything.
Approvals were called reviews before 1.244.0. The Sidecar still answers every earlier name on this page: the
X-Hoop-Review-* fields, the /.well-known/hoop/reviews/<id> path and the review_* MCP tools. An agent configured against them keeps working.Find the approval in the denial
On http and grpc, an approval denial carries the approval in structured fields, so a client does not parse the message. http sends them as headers on the403. grpc sends the same keys, in lowercase, as trailing metadata beside PERMISSION_DENIED.
The body stays the text message, so a client that knows nothing about approvals still reads it. kubectl prints it as
Error from server (Forbidden): ....
On postgres, mysql, mssql and mongodb, the approval id is in the error text only:
Read the status on an http listener
An http listener answersGET /.well-known/hoop/approvals/<id> itself, for an approval request filed on that listener. The agent asks the same host and port it sends its requests to:
/.well-known/hoop/reviews/<id>, answers the same.
next says what the agent does, so it never interprets a status on its own. See Status and next. rejection_reason is present on a rejection that gave one. expires_at is present only when the approval rule sets a time limit.
An error body is
{"message": "..."}. Every answer sends Cache-Control: no-store and closes the connection.
The whole /.well-known/hoop/ prefix belongs to the Sidecar. Every http listener keeps it, with or without a Control Plane, and never forwards it to the upstream. A route the upstream serves under that prefix is not reachable through the Sidecar.
The Sidecar answers it only as the first request on a connection. Behind another request, the listener closes the connection unanswered, because HTTP/1.1 pairs responses with requests by order. curl and Go clients resend on a new connection.
Other protocols have no path to reserve, so they read the status over MCP.
The MCP server
Themcp block starts a Model Context Protocol server with three read-only tools. It serves every listener of the Sidecar, whatever the protocol.
config.yaml
listenis required. Remove the block to turn the server off.- The Sidecar needs a Control Plane. A config with the block and no Control Plane is refused at startup.
- A reload does not start, stop or move the server. Restart the Sidecar after you add, remove or change the block.
- A bind failure stops the Sidecar, as a listener’s bind failure does.
listeners, then restart the Sidecar. On a Sidecar connected to a Control Plane, the file applies on the first import, or when the Sidecar runs from its config file.
Tools
Each tool is also served under its earlier name,
review_status, review_wait and review_list, with the same input and output. An answer points the agent at the tools of the family it called, so an agent allowed only the review_* tools is told to call review_wait.
approval_status and approval_wait return one approval request and what to do next. approval_list returns {"reviews": [...]} of the same shape (the key keeps its earlier name), and status filters on PENDING, APPROVED, REJECTED, REVOKED, PROCESSING, EXECUTED or EXPIRED.
instruction is next in words, for an agent that reads prose. timed_out and waited_seconds come from approval_wait only. timed_out: true is not an error: call again.
Keep the 60-second default for approval_wait. Some MCP clients drop a tool call that blocks for longer than 60 to 120 seconds. approval_wait sends a progress notification every 2 seconds to a client that asks for them.
When the config has an mcp block, a return-mode denial leads with the approval id and names the tool:
Status and next
The status path and the MCP tools map a status tonext the same way:
The resend runs through the listener like any statement, so the analyzer, the audit trail and masking apply, and the resend uses the approval. With
spend_approval_on: accepted, a resend that the upstream refuses gives the approval back. See The retry contract.
Two tool answers are errors:
Connect an agent
Add one MCP entry per Sidecar the agent uses, next to the connection string it pairs with. One entry covers every listener and every replica of that Sidecar: the server keeps no session, so it can sit behind a load balancer.- Claude Code
- Cursor
- Other clients
For one developer:For a team, commit
.mcp.json at the repository root. claude mcp add --scope project writes it:.mcp.json
hoop-approvals-payments and hoop-approvals-ledger. The denial does not name the Sidecar, so name each entry after the connection string it pairs with, and tell the agent to call the entry of the Sidecar that denied.
On Kubernetes
Setlisten: 0.0.0.0:8765 in the config file, so the Service reaches the pod. Then publish the port with one laneServices entry in the chart values:
values.yaml
http://<fullname>-mcp.<namespace>.svc:8765/mcp. <fullname> is <release>-hoopsidecar, or the release name alone when it already contains hoopsidecar. kubectl get svc shows it.
Every pod in the cluster can reach a ClusterIP Service. Add a NetworkPolicy that admits only the agent’s pods.
Next
Risk Analysis
Holding a statement for a person, return mode, and the retry contract.
Config File
Every other section of the config, and what startup refuses.