Files
gh-jezweb-claude-skills-ski…/references/permissions-guide.md
2025-11-30 08:23:58 +08:00

8.2 KiB

Permissions Guide

Complete guide to permission control in Claude Agent SDK.


Permission Modes

Overview

Three built-in modes:

Mode Behavior Use Case
default Standard checks General use, production
acceptEdits Auto-approve file edits Trusted refactoring
bypassPermissions Skip ALL checks CI/CD, sandboxed envs

Default Mode

Standard permission checks.

options: {
  permissionMode: "default"
}

Prompts user for:

  • File writes/edits
  • Potentially dangerous bash commands
  • Sensitive operations

Auto-allows:

  • Read operations (Read, Grep, Glob)
  • Safe bash commands

Accept Edits Mode

Automatically approves file modifications.

options: {
  permissionMode: "acceptEdits"
}

Auto-approves:

  • File edits (Edit)
  • File writes (Write)

Still prompts for:

  • Dangerous bash commands
  • Sensitive operations

Use when: Refactoring, code generation workflows


Bypass Permissions Mode

⚠️ DANGER: Skips ALL permission checks.

options: {
  permissionMode: "bypassPermissions"
}

Auto-approves EVERYTHING:

  • All file operations
  • All bash commands
  • All tools

ONLY use in:

  • CI/CD pipelines
  • Sandboxed containers
  • Docker environments
  • Trusted, isolated contexts

NEVER use in:

  • Production systems
  • User-facing environments
  • Untrusted inputs

Custom Permission Logic

canUseTool Callback

type CanUseTool = (
  toolName: string,
  input: any
) => Promise<PermissionDecision>;

type PermissionDecision =
  | { behavior: "allow" }
  | { behavior: "deny"; message?: string }
  | { behavior: "ask"; message?: string };

Basic Example

options: {
  canUseTool: async (toolName, input) => {
    // Allow read-only
    if (['Read', 'Grep', 'Glob'].includes(toolName)) {
      return { behavior: "allow" };
    }

    // Deny dangerous bash
    if (toolName === 'Bash' && input.command.includes('rm -rf')) {
      return {
        behavior: "deny",
        message: "Destructive command blocked"
      };
    }

    // Ask for confirmation
    if (toolName === 'Write') {
      return {
        behavior: "ask",
        message: `Create ${input.file_path}?`
      };
    }

    return { behavior: "allow" };
  }
}

Common Patterns

Pattern 1: Block Destructive Commands

canUseTool: async (toolName, input) => {
  if (toolName === 'Bash') {
    const dangerous = [
      'rm -rf',
      'dd if=',
      'mkfs',
      '> /dev/',
      'shutdown',
      'reboot',
      'kill -9',
      'pkill'
    ];

    for (const pattern of dangerous) {
      if (input.command.includes(pattern)) {
        return {
          behavior: "deny",
          message: `Blocked dangerous command: ${pattern}`
        };
      }
    }
  }

  return { behavior: "allow" };
}

Pattern 2: Protect Sensitive Files

canUseTool: async (toolName, input) => {
  if (toolName === 'Write' || toolName === 'Edit') {
    const sensitivePaths = [
      '/etc/',
      '/root/',
      '.env',
      'credentials',
      'secrets',
      'config/production',
      '.ssh/',
      'private_key'
    ];

    for (const path of sensitivePaths) {
      if (input.file_path?.includes(path)) {
        return {
          behavior: "ask",
          message: `⚠️  Modify sensitive file: ${input.file_path}?`
        };
      }
    }
  }

  return { behavior: "allow" };
}

Pattern 3: Environment-Based Permissions

const environment = process.env.NODE_ENV; // 'development' | 'staging' | 'production'

canUseTool: async (toolName, input) => {
  // Production: require approval for everything
  if (environment === 'production') {
    if (toolName === 'Bash' || toolName === 'Write' || toolName === 'Edit') {
      return {
        behavior: "ask",
        message: `PRODUCTION: Approve ${toolName}?`
      };
    }
  }

  // Staging: auto-approve edits
  if (environment === 'staging') {
    if (toolName === 'Write' || toolName === 'Edit') {
      return { behavior: "allow" };
    }
  }

  // Development: allow most things
  if (environment === 'development') {
    return { behavior: "allow" };
  }

  return { behavior: "allow" };
}

