Governing Claude agents with 10 lines of YAML
The Anthropic Agent SDK builds Claude-powered agents in Python. flux7-mesh governs them with YAML policies. MeshHooks connects the two in 3 lines of code. Full walkthrough, 5 minutes.
While building flux7-mesh (a governance proxy for agent tool calls) and learning the Anthropic Agent SDK, I wanted to make mesh7 a native part of the Claude ecosystem. The Agent SDK ships with a hooks system that intercepts tool calls before execution. I wrote a small integration layer called MeshHooks that plugs mesh7's policy engine into those hooks. This tutorial shows how to set it up.
1 Install
You need two things: the mesh7 binary (Go) and the Python SDK.
# mesh7 binary
go install github.com/KTCrisis/flux7-mesh/cmd/mesh7@latest
# Python SDK with Agent SDK support
pip install flux7-mesh[anthropic]
2 Define your tools
For this example, the agent has three tools: reading files, writing files, and deleting files.
tools.pyimport os
def read_file(path: str) -> str:
"""Read a file and return its contents."""
with open(path) as f:
return f.read()
def write_file(path: str, content: str) -> str:
"""Write content to a file."""
with open(path, "w") as f:
f.write(content)
return f"Written to {path}"
def delete_file(path: str) -> str:
"""Delete a file."""
os.remove(path)
return f"Deleted {path}"
Nothing special here. Plain Python functions, no framework dependency, no decorator. mesh7 doesn't care how you implement your tools. It only sees their names and parameters when they're called.
3 Write the mesh7 config
In Agent SDK mode, mesh7 doesn't proxy the tool calls. It doesn't need to know your tools or how to reach them. It acts purely as a policy engine: the hooks send a tool name and parameters, mesh7 evaluates them against the rules, and returns a decision via POST /decide.
listen: ":9090"
policy:
default_action: human_approval
rules:
- agent: "file-agent"
tool: "read_file"
action: allow
- agent: "file-agent"
tool: "write_file"
action: allow
conditions:
- param: path
pattern: "/tmp/*"
- agent: "file-agent"
tool: "delete_file"
action: deny
That's the 10 lines that matter. Three rules, each binding an agent identity to a tool with an action:
read_file: always allowed.write_file: allowed, but only under/tmp/*. Theconditionsblock lets you restrict based on parameter values, not just tool names. Here we're saying "write wherever you want, as long as it's in /tmp."delete_file: always denied. No condition, no exception.
Anything not matched by a rule falls back to human_approval, which means the call is queued and waits for a human to approve or deny it. This is the safety net: if the agent discovers a new tool or calls something you didn't anticipate, it won't execute silently.
4 Wire MeshHooks into your agent
This is where MeshHooks comes in. Three lines to connect your Agent SDK agent to the policy engine.
agent.pyfrom anthropic.agent import Agent, ToolDefinition
from mesh7 import MeshHooks
from tools import read_file, write_file, delete_file
# Connect to mesh7
hooks = MeshHooks(agent="file-agent")
# Define tools for the Agent SDK
tools = [
ToolDefinition(
name="read_file",
description="Read a file and return its contents",
input_schema={
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
function=read_file,
),
ToolDefinition(
name="write_file",
description="Write content to a file",
input_schema={
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"},
},
"required": ["path", "content"],
},
function=write_file,
),
ToolDefinition(
name="delete_file",
description="Delete a file",
input_schema={
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
function=delete_file,
),
]
# Create the agent with governance hooks
agent = Agent(
model="claude-sonnet-4-6",
tools=tools,
hooks=hooks.agent_sdk_hooks(),
)
result = agent.run("Read /tmp/notes.txt, then write a summary to /tmp/summary.txt")
The MeshHooks(agent="file-agent") constructor creates a set of hooks that the Agent SDK will call before every tool execution. Under the hood, MeshHooks sends the tool name and its parameters to mesh7's POST /decide endpoint, which evaluates the YAML policy and returns allow, deny, or ask. The agent ID "file-agent" is what links this agent instance to the rules you wrote in the config.
The hooks.agent_sdk_hooks() call returns a dict that the Agent SDK understands natively. It registers a PreToolUse hook on all tools (you can narrow the scope with a regex via tool_matcher if needed). The tool definitions and the agent creation are standard Agent SDK patterns, nothing mesh7-specific.
5 Run it
Two terminals:
# Terminal 1 — start mesh7
mesh7 --config config.yaml
# Terminal 2 — run the agent
python agent.py
Watch the mesh7 logs. You'll see every tool call evaluated against your policy:
INFO tool_call agent=file-agent tool=read_file decision=allow latency=0ms
INFO tool_call agent=file-agent tool=write_file decision=allow latency=0ms path=/tmp/summary.txt
INFO tool_call agent=file-agent tool=delete_file decision=deny latency=0ms
If Claude tries to delete a file, the hook returns deny before the function is ever called. The agent receives a denial reason in the hook response, understands the action was blocked, and adapts its plan accordingly.
What happens under the hood
The flow, step by step:
- Claude decides to call
write_filewith{"path": "/tmp/summary.txt", ...} - The Agent SDK fires the
PreToolUsehook before execution - MeshHooks sends
POST /decideto mesh7 with the tool name, params, and agent identity - mesh7 evaluates the YAML policy:
write_fileis allowed forfile-agentwhenpathmatches/tmp/* - mesh7 returns
allow. MeshHooks translates to the Agent SDK format - The tool function executes. mesh7 logs a structured trace
One design choice worth noting: if mesh7 is unreachable, MeshHooks defaults to deny. Fail closed, not open. The idea is that if the governance layer is down, the agent shouldn't keep running ungoverned. You can change this with fail_action="allow" if you prefer leniency in development, but for production agents, closed is the safer default.
What about human_approval?
We've used allow and deny so far, which are immediate decisions. But human_approval is the more interesting case: the agent wants to do something, and mesh7 decides a human should validate it first.
When a tool call hits a human_approval rule, mesh7 queues the request. The hook blocks, and the agent waits. On the other side, a human can resolve the pending approval through several channels:
- The CLI:
mesh7 approval listto see pending requests,mesh7 approval resolve <id> --approveor--deny - The HTTP API:
POST /approvals/{id}/approve - The flux7-console dashboard, which shows pending approvals in a dedicated view
- A supervisor agent (another Claude instance, or a rules engine) that polls pending approvals and resolves them programmatically
Once resolved, mesh7 unblocks the hook and returns the decision to the agent. If the human approved, the tool executes normally. If denied, the agent gets a denial reason and adapts.
The interesting part is what happens over time: if you connect flux7-memory, mesh7 records every approval as a queryable fact. After 3 consistent approvals for the same agent+tool pattern with 0 rejections, mesh7 starts auto-approving that pattern. The governance becomes less intrusive over time without becoming less safe.
Going further
This tutorial covers the simplest case. Everything below can be added without touching agent code. Only YAML and config:
| Feature | Config change | Effect |
|---|---|---|
| Human approval | action: human_approval | Tool call queued, agent waits, human approves/denies in console |
| Rate limiting | rate_limit: {max: 10, window: 60s} | Max 10 calls per minute per tool |
| Temporal grants | mesh7 grant create ... | Temporary override: allow delete_file for the next 30 minutes |
| Auto-approve | Connect flux7-memory | After 3 human approvals for the same pattern, auto-approve future calls |
| Multi-agent | Different agent: per rule | Each agent gets its own permissions, same mesh7 instance |
| JWT auth | auth.jwt in config | Agent identity from IdP tokens instead of strings |
All of this is policy and configuration. Your Python agent code stays exactly the same. That's the separation that matters: the governance layer evolves independently from the agent code. Different repo, different team, different deploy cycle. A platform engineer can tighten a policy or add rate limiting without redeploying the agent, and a developer can add new tools without waiting for a policy review.