# Anthropic Agent SDK: Per-User Auth, Policy & Audit

The Anthropic Agent SDK attributes every tool call to one API key. Add per-user identity, policy, and audit with one wrapper around your handlers.

# Anthropic Agent SDK + ACP — Governance Install Guide

The [Anthropic SDK](https://github.com/anthropics/anthropic-sdk-typescript) and [Claude Agent SDK](https://docs.claude.com/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 with `governHandlers`, bind the end user's JWT per request via `withContext`. See [the governance model](/docs/governance-model) for the shared concepts across every framework, or the [frameworks index](/frameworks) 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](/integrations/claude-code)). 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

```bash
npm install @agenticcontrolplane/governance-anthropic @anthropic-ai/sdk
```

## Minimal governed tool-use loop

```ts
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:

1. POST to ACP `/govern/tool-use` with the tool name + input + user JWT.
2. If ACP denies, return `"tool_error: <reason>"` — Claude sees the denial and adapts.
3. If ACP allows, run your handler.
4. POST the output to `/govern/tool-output` for audit + PII scan.
5. 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

1. **An IdP configured** — ACP verifies the end user's JWT against your IdP. Dashboard → Settings → Identity Provider.
2. **Tools listed** — names in your handler map must match tools enabled in your workspace. Dashboard → Policies → Tools.
3. **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](https://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:

1. Push a schema entry to `tools` — Claude uses this to decide when to call it.
2. Add the handler inside the `governHandlers({...})` call.
3. 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:

```ts
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:

```ts
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()`:

```ts
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:

```ts
// 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:

```ts
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 `governHandlers` or `governed` are covered.** Dispatching a `tool_use` block 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](#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.x` hook-only API — `npm install @agenticcontrolplane/governance-anthropic` gets it. Upgrading from `0.1.x` (the earlier loop-wrapper API)? The [package README](https://github.com/agentic-control-plane/acp-governance-sdks/tree/main/packages/governance-anthropic) has the migration note.

## Related

- [Anthropic Agent SDK governance reference](/blog/anthropic-agent-sdk-governance-reference) — the deep reference for this integration
- [Governed agent recipes](/series/governed-recipes) — four runnable Python agents built on this pattern
- [`@agenticcontrolplane/governance-anthropic` on npm](https://www.npmjs.com/package/@agenticcontrolplane/governance-anthropic)
- [`@agenticcontrolplane/governance` (core SDK)](https://www.npmjs.com/package/@agenticcontrolplane/governance)
- [Claude Code integration](/integrations/claude-code) — same protocol, terminal-side hook
- [CrewAI integration](/integrations/crewai)
- [LangChain / LangGraph integration](/integrations/langgraph)
- [Tool policies and scopes](/docs/policies)

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "Govern Anthropic Agent SDK with Agentic Control Plane",
  "totalTime": "PT5M",
  "step": [
    {"@type": "HowToStep", "name": "Install the SDK", "text": "npm install @agenticcontrolplane/governance-anthropic @anthropic-ai/sdk"},
    {"@type": "HowToStep", "name": "Wrap your handlers", "text": "Pass your handler map through governHandlers({...})."},
    {"@type": "HowToStep", "name": "Bind the user's JWT per request", "text": "Call withContext({ userToken }, async () => { ... }) around your tool-use loop."}
  ]
}
</script>
