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
The Anthropic Agent SDK is not Claude Code. Claude Code is a terminal application with its own hook integration (see Claude Code integration). The Agent SDK is a TypeScript library for building agents in your own infrastructure. This page covers the Agent SDK.
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.
Limitations
- Only handlers wrapped by
governHandlersorgovernedare covered. Dispatching atool_useblock to a handler outside this pattern bypasses governance. - LLM calls go direct to Anthropic. ACP governs tools and actions, not tokens. For per-user LLM cost attribution, pair with Portkey or a similar proxy.
- 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.
- Pre-release.
@agenticcontrolplane/governance-anthropic@0.2.xis a breaking rewrite around the hook-only pattern. Previous0.1.xused a thicker loop wrapper — if you were on that, see the migration note in the package README.