FirstOpsFirstOps
Documentation
Sign in

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_tools channel
  • MCP tools (mcp__<server>__<tool>) — mcp channel, with credential brokering through the FirstOps proxy
  • Each call returns allow, deny (the tool never runs), or modify (FirstOps rewrites the tool's input via updatedInput)

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.