Pattern 4: Deployment Confirmation

canUseTool: async (toolName, input) => {
  if (toolName === 'Bash') {
    const deploymentPatterns = [
      'deploy',
      'kubectl apply',
      'terraform apply',
      'helm install',
      'docker push',
      'npm publish'
    ];

    for (const pattern of deploymentPatterns) {
      if (input.command.includes(pattern)) {
        return {
          behavior: "ask",
          message: `🚀 DEPLOYMENT: ${input.command}\n\nProceed?`
        };
      }
    }
  }

  return { behavior: "allow" };
}

Pattern 5: Audit Logging

const auditLog: Array<{
  tool: string;
  input: any;
  decision: string;
  timestamp: Date;
}> = [];

canUseTool: async (toolName, input) => {
  // Log everything
  console.log(`[${new Date().toISOString()}] ${toolName}`);

  const decision = { behavior: "allow" as const };

  // Store audit log
  auditLog.push({
    tool: toolName,
    input,
    decision: decision.behavior,
    timestamp: new Date()
  });

  // Could also send to external service
  // await logToDatadog(toolName, input, decision);

  return decision;
}

Combining Modes with Custom Logic

options: {
  permissionMode: "acceptEdits",  // Auto-approve file edits
  canUseTool: async (toolName, input) => {
    // But still block dangerous bash
    if (toolName === 'Bash' && input.command.includes('rm -rf')) {
      return { behavior: "deny", message: "Blocked" };
    }

    // Custom logic runs AFTER permission mode
    return { behavior: "allow" };
  }
}

Security Best Practices

Do

  • Start with "default" mode
  • Use canUseTool for fine-grained control
  • Block known dangerous patterns
  • Require confirmation for sensitive ops
  • Log all tool usage for auditing
  • Test permission logic thoroughly
  • Use environment-based rules
  • Implement rate limiting if needed

Don't

  • Use "bypassPermissions" in production
  • Skip permission checks for "trusted" inputs
  • Allow arbitrary bash without filtering
  • Trust file paths without validation
  • Ignore audit logging
  • Assume AI won't make mistakes
  • Give blanket approvals

Testing Permissions

async function testPermissions() {
  const tests = [
    { tool: "Read", input: { file_path: "/etc/passwd" }, expectAllow: true },
    { tool: "Bash", input: { command: "rm -rf /" }, expectDeny: true },
    { tool: "Write", input: { file_path: ".env" }, expectAsk: true }
  ];

  for (const test of tests) {
    const decision = await canUseTool(test.tool, test.input);

    if (test.expectAllow && decision.behavior !== 'allow') {
      console.error(`FAIL: Expected allow for ${test.tool}`);
    }
    if (test.expectDeny && decision.behavior !== 'deny') {
      console.error(`FAIL: Expected deny for ${test.tool}`);
    }
    if (test.expectAsk && decision.behavior !== 'ask') {
      console.error(`FAIL: Expected ask for ${test.tool}`);
    }
  }
}

Common Scenarios

Allow Read-Only

canUseTool: async (toolName, input) => {
  if (['Read', 'Grep', 'Glob'].includes(toolName)) {
    return { behavior: "allow" };
  }
  return {
    behavior: "deny",
    message: "Read-only mode"
  };
}

Require Approval for All

canUseTool: async (toolName, input) => {
  return {
    behavior: "ask",
    message: `Approve ${toolName}?`
  };
}

Block Bash Entirely

canUseTool: async (toolName, input) => {
  if (toolName === 'Bash') {
    return {
      behavior: "deny",
      message: "Bash execution disabled"
    };
  }
  return { behavior: "allow" };
}

Error Handling

Handle permission errors gracefully:

for await (const message of response) {
  if (message.type === 'error') {
    if (message.error.type === 'permission_denied') {
      console.log('Permission denied for:', message.error.tool);
      // Continue with fallback or skip
    }
  }
}

For more details: See SKILL.md Template: templates/permission-control.ts