The guard checks only calls that can change something. Getting this wrong in the safe direction costs a check; in the other direction a write would go unchecked, so the rules are asymmetric.

How a tool is classified

First match wins:
  1. An editor’s call, set on Tools or through the mapping endpoint (action: read, or any confirm/edit/none = write).
  2. The app’s annotations. Declared side effects come first: destructiveHint: true, or openWorldHint: true on a tool not marked readOnlyHint: true, make it a write even when it’s named like a read (get_and_archive). Then readOnlyHint: true is a read, false a write, whatever the name says. A read-only tool that reaches the outside world (a web search: readOnlyHint and openWorldHint both true) stays a read.
  3. Claros’s guess, proposed once per tool when it’s registered. It may mark a tool write, but it can never mark a write-looking name (create_, update_, delete_, submit_, send_, pay_, call_, execute_ …) as read. Only an expert can.
  4. The name, until Claros answers: get, list, search, read, find, describe, fetch prefixes, check_, run_, export_, or exactly version / ping. Everything else is a write.
The guard asks GET /v1/tools/kinds?app=<server> after registering tools, on every tools/list and at most every 30 s on writes.

Registration

Every tools/list the server answers is passed to the agent unchanged, then its tools are sent to Claros (POST /v1/tools): only new tools or tools whose definition changed. It’s fire and forget: a failure is logged and retried on the next tools/list, and never delays a call.

Other paths

  • A JSON-RPC batch that contains a write tools/call is rejected.
  • Only tools/call is inspected; everything else is relayed byte for byte.