Basic Permission Handler
import { query, type CanUseTool } from '@qwen-code/sdk';
const canUseTool: CanUseTool = async (toolName, input) => {
// Allow all read operations
if (toolName.startsWith('read_') || toolName === 'list_directory') {
return { behavior: 'allow', updatedInput: input };
}
// Deny all write operations
return {
behavior: 'deny',
message: `Write operation ${toolName} is not allowed`,
};
};
const result = query({
prompt: 'Analyze the codebase structure',
options: {
permissionMode: 'default',
canUseTool,
},
});
for await (const message of result) {
if (message.type === 'assistant') {
console.log(message.message.content);
}
}
Path-Based Permissions
import { query, type CanUseTool } from '@qwen-code/sdk';
import * as path from 'path';
const ALLOWED_DIRECTORIES = ['src/', 'tests/', 'docs/'];
const canUseTool: CanUseTool = async (toolName, input) => {
// Check file path for file operations
if (toolName === 'write_file' || toolName === 'edit' || toolName === 'delete_file') {
const filePath = input.path as string;
// Check if path is within allowed directories
const isAllowed = ALLOWED_DIRECTORIES.some(dir =>
filePath.startsWith(dir)
);
if (!isAllowed) {
return {
behavior: 'deny',
message: `Cannot modify files outside of: ${ALLOWED_DIRECTORIES.join(', ')}`,
};
}
// Also check for dangerous patterns
if (filePath.includes('..')) {
return {
behavior: 'deny',
message: 'Path traversal detected',
};
}
}
// Allow safe operations
return { behavior: 'allow', updatedInput: input };
};
const result = query({
prompt: 'Refactor the code in the src directory',
options: {
permissionMode: 'default',
canUseTool,
},
});
Interactive Permission Requests
import { query, type CanUseTool } from '@qwen-code/sdk';
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 canUseTool: CanUseTool = async (toolName, input, { signal }) => {
// Auto-approve read operations
if (toolName.startsWith('read_')) {
return { behavior: 'allow', updatedInput: input };
}
// Ask user for write operations
console.log('\n--- Permission Request ---');
console.log('Tool:', toolName);
console.log('Input:', JSON.stringify(input, null, 2));
const approved = await askUser('Allow this operation?');
if (approved) {
return { behavior: 'allow', updatedInput: input };
}
return {
behavior: 'deny',
message: 'User denied permission',
};
};
const result = query({
prompt: 'Create a new configuration file',
options: {
permissionMode: 'default',
canUseTool,
},
});
for await (const message of result) {
if (message.type === 'assistant') {
console.log('\nAssistant:', message.message.content);
}
}
Logging All Tool Executions
import { query, type CanUseTool } from '@qwen-code/sdk';
import * as fs from 'fs/promises';
const LOG_FILE = 'tool-executions.log';
const canUseTool: CanUseTool = async (toolName, input) => {
// Log all tool requests
const logEntry = {
timestamp: new Date().toISOString(),
tool: toolName,
input: input,
approved: true,
};
// Append to log file
await fs.appendFile(
LOG_FILE,
JSON.stringify(logEntry) + '\n',
'utf-8'
);
// Allow all operations (but logged)
return { behavior: 'allow', updatedInput: input };
};
const result = query({
prompt: 'Modify the project structure',
options: {
permissionMode: 'default',
canUseTool,
},
});
for await (const message of result) {
if (message.type === 'result') {
console.log('All tool executions logged to', LOG_FILE);
}
}
Modifying Tool Input
import { query, type CanUseTool } from '@qwen-code/sdk';
const SAFE_DIR = '/tmp/safe-workspace';
const canUseTool: CanUseTool = async (toolName, input) => {
if (toolName === 'write_file') {
// Redirect all writes to safe directory
const originalPath = input.path as string;
const safePath = `${SAFE_DIR}/${originalPath}`;
console.log(`Redirecting write from ${originalPath} to ${safePath}`);
return {
behavior: 'allow',
updatedInput: {
...input,
path: safePath,
},
};
}
return { behavior: 'allow', updatedInput: input };
};
const result = query({
prompt: 'Create several configuration files',
options: {
permissionMode: 'default',
canUseTool,
},
});
Shell Command Filtering
import { query, type CanUseTool } from '@qwen-code/sdk';
const ALLOWED_COMMANDS = [
'git status',
'git diff',
'npm test',
'npm run lint',
'ls',
];
const canUseTool: CanUseTool = async (toolName, input) => {
if (toolName === 'run_terminal_cmd') {
const command = input.command as string;
// Check if command starts with allowed prefix
const isAllowed = ALLOWED_COMMANDS.some(allowed =>
command.startsWith(allowed)
);
if (!isAllowed) {
return {
behavior: 'deny',
message: `Command not allowed. Allowed commands: ${ALLOWED_COMMANDS.join(', ')}`,
};
}
}
return { behavior: 'allow', updatedInput: input };
};
const result = query({
prompt: 'Check git status and run tests',
options: {
permissionMode: 'default',
canUseTool,
},
});
Rate Limiting
import { query, type CanUseTool } from '@qwen-code/sdk';
class RateLimiter {
private operations: Map<string, number[]> = new Map();
private maxPerMinute = 10;
canExecute(toolName: string): boolean {
const now = Date.now();
const timestamps = this.operations.get(toolName) || [];
// Remove timestamps older than 1 minute
const recentTimestamps = timestamps.filter(
ts => now - ts < 60000
);
if (recentTimestamps.length >= this.maxPerMinute) {
return false;
}
recentTimestamps.push(now);
this.operations.set(toolName, recentTimestamps);
return true;
}
}
const rateLimiter = new RateLimiter();
const canUseTool: CanUseTool = async (toolName, input) => {
if (!rateLimiter.canExecute(toolName)) {
return {
behavior: 'deny',
message: `Rate limit exceeded for ${toolName}`,
interrupt: false,
};
}
return { behavior: 'allow', updatedInput: input };
};
const result = query({
prompt: 'Perform multiple file operations',
options: {
permissionMode: 'default',
canUseTool,
},
});
Conditional Permissions Based on Context
import { query, type CanUseTool } from '@qwen-code/sdk';
interface PermissionContext {
isProduction: boolean;
userRole: 'admin' | 'developer' | 'viewer';
}
function createPermissionHandler(context: PermissionContext): CanUseTool {
return async (toolName, input) => {
// Production restrictions
if (context.isProduction) {
if (toolName === 'delete_file' || toolName.includes('delete')) {
return {
behavior: 'deny',
message: 'File deletion not allowed in production',
interrupt: true,
};
}
}
// Role-based permissions
if (context.userRole === 'viewer') {
if (!toolName.startsWith('read_')) {
return {
behavior: 'deny',
message: 'Viewers can only perform read operations',
};
}
}
if (context.userRole === 'developer') {
// Developers can read and write, but not delete
if (toolName === 'delete_file') {
return {
behavior: 'deny',
message: 'Developers cannot delete files',
};
}
}
// Admins and allowed operations proceed
return { behavior: 'allow', updatedInput: input };
};
}
const result = query({
prompt: 'Modify the application',
options: {
permissionMode: 'default',
canUseTool: createPermissionHandler({
isProduction: false,
userRole: 'developer',
}),
},
});
Handling Abort Signals
import { query, type CanUseTool } from '@qwen-code/sdk';
const canUseTool: CanUseTool = async (toolName, input, { signal }) => {
// Simulate async permission check
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
resolve({ behavior: 'allow', updatedInput: input });
}, 2000);
// Handle abort signal
signal.addEventListener('abort', () => {
clearTimeout(timeout);
reject(new Error('Permission check aborted'));
});
});
};
const abortController = new AbortController();
const result = query({
prompt: 'Perform operations',
options: {
permissionMode: 'default',
canUseTool,
abortController,
},
});
// Abort after 5 seconds
setTimeout(() => abortController.abort(), 5000);
try {
for await (const message of result) {
console.log(message);
}
} catch (error) {
console.error('Query aborted:', error);
}
Permission with Timeout
import { query, type CanUseTool } from '@qwen-code/sdk';
const canUseTool: CanUseTool = async (toolName, input) => {
// This handler is subject to SDK timeout (default: 60s)
// If it doesn't respond in time, the tool is auto-denied
console.log(`Checking permission for ${toolName}...`);
// Simulate slow permission check
await new Promise(resolve => setTimeout(resolve, 5000));
return { behavior: 'allow', updatedInput: input };
};
const result = query({
prompt: 'Execute operations',
options: {
permissionMode: 'default',
canUseTool,
timeout: {
canUseTool: 10000, // 10 second timeout
},
},
});
See Also
- Permission Modes - Built-in permission modes
- query() Function - Query options
- Abort Example - Aborting queries
