Claude Integration
governed_tools wraps your Claude tool functions so AgentTrust checks each call before it runs.
Guide · Python · Claude (Anthropic Messages API)
Wrap your tool functions with one helper and every tool call Claude asks for is checked before it runs. Then run a small calculator agent with and without AgentTrust and watch a call get refused.
Tier: Team+
What you will build
With the Anthropic Messages API, Claude decides which tool to call, and your code runs it. Nothing stops Claude from asking for a tool it should not use. The AgentTrust helper governed_tools wraps each tool function so the call goes to AgentTrust first. By the end of this guide:
- Every tool call is checked before it runs, and its result after.
- A refused call never runs. Claude gets
Blocked by AgentTrust: <reason> (envelope ...)as the tool result and the run continues. - Every decision, allowed or refused, shows under Events in your console.
Prerequisites. Python 3.10 or later and the anthropic package, version 2 (anthropic>=2.0,<3.0). The "See it in action" walkthrough needs an Anthropic key (ANTHROPIC_API_KEY) unless you add --scripted, which replays a fixed conversation and needs no model and no key. The local gateway needs no account; the console route needs an AgentTrust console account.
Package name. Install agentrust-py and import agentrust_sdk. The PyPI package agentrust-sdk belongs to another company.
Connect AgentTrust in 4 steps
The helper works with a manual tool loop and with beta_tool. These steps connect to your console. With no account, use the local gateway instead.
Install the adapter
pip install "agentrust-py[claude]"Register the agent
In your console, select New agent and copy its API Key and Agent ID. The key starts with at_live_ and is shown once. Select the agent, then under Manifest & fingerprint select Declare and add the tools it may use, with an optional limit for a tool that moves money, such as issue_refund up to 100.
Do not skip the manifest. An agent with no declared tools is not restricted: every tool is allowed.
Point your app at the console
export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech
export AGENTRUST_KEY=at_live_...
export AGENTRUST_AGENT_ID=support-agentWrap the tools
Where: in the one place your tool functions become Claude tools. Wrap the functions first, then pass the wrapped ones to beta_tool. Your Anthropic client does not change.
+ import os
+ from agentrust_sdk.adapters.claude_agents import governed_tools
- tools = [beta_tool(add), beta_tool(multiply)]
+ tools = [beta_tool(fn) for fn in governed_tools(
+ [add, multiply], agent_id=os.environ["AGENTRUST_AGENT_ID"]
+ )]Apply governed_tools under beta_tool, not on the finished tool object. beta_tool builds its schema and validator from the function when it is created. A tool you hand to Claude unwrapped is not checked.
Your functions do not change. The wrapped function keeps its name, docstring and signature, so beta_tool builds the same schema for the model. Async functions stay async. For one function you can also write @beta_tool over @governed_tool(agent_id=...). In a manual loop, call the wrapped function for each tool_use block and send its return value back as the tool_result.
When Claude asks for a tool the manifest does not allow, or one over its limit, the tool does not run. Claude receives the reason as the tool result and can explain or try something else:
Blocked by AgentTrust: Tool-trust hard gate: tool_trust: 'issue_refund' amount 5000.0 exceeds declared max_amount 100 (envelope …)To raise an error on a block instead of letting Claude read the refusal, pass on_block="raise".
Check the replies too
ClaudeAgentGuard checks the replies Claude writes. It is separate from the tool wrapper.
from anthropic import Anthropic
from agentrust_sdk.adapters.claude_agents import ClaudeAgentGuard
guard = ClaudeAgentGuard(agent_id="support-agent")
client = guard.wrap_client(Anthropic()) # AsyncAnthropic: guard.wrap_async_client(...)A blocked reply raises BlockedError. With AGENTRUST_FAILURE_MODE=closed a gateway outage raises GatewayUnavailableError. messages.stream is not covered.
No account? Connect to a local gateway
Skip steps 2 and 3 and start a gateway inside your program before the agent's first tool call:
pip install "agentrust-py[claude,embedded]"from agentrust_sdk.embedded import embed_gateway
gateway = embed_gateway()
gateway.register_manifest("support-agent", {
"declared_tools": [
{"name": "lookup_order"},
{"name": "issue_refund", "max_amount": 100},
],
})Set AGENTRUST_AGENT_ID=support-agent (or pass the ID straight to governed_tools) and go to step 4. Decisions last only while the program runs, so use this to try AgentTrust, in tests and in CI.
Not on Claude? Wrap any Python function with @harness: see the @harness quickstart. On LangGraph, see the LangGraph quickstart. Serving tools over MCP? See the MCP quickstart.
See it in action
The same four steps, applied to a small calculator agent. You run it three ways and watch a tool call get refused. About 10 minutes. Each step has one thing to do and one thing you should see.
Before you start. Python 3.10 or later. Steps 1 to 4 need nothing else when you add --scripted to each command; without it they need an Anthropic key (ANTHROPIC_API_KEY). Steps 5 to 7 use an AgentTrust console account.
Create the project
Make a folder with its own Python environment and install what the program needs:
mkdir agentrust-quickstart && cd agentrust-quickstart
python -m venv .venv && source .venv/bin/activate # on Windows: .venv\Scripts\activate
pip install "agentrust-py[claude,embedded]"Create a file named claude_quickstart.py in that folder, paste the program below and save. It is the example examples/claude_quickstart.py from the AgentTrust SDK repository. The tools (add, multiply, divide) are at the top, the agent is the usual Messages API loop (send the messages, run each tool Claude asks for, send the results back), AgentTrust is one line (governed_tools, step 3), and the ways to run it are at the bottom.
governed_tools asks the AgentTrust gateway before the tool runs; the gateway records every check as an event.Here is what happens when the agent is asked to add, then to divide. The tool names come from the program you are about to create.
- Claude asks for a tool.
messages.createreturns atool_useblock foradd(ordivide), and your loop calls that tool. - The governed tool asks AgentTrust first. The wrapped function sends the tool name and its arguments to the gateway. This is the pre-check, and your function has not run.
- The gateway decides and records it. It compares the call with the agent's declared tools:
addis declared,divideis not. Either way it writes an event. - Allowed: the function runs. Refused: it never does.
add(3, 4)runs and its result goes back for a post-check, a second event.dividebecomes aBlocked by AgentTrustresult instead. - Claude gets a result either way. It reads the answer or the refusal and replies, and the loop finishes.
So an allowed call leaves two events (before and after it ran) and a refused call leaves one. With --console they appear under Events in step 7; with --local the gateway lives inside the program and forgets them when it exits.
See the whole program
claude_quickstart.py
"""
claude_quickstart.py — a Claude agent whose tool calls AgentTrust checks.
The agent is a calculator: Claude (Anthropic Messages API) with add, multiply
and divide tools, asked to do two sums. The loop is the usual one: send the
messages, run each tool Claude asks for, send the results back.
AgentTrust is one line (marked "AgentTrust" below): the tools are wrapped with
governed_tools before they are turned into Claude tools. Every call is checked
before it runs, and its result after. A tool the agent may not use is refused
before its code runs; Claude is told why as the tool result and the run still
finishes. The agent may use add and multiply, not divide.
RUN IT (in the folder that holds this file)
pip install "agentrust-py[claude,embedded]"
export ANTHROPIC_API_KEY=... # or use --scripted, which needs no key
python claude_quickstart.py --without-agentrust # AgentTrust off: every tool runs
python claude_quickstart.py --local # a gateway starts inside this program; no account
python claude_quickstart.py --console # your AgentTrust console keeps every decision
python claude_quickstart.py --scripted # replay a fixed conversation instead of calling a model
--console needs three settings, from the agent you create in the console:
export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech
export AGENTRUST_KEY=at_live_...
export AGENTRUST_AGENT_ID=...
ANOTHER MODEL
CLAUDE_QUICKSTART_MODEL=claude-opus-5-5 (export ANTHROPIC_API_KEY).
Any Claude model that supports tool use works.
"""
from __future__ import annotations
import argparse
import os
import sys
import tempfile
from types import SimpleNamespace
from anthropic import Anthropic, beta_tool
from agentrust_sdk.adapters.claude_agents import governed_tools
AGENT_ID = os.environ.get("AGENTRUST_AGENT_ID", "calculator-agent")
MODEL = os.environ.get("CLAUDE_QUICKSTART_MODEL", "claude-sonnet-5-5")
# The tools this agent may call. With --local they are set here; with --console
# you declare the same list on the agent in the console. divide is left out on purpose.
MANIFEST = {"declared_tools": [{"name": "add"}, {"name": "multiply"}]}
SYSTEM = (
"You do the arithmetic you are asked for, using your tools, one tool call at a time. "
"If a tool is refused, say the operation could not be done."
)
def multiply(a: int, b: int) -> int:
"""Multiply `a` and `b`."""
return a * b
def add(a: int, b: int) -> int:
"""Add `a` and `b`."""
return a + b
def divide(a: int, b: int) -> float:
"""Divide `a` by `b`."""
return a / b
# AgentTrust: every call to these tools is checked before it runs and after it
# returns. Turn the wrapped functions into Claude tools, not the plain ones.
tools = [beta_tool(fn) for fn in governed_tools([add, multiply, divide], agent_id=AGENT_ID)]
REQUESTS = ["Add 3 and 4.", "Divide 10 by 2."]
def ask(request: str, client, max_turns: int = 6) -> str:
"""The agent loop: run the tools Claude asks for until it answers in text."""
by_name = {tool.name: tool for tool in tools}
messages = [{"role": "user", "content": request}]
for _ in range(max_turns):
reply = client.messages.create(
model=MODEL, max_tokens=1024, system=SYSTEM, messages=messages,
tools=[tool.to_dict() for tool in tools],
)
messages.append({"role": "assistant", "content": reply.content})
calls = [block for block in reply.content if block.type == "tool_use"]
if not calls:
return "".join(block.text for block in reply.content if block.type == "text")
results = []
for call in calls:
# A refused call returns the refusal text here, and Claude reads it next turn.
output = by_name[call.name].call(call.input)
print(f" tool {call.name}({', '.join(f'{k}={v}' for k, v in call.input.items())}) -> {output}")
results.append({"type": "tool_result", "tool_use_id": call.id, "content": str(output)})
messages.append({"role": "user", "content": results})
return "(stopped: too many turns)"
# --scripted: replay a fixed conversation, so the quickstart runs with no model and no key.
# A replay does not read tool results, so its last line is the same with AgentTrust on or off;
# what differs is what each tool returned.
# Each reply is either a tool call (name, arguments) or the final text.
SCRIPT = [
("add", {"a": 3, "b": 4}), "3 + 4 = 7.",
("divide", {"a": 10, "b": 2}), "That is everything you asked for.",
]
class ScriptedClient:
"""Stands in for Anthropic(): messages.create returns the next scripted reply."""
def __init__(self, replies=SCRIPT) -> None:
self._replies = list(replies)
self.messages = SimpleNamespace(create=self._create)
def _create(self, **kwargs):
reply = self._replies.pop(0)
if isinstance(reply, str):
block = SimpleNamespace(type="text", text=reply)
else:
name, args = reply
block = SimpleNamespace(type="tool_use", id=f"toolu_{len(self._replies)}", name=name, input=args)
return SimpleNamespace(content=[block], stop_reason="end_turn")
def ask_agent(where: str, scripted: bool) -> bool:
"""Ask the agent to add, then to divide. False if the model can't be reached."""
print(f"AgentTrust: {where} · agent {AGENT_ID} · model {'scripted' if scripted else MODEL}")
client = ScriptedClient() if scripted else Anthropic()
try:
for request in REQUESTS:
print(f"\n=== {request} ===")
print(f"\nResult: {ask(request, client)}")
except Exception as exc: # noqa: BLE001 - any provider error ends the run
print(f"\nThe model call failed ({MODEL}): {exc}\n"
"Check ANTHROPIC_API_KEY is set, or run with --scripted.", file=sys.stderr)
return False
return True
# --without-agentrust: every tool runs. For comparison.
def run_without_agentrust(scripted: bool) -> int:
os.environ["AGENTRUST_ENABLED"] = "false" # the SDK's off switch: wrapped tools pass every call through
return 0 if ask_agent("off (every tool runs)", scripted) else 1
# --local: a gateway runs inside this program; no account needed.
def run_local(scripted: bool) -> int:
from agentrust_sdk.embedded import embed_gateway
# SQLite in a temp dir, gone after the run. embed_gateway sets
# AGENTRUST_GATEWAY_URL and AGENTRUST_KEY, which governed_tools reads.
gateway = embed_gateway(db_path=os.path.join(tempfile.mkdtemp(), "gateway.db"))
gateway.register_manifest(AGENT_ID, MANIFEST)
try:
ok = ask_agent(f"local gateway {gateway.url} (no account, nothing kept)", scripted)
finally:
gateway.stop()
return 0 if ok else 1
# --console: the SDK sends every check to your console, which keeps the decisions.
def run_with_console(scripted: bool) -> int:
missing = [v for v in ("AGENTRUST_GATEWAY_URL", "AGENTRUST_KEY", "AGENTRUST_AGENT_ID") if not os.environ.get(v)]
if missing:
print(f"Set {', '.join(missing)} (from the agent you created in the console), or use --local.",
file=sys.stderr)
return 2
console = os.environ["AGENTRUST_GATEWAY_URL"]
ok = ask_agent(f"console {console}", scripted)
print(f"\nOpen Events in the console ({console}) and filter by agent ID {AGENT_ID}.")
return 0 if ok else 1
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="The Claude calculator agent, governed by AgentTrust.")
where = parser.add_mutually_exclusive_group()
where.add_argument("--local", action="store_true", help="run a gateway inside this program, no account (the default)")
where.add_argument("--console", action="store_true", help="send every check to your AgentTrust console")
where.add_argument("--without-agentrust", action="store_true", help="for comparison: AgentTrust off, every tool runs")
parser.add_argument("--scripted", action="store_true", help="replay a fixed conversation: no model, no API key")
args = parser.parse_args(argv)
if args.without_agentrust:
return run_without_agentrust(args.scripted)
return run_with_console(args.scripted) if args.console else run_local(args.scripted)
if __name__ == "__main__":
sys.exit(main())Check that the file works:
python claude_quickstart.py --helpRun it without AgentTrust
First, see what the agent does on its own:
python claude_quickstart.py --without-agentrust --scriptedThe scripted agent replays a fixed conversation and does not read tool results, so its last line is the same with AgentTrust on or off. What differs is what each tool returned.
AgentTrust: off (every tool runs) · agent calculator-agent · model scripted
=== Add 3 and 4. ===
tool add(a=3, b=4) -> 7
Result: 3 + 4 = 7.
=== Divide 10 by 2. ===
tool divide(a=10, b=2) -> 5.0
Result: That is everything you asked for.The agent used divide and nothing stopped it. For a calculator that is harmless. Swap divide for issue_refund or delete_user and it would run just the same.
Wrap the tools
This is the only change AgentTrust needs, and it is already in your file. Nothing to edit. It sits right after the tools are defined and before the loop uses them. On your own agent it looks like this:
+ from agentrust_sdk.adapters.claude_agents import governed_tools
- tools = [beta_tool(fn) for fn in [add, multiply, divide]]
+ tools = [beta_tool(fn) for fn in governed_tools([add, multiply, divide], agent_id=AGENT_ID)]Names, descriptions and argument types stay the same, so Claude sees what it saw before. Allowed calls run and their results are checked. Refused calls never run.
Run it locally
Run the same agent with AgentTrust on:
python claude_quickstart.py --local --scriptedThe program starts a small AgentTrust gateway inside itself and allows only the tools in MANIFEST: add and multiply. divide is not on the list. No account needed.
AgentTrust: local gateway http://127.0.0.1:8765 (no account, nothing kept) · agent calculator-agent · model scripted
=== Add 3 and 4. ===
tool add(a=3, b=4) -> 7
Result: 3 + 4 = 7.
=== Divide 10 by 2. ===
tool divide(a=10, b=2) -> Blocked by AgentTrust: Tool-trust hard gate: action 'divide' is not in this agent's declared_tools (envelope dec75b50-beb1-4603-a8e7-cd8bdad96951)
Result: That is everything you asked for.add ran. divide was refused before its code ran, and Claude was told why, so the run still finished. Nothing is kept after the program exits.
Create the agent in your console
To keep every decision and manage the agent from the console, register it there:
- Open Agents, select New agent, name it
claude-quickstart, and select Create. - Copy the API Key and the Agent ID before you select Done. The key starts with
at_live_and is shown only once. Never paste it in a chat, a ticket or a commit. - Select the agent. Under Manifest & fingerprint, select Declare. Under Declared tools, select + Add tool twice, type
addandmultiply, and select Save manifest.
That list is the same one MANIFEST holds in the program.
Do not skip the manifest. An agent with no declared tools is not restricted: every tool is allowed, so divide would run.
Run it against the console
Give the program the three values from step 5, then run it with --console:
export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech # your console's address
export AGENTRUST_KEY=at_live_... # the API key
export AGENTRUST_AGENT_ID=claude-quickstart # the Agent ID
python claude_quickstart.py --console --scriptedThe first line names the console, and the run should match step 4: add returns 7 and divide is refused with the same Blocked by AgentTrust text. The last line tells you where to look. Drop --scripted to use a real model.
See the decisions in the console
Open Events in the console and type your Agent ID into Filter by Agent ID. You will see one row per decision: add approved before and after it ran, and divide blocked. Select a row to see the scores behind the decision. The Events view looks the same as in the LangGraph quickstart; this page has no captures of its own yet.
You are done. Your agent's tool calls are now checked, refused when they should not run, and recorded.
What AgentTrust adds
The same agent, without and with the one change: wrapping its tools.
| Your agent can… | Without | With |
|---|---|---|
| Be limited to the tools you allow you ran this | ✗ | ✓ |
| Have a call stopped before it runs you ran this | ✗ | ✓ |
| Tell Claude why a call was refused, and keep running you ran this | ✗ | ✓ |
| Leave a record of every decision, with scores | ✗ | ✓ |
| Have a tool's result checked before Claude sees it | ✗ | ✓ |
| Cap risky amounts, such as a refund over 100 | ✗ | ✓ |
| Hold a risky call until a person approves it | ✗ | ✓ |
| Be stopped instantly with a kill switch | ✗ | ✓ |
| Refuse every call if AgentTrust can't be reached (optional) | ✗ | ✓ |
Have each reply Claude writes checked (ClaudeAgentGuard) | ✗ | ✓ |
✗ means the Anthropic SDK does not provide it, so you would build it yourself. The first three rows are what you ran. The others are covered by the SDK's tests, run against a real embedded gateway, and the Claude adapter guide (docs/framework-adapters/claude-agents.md), not run here.
Options
| Option | Default | Effect |
|---|---|---|
on_block | "refuse" | "raise" raises BlockedError instead of returning the refusal text |
review_timeout_s | 300 | how long a held call waits for a person before it is refused |
AGENTRUST_FAILURE_MODE | open | closed refuses every call when the gateway cannot be reached |
AGENTRUST_ENABLED=false | on | kill switch: tools run plainly, nothing is checked |
Best practices
- Wrap every tool the agent has; an unwrapped tool is not checked
- Give each agent its own
agent_id - Declare tools and limits in the manifest
- Use
ClaudeAgentGuardas well when the reply text matters
Troubleshooting
| Issue | Fix |
|---|---|
RuntimeError: Claude Agents adapter requires Team tier | Upgrade to Team |
| A tool runs that should be refused | It was not wrapped, or the agent has no declared tools |
Every call is refused with gateway unavailable | Closed failure mode and the gateway cannot be reached |
governed_tools applied but the tool still unchecked | It was applied to the beta_tool object. Wrap the function, then pass it to beta_tool |
Next steps
Hold risky calls for a person. Set the agent's mode to Human Review. Rejected or timed-out calls do not run.
Serve tools to other clients. Guard an MCP server with one line: see the MCP quickstart.