Claude Agent SDK Integration#
Govern a Claude Agent SDK agent with FirstOps. A single PreToolUse hook governs every tool the agent runs — built-ins (Bash, Write, Read, WebFetch), MCP tools, and custom tools — with block, argument rewrite, or audit.
What FirstOps governs#
The Claude Agent SDK runs tools inside the Claude Code engine. FirstOps hooks into the PreToolUse event, so every tool call is evaluated against your policies before it runs:
- Built-in tools (
Bash,Write,Read, …) —system_toolschannel - MCP tools (
mcp__<server>__<tool>) —mcpchannel, with credential brokering through the FirstOps proxy - Each call returns
allow,deny(the tool never runs), ormodify(FirstOps rewrites the tool's input viaupdatedInput)
With permission_mode="bypassPermissions", the FirstOps hook is the sole gate: a hook deny blocks the tool; everything else flows.
Setup#
1. Create an agent principal#
In the FirstOps dashboard, create an agent. You'll get an agent ID and a DPoP private-key PEM.
2. Install#
pip install "firstops[claude]"
The Claude Agent SDK uses your local Claude Code authentication.
3. Initialize FirstOps and wire the hook#
import firstops
from firstops.integrations.claude import firstops_hooks
from claude_agent_sdk import query, ClaudeAgentOptions
fo = firstops.init(
agent_id="<agent-uuid>",
private_key_pem=open("agent-key.pem").read(),
)
options = ClaudeAgentOptions(
hooks=firstops_hooks(fo), # ← every tool call governed by FirstOps
allowed_tools=["Bash", "Write", "Read"],
permission_mode="bypassPermissions",
)
4. (Optional) Add MCP servers through the FirstOps proxy#
Point the MCP server at the FirstOps proxy. FirstOps brokers the upstream credentials — the agent never holds the Notion/GitHub/Slack token.
options = ClaudeAgentOptions(
hooks=firstops_hooks(fo),
mcp_servers={
"notion": {"type": "http", "url": firstops.mcp_url("<notion-connection-id>")},
},
allowed_tools=["Write", "mcp__notion"],
permission_mode="bypassPermissions",
)
Full example — MCP + built-in tool, all governed#
This agent fetches customer records from Notion (MCP) and writes them to a local file (the built-in Write tool). The single PreToolUse hook governs both.
import asyncio
import firstops
from firstops.integrations.claude import firstops_hooks
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def main():
fo = firstops.init(
agent_id="<agent-uuid>",
private_key_pem=open("agent-key.pem").read(),
)
try:
options = ClaudeAgentOptions(
hooks=firstops_hooks(fo), # ← the only FirstOps line: governs every tool
mcp_servers={
"notion": {"type": "http", "url": firstops.mcp_url("<notion-connection-id>")},
},
allowed_tools=["Write", "Read", "mcp__notion"],
permission_mode="bypassPermissions",
)
prompt = (
"Use the Notion tools to find the customer database and fetch the "
"customer records, then write them to customers.txt."
)
# Iterate the stream to run the agent; print the final result.
async for message in query(prompt=prompt, options=options):
if isinstance(message, ResultMessage):
print(message.result)
finally:
firstops.shutdown()
asyncio.run(main())
Every Notion MCP call and every built-in tool call is evaluated against your policies — block a tool, scrub PII from its arguments, or audit — without changing the agent logic.
Interactive Claude Code / Claude Desktop#
For interactive use (not the SDK), govern Claude Code under your own identity with the FirstOps daemon:
fo auth login
fo setup
This routes Claude Code's MCP connections and tool actions through FirstOps. See Human-Delegated Agents.
What gets enforced#
Every policy on the agent's access group applies to every tool call — including Claude's automatic tool selection and MCP tools. A deny stops the tool before it runs; a modify rewrites its input.