Anthropic Agent SDK + ACP — Governance Install Guide
The Anthropic SDK and Claude Agent SDK let you build tool-use loops around Claude. Out of the box, a production deployment attributes every tool call to one backend API key — no per-user policy, no per-user audit, no governance.
@agenticcontrolplane/governance-anthropic closes that gap. One call wraps your handler map; before each tool handler runs, ACP decides allow / deny / redact. Same governance model as Claude Code — same /govern/tool-use endpoint, same workspace policies.
Starter · 5-minute install.
npm install @agenticcontrolplane/governance-anthropic @anthropic-ai/sdk, wrap your handler map withgovernHandlers, bind the end user’s JWT per request viawithContext. See the governance model for the shared concepts across every framework, or the frameworks index for other options.
Disambiguation
Naming, because Anthropic’s lineup has shifted under this page: Claude Code is the terminal application with its own hook integration (see Claude Code integration). The Claude Agent SDK (@anthropic-ai/claude-agent-sdk, formerly the Claude Code SDK) is Claude Code’s engine as a library, with its own native PreToolUse hooks and canUseTool gate. This page covers the third thing: hand-rolled tool-use loops on the raw Anthropic SDK (@anthropic-ai/sdk), where you own the loop and governHandlers wraps your handlers. If you’re on the Claude Agent SDK, its native hooks accept the same ACP hook script as Claude Code.
Install
npm install @agenticcontrolplane/governance-anthropic @anthropic-ai/sdk
Minimal governed tool-use loop
import Anthropic from "@anthropic-ai/sdk";
import express from "express";
import { governHandlers, withContext } from "@agenticcontrolplane/governance-anthropic";
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const app = express();
app.use(express.json());
// Your tools — plain async handlers, your code, your credentials.
const handlers = governHandlers({
web_search: async ({ query }: { query: string }) => doSearch(query),
send_email: async ({ to, subject, body }: { to: string; subject: string; body: string }) =>
sendMail(to, subject, body),
});
const tools: Anthropic.Tool[] = [
{ name: "web_search", description: "Search the web",
input_schema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } },
{ name: "send_email", description: "Send email",
input_schema: { type: "object", properties: { to: { type: "string" }, subject: { type: "string" }, body: { type: "string" } }, required: ["to", "subject", "body"] } },
];
app.post("/run", async (req, res) => {
const token = req.header("authorization")!.slice("Bearer ".length);
await withContext({ userToken: token }, async () => {
const messages: Anthropic.MessageParam[] = [{ role: "user", content: req.body.prompt }];
for (let i = 0; i < 10; i++) {
const msg = await anthropic.messages.create({
model: "claude-sonnet-4-6", max_tokens: 1024, tools, messages,
});
messages.push({ role: "assistant", content: msg.content });
if (msg.stop_reason !== "tool_use") {
const text = msg.content.filter((b): b is Anthropic.TextBlock => b.type === "text").map(b => b.text).join("\n");
return res.json({ result: text });
}
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of msg.content) {
if (block.type !== "tool_use") continue;
const output = await handlers[block.name](block.input);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: typeof output === "string" ? output : JSON.stringify(output),
});
}
messages.push({ role: "user", content: toolResults });
}
res.status(500).json({ error: "max iterations" });
});
});
What governHandlers does
Takes Record<string, AsyncHandler>. Returns a handler map of the same shape where each function is wrapped to:
- POST to ACP
/govern/tool-usewith the tool name + input + user JWT. - If ACP denies, return
"tool_error: <reason>"— Claude sees the denial and adapts. - If ACP allows, run your handler.
- POST the output to
/govern/tool-outputfor audit + PII scan. - If ACP redacts, return the redacted version; if ACP blocks, return
"tool_error".
Drop-in — the map shape is preserved. Rest of your loop unchanged.
Fail-open
Network errors, timeouts (5s default), non-2xx responses → the tool proceeds with reason "fail-open". Matches Claude Code hook behavior. Governance is never a single point of failure for the agent.
Configure your ACP workspace
- An IdP configured — ACP verifies the end user’s JWT against your IdP. Dashboard → Settings → Identity Provider.
- Tools listed — names in your handler map must match tools enabled in your workspace. Dashboard → Policies → Tools.
- Policy per tier — set allow/deny, rate limits, PII mode. Dashboard → Policies.
What shows up in the dashboard
Every tool call appears in cloud.agenticcontrolplane.com/activity with:
- Actor — the end user’s sub
- Tool name — the key in your handler map
- Decision — allow / deny / redact, with reason
- Session — groups tool calls from one request
- Findings — PII detected in input or output
Anthropic SDK tool calls sit alongside Claude Code, Cursor, CrewAI, and LangChain calls from the same user. One audit log across every agent surface.
Adding a new tool
Three spots to edit:
- Push a schema entry to
tools— Claude uses this to decide when to call it. - Add the handler inside the
governHandlers({...})call. - Match the key between them.
Governance is automatic because the whole map is wrapped.
Alternative: wrap one handler at a time
If you don’t want to wrap the whole map, use governed from the core package:
import { governed } from "@agenticcontrolplane/governance";
const handlers = {
web_search: governed("web_search", async ({ query }) => doSearch(query)),
send_email: governed("send_email", async (input) => sendMail(input)),
};
Same result, handler-by-handler.
Price and meter the model calls
governed is the interception plane — what your agent does. The proxy
plane covers what it spends. Both run against the same gateway, and init()
wires them together:
import { init } from "@agenticcontrolplane/governance";
init(); // call before you construct any model client
Model calls now land on the same trail as tool calls, carrying real cost,
tokens, and cache economics. Constructing clients explicitly instead? Pass the
options rather than calling init():
import { modelClientOptions } from "@agenticcontrolplane/governance";
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(modelClientOptions("anthropic"));
Shapes are "anthropic", "openai" (chat completions), and
"openai-responses". The last two are not interchangeable — /v1 serves
chat completions, /openai/v1 serves responses.
init() is all-or-nothing per provider: it sets the base URL and the key, or
it leaves that provider completely alone and tells you which one it skipped. If
OPENAI_BASE_URL already points at your own gateway, ACP won’t silently
reroute you.
Your coverage: interception ✓ (decorated tools) · proxy ✓ (model calls).
TypeScript types and module boundaries
The full exported surface of @agenticcontrolplane/governance-anthropic, verbatim from its type declarations:
// The one function this adapter adds. Replaces each handler with a governed
// version running PreToolUse -> handler -> PostToolUse. Preserves map shape.
export declare function governHandlers<H extends ToolHandlerMap>(handlers: H): H;
export type ToolHandler = (input: any) => Promise<any>;
export type ToolHandlerMap = Record<string, ToolHandler>;
// Re-exported from the framework-agnostic core:
export { governed, withContext, configure, getContext } from "@agenticcontrolplane/governance";
export type {
Config, Decision, GovernanceContext, PostAction,
PostToolOutputResponse, PreToolUseResponse,
} from "@agenticcontrolplane/governance";
The decision contract those types carry — this is the whole policy surface:
type Decision = "allow" | "deny" | "ask";
type PostAction = "pass" | "redact" | "block";
interface PreToolUseResponse { decision: Decision; reason?: string }
interface PostToolOutputResponse {
action: PostAction;
modified_output?: string;
reason?: string;
findings?: { pii?: { types: string[] } };
}
Module boundaries. @agenticcontrolplane/governance owns identity, context propagation, the policy call, and audit emission — it is framework-agnostic and is the same core the Cursor, Codex, and other framework adapters use. @agenticcontrolplane/governance-anthropic owns only the Anthropic-shaped seam: mapping a ToolHandlerMap onto that core and understanding Anthropic’s tool_use / tool_result content blocks. No Anthropic specifics leak into the core, and no policy logic lives in the adapter.
Agent tier is part of the request contract (agent_tier: "interactive" | "subagent" | "background" | "api"), so one handler map can carry a stricter posture unattended than it does with a human watching.
If you want the loop written for you rather than dispatching yourself, runMessagesWithTools takes RunOptions (client, acp, agent, model, messages, tools, toolHandlers, plus optional system, maxIterations default 20, maxTokens default 4096) and returns a RunResult with finalContent, messages, iterations, and a truncated flag set when maxIterations was hit.
Limitations
- Only handlers wrapped by
governHandlersorgovernedare covered. Dispatching atool_useblock to a handler outside this pattern bypasses governance. - The two planes are wired separately. The decorator covers tool calls; the proxy covers model calls. Both ship, but each is its own edit — see Price and meter the model calls above. Decorating tools without repointing the model client gives you control with no cost data.
- Works with any tool-use loop. The package doesn’t impose a loop structure — it just wraps handlers. Use it with the raw Messages API, the Agent SDK, or your own custom runner.
- Version note. This guide documents the
0.2.xhook-only API —npm install @agenticcontrolplane/governance-anthropicgets it. Upgrading from0.1.x(the earlier loop-wrapper API)? The package README has the migration note.
Related
- Anthropic Agent SDK governance reference — the deep reference for this integration
- Governed agent recipes — four runnable Python agents built on this pattern
@agenticcontrolplane/governance-anthropicon npm@agenticcontrolplane/governance(core SDK)- Claude Code integration — same protocol, terminal-side hook
- CrewAI integration
- LangChain / LangGraph integration
- Tool policies and scopes