# Claude Agent SDK

Source: https://permdock.com/docs/adapters/claude-agent

permdock/claude-agent answers Claude Agent SDK canUseTool callbacks and PermissionRequest hooks from PermDock decisions, mapping built-in tools such as Bash to typed permissions.

`permdock/claude-agent` plugs PermDock into the two permission surfaces of the Claude Agent SDK: the `canUseTool` callback that runs before every tool invocation, and the `PermissionRequest` hook that fires when the SDK is about to ask a human. Both are answered from one `Decision`, so the same policy that guards an HTTP route decides whether an agent may run a shell command.

## Purpose [#purpose]

The Claude Agent SDK exposes tool use to the host application as a callback: the SDK proposes a tool call, the host allows or denies it, optionally rewriting the input. Without a policy layer, hosts hard-code tool names and argument patterns in that callback. `permdock/claude-agent` replaces the hard-coding with a `tools` map from SDK tool names to permission references, and applies PermDock's [subject](/docs/concepts/subject) model so the agent acts as an `actor` under the user's authority and never exceeds it. The result is the same three-outcome mapping used by [permdock/ai-sdk](/docs/adapters/ai-sdk) and [permdock/mcp](/docs/adapters/mcp), with a human approval path where a grant says `approval: 'human'`.

## API [#api]

```ts
import { createPermDock } from "permdock/claude-agent";
import { z } from "zod";

const PostArgs = z.object({ id: z.string() }); // tool input arrives as `unknown`

const { canUseTool, permissionRequestHook } = createPermDock(policy, {
  subject: () => currentUser,
  actor: () => ({ id: "claude-agent", kind: "claude-agent" }),
  delegation: () => ({
    scopes: [permissions.post.read.scope, permissions.post.update.scope],
  }),
  tools: {
    Bash: {
      permission: permissions.shell.run,
      data: (input) => ({ command: input.command }),
    },
    Write: {
      permission: permissions.file.write,
      data: (input) => ({ path: input.file_path }),
    },
    Read: {
      permission: permissions.file.read,
      data: (input) => ({ path: input.file_path }),
    },
    mcp__posts__delete_post: {
      permission: permissions.post.delete,
      data: (input) => loadPost(PostArgs.parse(input).id),
    },
  },
});

query({
  prompt,
  options: {
    canUseTool,
    hooks: { PermissionRequest: [{ hooks: [permissionRequestHook] }] },
  },
});
```

* `tools` keys are SDK tool names, including built-ins (`Bash`, `Read`, `Write`, `Edit`, `WebFetch`) and MCP tools by their prefixed name. Each entry names a permission and, for instance-level actions, a `data` resolver that turns the tool input into the resource shape the permission's schema expects.
* `subject` and `actor` are resolved once per `query` unless they are functions of the call context; `delegation` states what the user handed the agent (the scopes of scoped credentials, or the permissions the host chooses) and has no default: an `actor` with no `delegation` is denied every tool with reason `no-delegation`.
* `canUseTool` and `permissionRequestHook` are ready to pass to the SDK and take its real shapes: `canUseTool(toolName, input, { signal, mcpServer, agentID, toolUseID })` and the `PermissionRequest` hook input (`hook_event_name`, `session_id`, `agent_id`, `tool_name`, `tool_input`, `mcp_server`). The hook returns `{}` for any other event.
* `mcpSources` (default `['sdk']`) lists the MCP server sources whose tools are trusted. An `mcp__` tool is denied when the SDK reports no server provenance, when the server's `source` is not listed, or when the tool name does not start with `mcp__<server name>__`; trust keys on the source, never on the name alone.
* `store` (an `ApprovalStore`), `sink`, `limits`, `memberships`, `customRoles` and `tenant` behave as on every adapter.

## Request lifecycle [#request-lifecycle]

