The Claude Agent SDK provides powerful permission controls that allow you to manage how Claude uses tools in your application.
This guide covers how to implement permission systems using the canUseTool callback, hooks, and settings.json permission rules. For complete API documentation, see the TypeScript SDK reference.
The Claude Agent SDK provides four complementary ways to control tool usage:
Use cases for each approach:
canUseTool - Dynamic approval for uncovered cases, prompts user for permissionProcessing Order: PreToolUse Hook → Deny Rules → Allow Rules → Ask Rules → Permission Mode Check → canUseTool Callback → PostToolUse Hook
Permission modes provide global control over how Claude uses tools. You can set the permission mode when calling query() or change it dynamically during streaming sessions.
The SDK supports four permission modes, each with different behavior:
| Mode | Description | Tool Behavior |
|---|---|---|
default | Standard permission behavior | Normal permission checks apply |
plan | Planning mode - no execution | Claude can only use read-only tools; presents a plan before execution (Not currently supported in SDK) |
acceptEdits | Auto-accept file edits | File edits and filesystem operations are automatically approved |
bypassPermissions | Bypass all permission checks | All tools run without permission prompts (use with caution) |
You can set the permission mode in two ways:
Set the mode when creating a query:
import { query } from "@anthropic-ai/claude-agent-sdk";
const result = await query({
prompt: "Help me refactor this code",
options: {
permissionMode: 'default' // Standard permission mode
}
});Change the mode during a streaming session:
acceptEdits)In accept edits mode:
Auto-approved operations:
bypassPermissions)In bypass permissions mode:
Permission modes are evaluated at a specific point in the permission flow:
bypassPermissions mode - If active, allows all remaining toolscanUseTool callbackcanUseTool callback - Handles remaining casesThis means:
bypassPermissions modebypassPermissions mode overrides the canUseTool callback for unmatched toolsExample of mode progression:
// Start in default mode for controlled execution
permissionMode: 'default'
// Switch to acceptEdits for rapid iteration
await q.setPermissionMode('acceptEdits')The canUseTool callback is passed as an option when calling the query function. It receives the tool name and input parameters, and must return a decision- either allow or deny.
canUseTool fires whenever Claude Code would show a permission prompt to a user, e.g. hooks and permission rules do not cover it and it is not in acceptEdits mode.
Here's a complete example showing how to implement interactive tool approval:
The AskUserQuestion tool allows Claude to ask the user clarifying questions during a conversation. When this tool is called, your canUseTool callback receives the questions and must return the user's answers.
When canUseTool is called with toolName: "AskUserQuestion", the input contains:
{
questions: [
{
question: "Which database should we use?",
header: "Database",
options: [
{ label: "PostgreSQL", description: "Relational, ACID compliant" },
{ label: "MongoDB", description: "Document-based, flexible schema" }
],
multiSelect: false
},
{
question: "Which features should we enable?",
header: "Features",
options: [
{ label: "Authentication", description: "User login and sessions" },
{ label: "Logging", description: "Request and error logging" },
{ label: "Caching", description: "Redis-based response caching" }
],
multiSelect: true
}
]
}Return the answers in updatedInput.answers as a record mapping question text to the selected option label(s):
return {
behavior: "allow",
updatedInput: {
questions: input.questions, // Pass through original questions
answers: {
"Which database should we use?": "PostgreSQL",
"Which features should we enable?": "Authentication, Caching"
}
}
}Multi-select answers are comma-separated strings (e.g., "Authentication, Caching").
import { query } from "@anthropic-ai/claude-agent-sdk";
// Create an async generator for streaming input
async function* streamInput() {
yield {
type: 'user',
message: {
role: 'user',
content: "Let's start with default permissions"
}
};
// Later in the conversation...
yield {
type: 'user',
message: {
role: 'user',
content: "Now let's speed up development"
}
};
}
const q = query({
prompt: streamInput(),
options: {
permissionMode: 'default' // Start in default mode
}
});
// Change mode dynamically
await q.setPermissionMode('acceptEdits');
// Process messages
for await (const message of q) {
console.log(message);
}import { query } from "@anthropic-ai/claude-agent-sdk";
async function promptForToolApproval(toolName: string, input: any) {
console.log("\n🔧 Tool Request:");
console.log(` Tool: ${toolName}`);
// Display tool parameters
if (input && Object.keys(input).length > 0) {
console.log(" Parameters:");
for (const [key, value] of Object.entries(input)) {
let displayValue = value;
if (typeof value === 'string' && value.length > 100) {
displayValue = value.substring(0, 100) + "...";
} else if (typeof value === 'object') {
displayValue = JSON.stringify(value, null, 2);
}
console.log(` ${key}: ${displayValue}`);
}
}
// Get user approval (replace with your UI logic)
const approved = await getUserApproval();
if (approved) {
console.log(" ✅ Approved\n");
return {
behavior: "allow",
updatedInput: input
};
} else {
console.log(" ❌ Denied\n");
return {
behavior: "deny",
message: "User denied permission for this tool"
};
}
}
// Use the permission callback
const result = await query({
prompt: "Help me analyze this codebase",
options: {
canUseTool: async (toolName, input) => {
return promptForToolApproval(toolName, input);
}
}
});