Skip to main content
Custom permission handlers let you programmatically approve or deny tool executions with custom logic.

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