Least-Privilege Tool-Scoping Checklist for Production Claude Agents
Every tool you give a Claude agent is a capability an attacker could try to trigger through prompt injection. This checklist walks through scoping each tool down to the minimum it needs, before the agent goes to production.
Name the exact capability the tool grants: Write one sentence describing what the tool can do in the real world, not what it's for. "Sends an email to any address" is the honest description of a generic send_email tool, even if its intended purpose is password resets.
Ask whether a narrower tool would work: A send_password_reset_email tool that only accepts a user_id and looks up the address internally is safer than a general send_email tool, because the model never has an address parameter to manipulate.
Identify what data the tool can read: List every field, table, or document a tool's execution touches, including anything it reads internally to fulfill a call, not just what's in its input_schema.
Identify what data the tool can write or change: Separately list every write, update, delete, or external side effect, this is usually the higher-risk half of the tool's surface.
Decide if the tool needs write access at all: A large share of "action" tools can be redesigned as read-plus-human-confirmation, where Claude proposes an action and a human or a separate confirmation step executes it.
Write a narrow input_schema: Constrain types, use enums instead of free-text strings wherever the valid values are a known, finite set, and mark every field that must be present as required.
Avoid open-ended string parameters for anything that maps to a real action: A query: string field on a database tool is far more dangerous than a set of specific, enumerated lookup parameters.
Write the tool description defensively: State explicitly what the tool must not be used for, in addition to what it's for, since the description itself shapes when the model reaches for the tool.
Cap the blast radius of a single call: For tools that operate on multiple records, add an explicit limit (max_records, pagination) so one call cannot touch an entire dataset.
Scope credentials per tool, not per application: Give each tool its own scoped API key, database role, or service account limited to exactly what that tool needs, never share one broad credential across every tool in the agent.
Validate every argument before executing, regardless of what the model claims: Treat arguments Claude passes as untrusted input, the same way you'd treat a value from an HTTP request body.
Allowlist destinations for any tool that sends data externally: An email, webhook, or API-calling tool should check the target address or URL against an allowlist before sending, not trust whatever the model supplied.
Enforce rate limits per tool, per session: Cap how many times a given tool can be called within a conversation or a time window, to bound the damage of a hijacked agent making repeated calls.
Log every tool call with its arguments and outcome: Keep an audit trail (with PII redacted per your logging policy) that lets you reconstruct exactly what a tool did and why, after the fact.
Fail closed on validation errors: If a tool's input fails validation, return an error the model can act on, don't silently coerce malformed input into something that "seems close enough" to execute.
Run an adversarial test pass: Attempt to get the agent to misuse each tool through a crafted retrieved document or user message, and confirm the scoping and validation hold.
Confirm no tool has standing access beyond its stated purpose: Re-derive the credential scope for each tool from its input_schema and description, and flag any mismatch where the underlying access is broader than the tool needs.
Map tool access against the combined threat model: For each tool with write or external-communication capability, confirm what untrusted content (if any) could feed into a call to that tool, this is the multiplying risk factor from the threat model.
Get sign-off from someone who did not write the tool: A second reviewer catches scope creep the original author has stopped noticing.
Document a revocation path: Know, in advance, how to disable or restrict a specific tool quickly if it's found to be misused in production, without taking down the entire agent.
Tiers 1-2 (items 1-10) happen at design time, before any code is written, and are the cheapest to get right early, reworking a tool's scope after launch means migrating callers and credentials.
Tier 3 (items 11-15) is enforced in code, at the point the tool actually executes, and should be covered by automated tests, not just manual review.
Tier 4 (items 16-20) is a gate before deployment, not a one-time step, repeat it whenever a tool's definition or backing permissions change.
Treating the input_schema as the entire security boundary. A narrow schema helps, but if the backing credential or database role is broad, a validation bug or edge case can still reach data the tool was never meant to touch. Fix: scope the credential itself, don't rely on schema validation alone.
Sharing one API key or service account across every tool in an agent. This collapses the blast radius of the entire toolset into whatever that one credential can do, defeating the point of per-tool scoping. Fix: issue a separate, minimally-scoped credential per tool or per tool category.
Writing a tool description that only explains the happy path. A description that says "sends a notification" without stating "only to addresses on the internal allowlist" gives the model, and any injected instruction, no signal about the intended boundary. Fix: state constraints explicitly in the description, not just in code.
Do I need to run this entire checklist for a purely read-only tool?
Yes, but the stakes differ. Read-only tools still need Tier 1-2 scoping (what data can it read, is the schema narrow) and Tier 3 validation, since a read tool with access to sensitive data can still be an exfiltration path if its output ever reaches an untrusted channel.
Why does item 10 insist on a separate credential per tool instead of one shared credential for the whole agent?
A shared credential means every tool inherits the combined permissions of all of them, so a validation gap in any single tool exposes everything the shared credential can access. Per-tool credentials keep each tool's actual blast radius equal to its stated scope.
What's the fastest way to turn a broad tool into a narrow one?
Replace open-ended parameters (a free-text query, an arbitrary target address) with enumerated or internally-resolved values. A send_notification(user_id, template) tool that looks up the address internally is narrower than send_notification(to_address, message).
Should validation logic live in the tool's Python function or somewhere else?
It should live in the same function that executes the tool's side effect, as the first thing that function does, so there is no code path where an unvalidated argument can reach the real action. Centralizing it there also makes it easy to unit test independent of a live API call.
How is this checklist different from general input validation practices?
General input validation assumes a human or a trusted client is the source of input. Here, the "client" calling the tool is a model whose arguments can be influenced by content it read (including injected instructions), so validation has to assume the input is adversarial by default, not just malformed by accident.
What does "fail closed" mean in item 15, concretely?
def send_notification(user_id: str, template: str) -> dict: if template not in ALLOWED_TEMPLATES: raise ValueError(f"Unknown template: {template!r}") # never silently substitute a default template here return dispatch(user_id, template)
Rejecting with a clear error, rather than silently defaulting to some fallback behavior, keeps the failure visible instead of quietly executing something unintended.
Is a rate limit per tool really necessary if the tool itself is narrowly scoped?
Yes. Narrow scope limits what a single call can do, a rate limit limits how many times it can be repeated. A tool that can only send one type of low-risk notification is still a problem if it can be triggered a thousand times in one session.
Who should be the "second reviewer" in item 19?
Anyone with enough context to evaluate the tool's actual capability, not just its intended purpose, and who didn't write the code, since the original author's mental model of "what this tool is for" makes it easy to overlook what it's actually capable of.
How does this checklist relate to the combined risk described in the security and RAG threat model?
Tool scope is one of the three multiplying factors in that threat model (untrusted content, tool access, permission scope). This checklist directly targets the last two, reducing what a tool can do and how broadly it's allowed to do it, independent of whatever defenses exist against the untrusted-content factor.
What is a revocation path, and why does item 20 ask for it in advance?
A revocation path is a fast, tested way to disable or restrict a specific tool (feature flag, credential rotation, config toggle) without redeploying the whole agent. Having it ready before an incident, rather than improvising during one, is what makes the difference between a contained issue and an extended outage.
Should this checklist be applied to internal, developer-facing agents too, or only customer-facing ones?
Internal agents still combine untrusted content (internal documents, tickets, code, other employees' input) with tool access, so the same risk factors apply. Internal-only reduces who could be an attacker, it doesn't eliminate the need for scoping.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official anthropic Python SDK (latest 0.x release). Model names, pricing, and SDK versions move quickly - verify current specifics at platform.claude.com/docs before relying on them.
Reviewed by Chris St. John·Last updated Jul 19, 2026