The Read-Only Agent That Wasn't: Auditing What Your Tools Can Actually Reach

A tool named `check_service_health` woke someone at 3am. Read-only is a claim about effects at the far end of every call, and there's a short audit that either proves it or shows you what you actually deployed.

An agent described in its own design doc as read-only paged the on-call engineer at three in the morning. It had called exactly one tool: a health-check endpoint. That endpoint, on failure, opens an incident.

"Read-only" was a claim about the names of the tools — get_, list_, check_ — not about their effects. Nobody had verified the effects, because the naming made verification feel unnecessary.

Read-only is a property of effects, not of prefixes

The only meaningful definition: after this run, nothing outside the agent's own context is different. That is a claim about what happens on the far side of every tool call, including calls the agent makes indirectly, and it cannot be established by reading a tool list.

Three ways a read tool writes:

The side-effecting read. Health checks that open incidents. Search endpoints that log queries into a system others act on. Any GET that increments a counter someone alerts on, refreshes a cache, or extends a lock. These are ordinary API design; they only become surprising when something starts calling them thousands of times autonomously.

The tool that fans out. A "fetch this URL" tool is not one capability, it's every capability reachable by URL — including internal admin endpoints that accept GET for actions they shouldn't. Browser and computer-use tools are the extreme case: one tool whose real surface is everything the browser session is logged into.

The read that feeds a write. The agent reads a document, and the document contains text that shapes the next call. On its own the read changed nothing. In a loop with any write tool at all, it is the first half of an exploit, and the boundary you thought you had is between two calls in the same context.

The audit that actually settles it

Skip the tool list. Build a reachability table, one row per tool, three columns:

Tool Effects at the far end What it can reach
check_service_health opens an incident on failure; writes to status history pager rotation, status page
fetch_url none directly every host the network allows, with the agent's cookies and headers
search_tickets writes a query-audit row ticket index only

Filling in column two requires reading the implementation or asking whoever owns the endpoint — there is no shortcut, and that is the point. Filling in column three usually produces the surprise: the row where one innocuous tool's reach is "most of the internal network."

⚠️ Do this per deployment, not per tool. The same fetch_url implementation is read-only behind an allowlist and unbounded without one.

Enforce where the model can't reach

An audit is a snapshot; enforcement is what keeps it true after the next change.

Use a credential that cannot write. A database role with SELECT only, an API token issued read-scope, a network policy allowing the three hosts the job needs. The advantage over prompt-level or code-level rules is that it holds no matter what the agent does, including things nobody anticipated, and it keeps holding when someone adds a tool next month without re-reading the design doc.

Put allowlists on the fan-out tools. A URL fetcher without a host allowlist is not a read-only tool under any configuration. Same for shell tools, query tools, and file readers with a path that isn't rooted.

Make the boundary testable. One eval case whose expected result is a permission error, run in CI. It fails loudly the day someone grants the service account a broader role for an unrelated reason — which is how these regressions actually arrive.

✅ What to check before calling a run read-only

  • Every tool's far-end effects are documented, from the implementation and not from the name.
  • Fan-out tools are bounded by an allowlist that is enforced server-side.
  • The credential itself lacks write permission, so the claim survives a prompt change.
  • At least one test asserts that a write attempt fails.
  • The audit is re-run when tools are added — a checklist item on the PR template does this well enough.

The takeaway

Read-only is not a naming convention, a system-prompt instruction, or an intention. It is a property you establish by enumerating what each tool does at the far end, bounding the tools whose reach is open-ended, and issuing a credential that makes the claim true whether or not the rest of the reasoning holds. Until the credential can't write, the agent isn't read-only — it's just polite.

Keep reading

Similar posts

Matched on shared tags and category — the more bars, the stronger the overlap with what you just read.