Permission modes control how the SDK handles tool execution approval. They provide different levels of control over which operations the AI can perform automatically.
Permission Modes Overview
The SDK supports four built-in permission modes:
| Mode | Description | Use Case |
|---|
default | Write tools denied unless approved via canUseTool or allowedTools. Read-only tools execute automatically. | Most interactive applications |
plan | Blocks all write tools. AI presents a plan instead of executing. | Planning and analysis workflows |
auto-edit | Auto-approves edit tools (edit, write_file). Other tools require confirmation. | Code editing assistants |
yolo | All tools execute automatically without confirmation. | Trusted automation scripts |
Default Mode
The safest interactive mode. Write operations require explicit approval.
Behavior
- Read-only tools: Execute automatically (e.g.,
read_file, list_directory)
- Write tools: Denied unless approved via
canUseTool callback or in allowedTools
- Shell commands: Require approval
Example
import { query } from '@qwen-code/sdk';
const result = query({
prompt: 'Read package.json and create a backup',
options: {
permissionMode: 'default',
canUseTool: async (toolName, input) => {
// Read operations: auto-approve
if (toolName === 'read_file') {
return { behavior: 'allow', updatedInput: input };
}
// Write operations: ask user
const approved = await askUser(`Allow ${toolName} on ${input.path}?`);
if (approved) {
return { behavior: 'allow', updatedInput: input };
}
return { behavior: 'deny', message: 'User denied permission' };
},
},
});
If no canUseTool callback is provided, write tools are automatically denied:
const result = query({
prompt: 'Create a new file',
options: {
permissionMode: 'default',
// No canUseTool - write operations will be denied
},
});
Plan Mode
Blocks all write operations. The AI explains what it would do instead of executing.
Behavior
- Read-only tools: Execute normally
- Write tools: Blocked (AI instructed to present a plan)
- Special:
exit_plan_mode tool allowed (if available)
Example
const result = query({
prompt: 'How would you refactor this codebase?',
options: {
permissionMode: 'plan',
},
});
for await (const message of result) {
if (message.type === 'assistant') {
// AI will explain the refactoring plan instead of executing
console.log(message.message.content);
}
}
Use Cases
- Getting implementation plans before execution
- Code review and suggestions
- Architecture analysis
- Exploration without modification
Auto-Edit Mode
Auto-approves file editing operations while requiring confirmation for other tools.
Behavior
- Edit tools (
edit, write_file): Execute automatically
- Other write tools: Require approval via
canUseTool or allowedTools
- Read-only tools: Execute automatically
Example
const result = query({
prompt: 'Fix all TypeScript errors in the src/ directory',
options: {
permissionMode: 'auto-edit',
// File edits happen automatically
// Other operations (like shell commands) would need approval
},
});
With Additional Approvals
const result = query({
prompt: 'Fix errors and run tests',
options: {
permissionMode: 'auto-edit',
canUseTool: async (toolName, input) => {
// Edits already auto-approved by mode
// This only gets called for non-edit tools
if (toolName === 'run_terminal_cmd' && input.command?.startsWith('npm test')) {
return { behavior: 'allow', updatedInput: input };
}
return { behavior: 'deny', message: 'Not allowed' };
},
},
});
Use Cases
- Code refactoring assistants
- Automated fix tools
- Documentation generators
- Bulk code updates
YOLO Mode
Auto-approves all tools without confirmation. Use with extreme caution.
Behavior
- All tools: Execute automatically
- No callbacks:
canUseTool is never called
- No restrictions: Except tools in
excludeTools
YOLO mode grants the AI full access to file system operations and shell commands. Only use in trusted, isolated environments.
Example
const result = query({
prompt: 'Set up the project structure and install dependencies',
options: {
permissionMode: 'yolo',
cwd: '/safe/isolated/directory',
},
});
Safe YOLO with Exclusions
Combine with excludeTools to prevent dangerous operations:
const result = query({
prompt: 'Analyze and refactor the code',
options: {
permissionMode: 'yolo',
excludeTools: [
'delete_file',
'run_terminal_cmd',
'ShellTool(rm )',
'ShellTool(git push)',
],
},
});
Use Cases
- Automated CI/CD pipelines in isolated environments
- Batch processing scripts
- Development environment setup
- Testing and experimentation in sandboxes
Permission Priority Chain
Understanding the order of permission checks:
1. excludeTools -> Blocks completely (highest priority)
↓
2. permissionMode: 'plan' -> Blocks non-read-only tools
↓
3. permissionMode: 'yolo' -> Auto-approves all tools
↓
4. allowedTools -> Auto-approves matching tools
↓
5. canUseTool callback -> Custom approval logic
↓
6. Default behavior -> Auto-deny in SDK mode
Example with Multiple Controls
const result = query({
prompt: 'Modify the project',
options: {
permissionMode: 'default',
// Highest priority - always blocked
excludeTools: ['delete_file', 'ShellTool(rm )'],
// Auto-approve these tools
allowedTools: ['ShellTool(git status)', 'ShellTool(npm test)'],
// Custom logic for everything else
canUseTool: async (toolName, input) => {
// This won't be called for:
// - Tools in excludeTools (already blocked)
// - Tools in allowedTools (already approved)
// - Read-only tools (auto-approved in default mode)
if (toolName === 'write_file') {
return { behavior: 'allow', updatedInput: input };
}
return { behavior: 'deny', message: 'Not allowed' };
},
},
});
Custom Permission Handler
The canUseTool callback provides fine-grained control over tool execution.
Signature
type CanUseTool = (
toolName: string,
input: Record<string, unknown>,
options: {
signal: AbortSignal;
suggestions?: PermissionSuggestion[] | null;
}
) => Promise<PermissionResult>;
type PermissionResult =
| {
behavior: 'allow';
updatedInput: Record<string, unknown>;
}
| {
behavior: 'deny';
message: string;
interrupt?: boolean;
};
Parameters
Name of the tool requesting permission.
input
Record<string, unknown>
required
Input parameters for the tool.
Signal for cancelling the permission request.
options.suggestions
PermissionSuggestion[] | null
Suggested actions from the CLI (if available).
Return Value
Whether to allow or deny the tool execution.
Modified input parameters (only for 'allow'). Return the original input if no modifications needed.
Explanation for denial (only for 'deny').
Whether to interrupt the entire session on denial (only for 'deny').
Examples
canUseTool: async (toolName, input) => {
if (toolName === 'write_file') {
// Redirect all writes to a safe directory
return {
behavior: 'allow',
updatedInput: {
...input,
path: `/safe/dir/${input.path}`,
},
};
}
return { behavior: 'deny', message: 'Not allowed' };
}
Deny with Interrupt
canUseTool: async (toolName, input) => {
if (toolName === 'delete_file') {
return {
behavior: 'deny',
message: 'File deletion is not allowed',
interrupt: true, // Stop the entire session
};
}
return { behavior: 'allow', updatedInput: input };
}
Path-Based Approval
canUseTool: async (toolName, input) => {
if (toolName === 'write_file' || toolName === 'edit') {
const path = input.path as string;
// Only allow writes to specific directories
if (path.startsWith('src/') || path.startsWith('tests/')) {
return { behavior: 'allow', updatedInput: input };
}
return {
behavior: 'deny',
message: `Cannot modify files outside src/ and tests/: ${path}`,
};
}
return { behavior: 'allow', updatedInput: input };
}
Interactive Approval
import * as readline from 'readline';
function askUser(question: string): Promise<boolean> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(`${question} (y/n): `, (answer) => {
rl.close();
resolve(answer.toLowerCase() === 'y');
});
});
}
const result = query({
prompt: 'Modify the codebase',
options: {
permissionMode: 'default',
canUseTool: async (toolName, input) => {
// Auto-approve reads
if (toolName.startsWith('read_') || toolName === 'list_directory') {
return { behavior: 'allow', updatedInput: input };
}
// Ask user for writes
const approved = await askUser(
`Allow ${toolName}?\nInput: ${JSON.stringify(input, null, 2)}`
);
if (approved) {
return { behavior: 'allow', updatedInput: input };
}
return { behavior: 'deny', message: 'User denied permission' };
},
},
});
Auto-approve specific tools without calling canUseTool.
Pattern matching:
- Tool name:
'write_file', 'run_shell_command'
- Tool class:
'WriteTool', 'ShellTool'
- Shell command prefix:
'ShellTool(git status)'
options: {
permissionMode: 'default',
allowedTools: [
'read_file', // Specific tool
'ShellTool(git status)', // Git status commands
'ShellTool(npm test)', // npm test commands
'ShellTool(ls)', // ls commands
],
}
Block specific tools completely (highest priority).
options: {
permissionMode: 'yolo',
excludeTools: [
'delete_file', // Block file deletion
'ShellTool(rm )', // Block rm commands
'ShellTool(git push)', // Block git push
'run_terminal_cmd', // Block all terminal commands
],
}
Timeout Configuration
The canUseTool callback must respond within a timeout period (default: 60 seconds).
options: {
permissionMode: 'default',
canUseTool: async (toolName, input) => {
// Must respond within timeout
return { behavior: 'allow', updatedInput: input };
},
timeout: {
canUseTool: 120000, // 2 minutes
},
}
If the timeout is exceeded, the tool request is automatically denied with a timeout error.
Best Practices
1. Start with Default Mode
Use default mode with a custom canUseTool handler for most applications:
options: {
permissionMode: 'default',
canUseTool: async (toolName, input) => {
// Implement your approval logic
},
}
2. Use Plan Mode for Analysis
When you want suggestions without execution:
options: {
permissionMode: 'plan',
}
Use allowedTools and excludeTools to fine-tune any mode:
options: {
permissionMode: 'auto-edit',
allowedTools: ['ShellTool(git status)'],
excludeTools: ['delete_file', 'ShellTool(rm )'],
}
Always validate and sanitize tool inputs:
canUseTool: async (toolName, input) => {
if (toolName === 'write_file') {
const path = input.path as string;
// Validate path
if (path.includes('..') || path.startsWith('/')) {
return { behavior: 'deny', message: 'Invalid path' };
}
return { behavior: 'allow', updatedInput: input };
}
return { behavior: 'allow', updatedInput: input };
}
5. Log Permission Decisions
Log all permission decisions for audit trails:
canUseTool: async (toolName, input) => {
const decision = /* your logic */;
console.log({
timestamp: new Date().toISOString(),
toolName,
input,
decision: decision.behavior,
});
return decision;
}
See Also