1. The model proposes a tool call. The SDK invokes `canUseTool(toolName, input, context)`.
2. The adapter looks up `toolName` in `tools`. Unmapped tools are denied (fail closed) with a message naming the tool.
3. If `data` is declared, the input is projected into the resource shape and validated against the resource schema (boundary mode).
4. `permdock.decide(permission, data)` runs.
5. The outcome maps to the SDK's return value:

| Decision outcome | `canUseTool` result |
| --- | --- |
| `granted` | allow, with the input passed back unchanged as `updatedInput` |
| `denied` | deny, with a message built from denials and alternatives |
| `approval-required` | deny, with a message naming the permission, the resource and `Approval <token> is pending; retry the call once it is approved.`; the pending request is written to the `store` |

6. A reviewer resolves the request through `approvalsHandler`, `resolveApproval` or the PermDock Cloud inbox. When the agent retries the same call, the adapter recomputes the token from permission, resource, principal, actor and input, finds the approved record in the store, consumes it and allows the call once; a replay is denied with `approval-consumed`. `permissionRequestHook` answers the SDK's own prompt with the same decision, so the SDK never asks a human to allow what PermDock denied or has not yet approved.
7. `on('decision')` fires for every step, including the human's answer, for audit and `permdock/otel`.

## What it validates [#what-it-validates]

* Tool input against the permission's resource schema after `data` projection (`validate: 'boundary'`). For `Bash`, a `shell.run` resource can carry `command` so `where` conditions such as an allow-listed prefix are expressed as portable conditions, not regexes in the callback.
* Subject provenance: the subject comes from the host process, never from the conversation or tool input.
* Approval replays: the token is recomputed on every retry and must match an approved record in the store; an approval resumes one call, and a replay is denied with `approval-consumed`.
* MCP provenance: `mcp__` tools only from trusted `mcpSources` whose server name matches the tool prefix.
* Coverage in development: a warning lists SDK tools enabled for the session that are missing from `tools`, because those calls will be denied at runtime.

## How denials surface [#how-denials-surface]

* `denied` returns the SDK deny behaviour with a message such as `Denied: shell.run (command not allow-listed). You may: file.read, file.write.` The message is built from `Decision.denials` reasons and `alternatives` so the model can self-correct.
* `approval-required` is a deny whose message tells the model the call is waiting for approval and carries the token, so the model can tell the user and retry instead of treating it as a final refusal.
* Errors thrown by a `data` resolver or by the policy are caught, logged through `on('decision')` and reported as a deny; the agent loop never receives an unhandled exception from the permission layer.
* There is no pass-through or default-allow mode; the SDK's own permission modes remain in force on top of PermDock's answer.
* PermDock never rewrites tool input. Forcing a `cwd` for `Bash` or similar is host code, and a `shell.run` resource schema is defined by the application; the entry ships no built-in tool schemas.
* A pending approval lives in the `store`, so with a durable `ApprovalStore` it survives a restarted session: the retried call recomputes the same token and finds the record.

## Example app [#example-app]

`apps/examples/claude-agent`: HTTP harness on `127.0.0.1:3473` with `GET /health`. `GET /list_posts` calls `canUseTool` and returns `behavior: 'allow'`. `GET /delete_post` returns a deny carrying the pending approval token; after `POST /approvals?token=…` the next `GET /delete_post` is allowed once and the one after that is denied. No Anthropic API key; a full `query()` loop needs the Claude Code runtime and is not part of the test suite.

## Related standards [#related-standards]

* [Approvals](/docs/security/approvals): `approval: 'human'`, replay-safe `token`.
* [Delegation](/docs/security/delegation): the agent as `actor`, principal grants intersected with delegated authority.
* [OWASP Agentic Top 10](/docs/security/owasp-agentic): ASI02 Tool Misuse, ASI03 Identity and Privilege Abuse, least agency.
* [MCP authorization](/docs/standards/mcp-authorization): for MCP tools reached through the Claude Agent SDK.
