Permissions - Trust and Safety

Level: Enhancement (beyond minimal agent) Prerequisites: Tool system


What This Adds

Multi-layer permission system controlling what the agent can do:

  1. Tool permissions - Allow/reject/ask for specific tools
  2. Safe commands - Auto-approved command prefixes
  3. Guarded files - Protected file patterns
  4. MCP permissions - Trust decisions for MCP servers

Permission Actions

Action Behavior
allow Permit without asking
reject Block the action
ask Prompt user for confirmation
delegate Delegate to external program

Safe Commands

Safe Read Commands (Always Allowed)

Read-only commands that never need confirmation:

const SAFE_READ_COMMANDS = [
  // File/Directory inspection
  "ls", "dir", "find", "cat", "head", "tail", "less", "more",
  "grep", "egrep", "fgrep", "tree", "file", "wc", "pwd", "stat",

  // System info
  "du", "df", "ps", "top", "htop", "uname", "whoami",
  "echo", "printenv", "id", "which", "whereis",
  "date", "cal", "uptime", "free",

  // Network inspection
  "ping", "dig", "nslookup", "host", "netstat", "ss",
  "lsof", "ifconfig", "ip",

  // Documentation
  "man", "info"
];

Safe Command Prefixes (Auto-Approved)

Commands starting with these prefixes are auto-approved:

const SAFE_COMMAND_PREFIXES = [
  // Build & test commands
  "go test", "go run", "go build", "go vet", "go fmt",
  "cargo test", "cargo run", "cargo build", "cargo check", "cargo fmt",
  "npm test", "npm run", "npm list", "npm outdated",
  "yarn test", "yarn run", "yarn list",
  "pnpm test", "pnpm run", "pnpm build", "pnpm check",
  "pytest", "jest", "mocha",
  "mvn test", "mvn verify",
  "gradle tasks", "gradle test",
  "dotnet test", "dotnet list",

  // Version/help commands
  "node -v", "node --version",
  "python -V", "python --version",
  "ruby -v", "ruby --version",
  "go version",
  "rustc --version",

  // Git read-only
  "git status", "git show", "git diff", "git grep",
  "git branch", "git tag", "git remote -v", "git log",
  "git rev-parse"
];

Command Evaluation Logic

function commandRequiresApproval(
  command: string,
  userAllowlist: string[]
): boolean {
  // User can allow everything with "*"
  if (userAllowlist.includes("*")) {
    return false;
  }

  const parsedCommands = parseShellCommand(command);

  return !parsedCommands.every(segment => {
    if (!segment) return true;

    const baseCommand = segment.split(" ")[0];

    // Check safe read commands
    if (SAFE_READ_COMMANDS.includes(baseCommand)) {
      return true;
    }

    // Check safe prefixes
    for (const prefix of SAFE_COMMAND_PREFIXES) {
      if (segment.startsWith(prefix)) {
        return true;
      }
    }

    // Check user allowlist
    for (const pattern of userAllowlist) {
      if (segment.startsWith(pattern)) {
        return true;
      }
    }

    return false;
  });
}

Guarded Files

Certain file patterns require explicit approval:

const GUARDED_FILE_PATTERNS = [
  "**/.env",
  "**/.env.*",
  "**/.envrc",
  "**/secrets.*",
  "**/credentials.*",
  "**/*_rsa",
  "**/*_dsa",
  "**/*_ed25519",
  "**/*.pem",
  "**/*.key",
  "**/*.p12",
  "**/*.pfx"
];

function isGuardedFile(path: string): boolean {
  return GUARDED_FILE_PATTERNS.some(pattern =>
    minimatch(path, pattern)
  );
}

Allowlist Override

Users can explicitly allow guarded files:

interface GuardedFilesConfig {
  allowlist: string[];
}

function checkGuardedFile(
  path: string,
  config: GuardedFilesConfig
): PermissionResult {
  // Allowlist bypasses guard
  if (config.allowlist.some(p => minimatch(path, p))) {
    return { permitted: true };
  }

  if (isGuardedFile(path)) {
    return {
      permitted: false,
      requiresApproval: true,
      reason: "File matches guarded pattern"
    };
  }

  return { permitted: true };
}

Thread-Level Allowlist

Permissions can be remembered per-thread:

interface Thread {
  allowedInputs: Record<string, boolean>;
}

function checkThreadAllowlist(
  thread: Thread,
  toolName: string,
  input: unknown
): PermissionResult | null {
  const key = JSON.stringify({ tool: toolName, input });

  if (thread.allowedInputs[key] !== undefined) {
    return {
      permitted: thread.allowedInputs[key],
      reason: "Previously approved/denied"
    };
  }

  return null;  // Not in allowlist, check other rules
}

function rememberDecision(
  thread: Thread,
  toolName: string,
  input: unknown,
  permitted: boolean
): void {
  const key = JSON.stringify({ tool: toolName, input });
  thread.allowedInputs[key] = permitted;
}

MCP Server Permissions

Trust Decisions

MCP servers require explicit trust:

interface MCPTrustEntry {
  serverName: string;
  specHash: string;  // Hash of server config for verification
  allow: boolean;
}

interface MCPPermissionRule {
  matches: {
    serverName?: string;
    toolName?: string;
  };
  action: "allow" | "reject";
}

Trust Evaluation Flow

1. MCP Server Discovered
         │
         ▼
2. Check mcpTrustedServers
   - Match by serverName + specHash
   - If found & allow=true → TRUSTED
   - If found & allow=false → REJECTED
         │
         ▼
3. Check workspace trust
   - If workspace.allowAllMcpServers=true → TRUSTED
         │
         ▼
4. Check mcpPermissions rules
   - First-match-wins
   - action: allow → TRUSTED
   - action: reject → REJECTED
         │
         ▼
5. Default: ASK USER
   - Prompt for decision
   - Optionally persist

Implementation

interface MCPPermissionContext {
  trustedServers: MCPTrustEntry[];
  workspaceAllowAll: boolean;
  permissionRules: MCPPermissionRule[];
}

function checkMCPPermission(
  serverName: string,
  specHash: string,
  context: MCPPermissionContext
): PermissionResult {
  // 1. Check explicit trust entries
  const trustEntry = context.trustedServers.find(
    e => e.serverName === serverName && e.specHash === specHash
  );

  if (trustEntry) {
    return { permitted: trustEntry.allow };
  }

  // 2. Check workspace-level trust
  if (context.workspaceAllowAll) {
    return { permitted: true };
  }

  // 3. Check permission rules
  for (const rule of context.permissionRules) {
    if (matchesRule(rule.matches, serverName)) {
      return { permitted: rule.action === "allow" };
    }
  }

  // 4. Default: require approval
  return {
    permitted: false,
    requiresApproval: true,
    reason: "MCP server not trusted"
  };
}

Dangerous Mode

For users who want no prompts:

interface PermissionConfig {
  dangerouslyAllowAll: boolean;  // Default: false
}

function checkPermission(
  toolName: string,
  args: unknown,
  config: PermissionConfig
): PermissionResult {
  // Dangerous mode bypasses all checks
  if (config.dangerouslyAllowAll) {
    return { permitted: true };
  }

  // Normal permission evaluation...
}

Warning: This should require explicit acknowledgment of risks.


Restricted Configurations

Some settings can only be set in user settings, never workspace:

const RESTRICTED_CONFIGS = [
  "mcpServers",
  "mcpPermissions",
  "guardedFiles.allowlist",
  "tools.disable",
  "permissions"
];

function validateConfig(
  config: Config,
  source: "user" | "workspace"
): Config {
  if (source === "workspace") {
    for (const key of RESTRICTED_CONFIGS) {
      if (config[key] !== undefined) {
        console.warn(`Ignoring restricted config "${key}" from workspace`);
        delete config[key];
      }
    }
  }
  return config;
}

Permission Evaluation Order

First-match-wins evaluation:

async function evaluatePermission(
  toolName: string,
  args: unknown,
  context: PermissionContext
): Promise<PermissionResult> {
  // 1. Check dangerous mode
  if (context.config.dangerouslyAllowAll) {
    return { permitted: true };
  }

  // 2. Check thread-level allowlist
  const threadResult = checkThreadAllowlist(context.thread, toolName, args);
  if (threadResult) return threadResult;

  // 3. Check tool-specific rules
  const toolResult = await checkToolPermission(toolName, args, context);
  if (toolResult) return toolResult;

  // 4. Check global permission rules
  for (const rule of context.permissionRules) {
    if (matchesRule(rule, toolName, args)) {
      return { permitted: rule.action === "allow" };
    }
  }

  // 5. Default: tool-specific default
  return getToolDefaultPermission(toolName);
}

User Prompts

When approval is required, prompt clearly:

interface PermissionPrompt {
  tool: string;
  description: string;
  args: Record<string, unknown>;
  risks: string[];
  options: ["Allow", "Allow Always", "Deny", "Deny Always"];
}

async function promptForPermission(
  prompt: PermissionPrompt
): Promise<PermissionDecision> {
  // Show prompt to user with clear description of what
  // will happen and any risks

  const decision = await showPrompt(prompt);

  // Handle "always" options by remembering
  if (decision === "Allow Always" || decision === "Deny Always") {
    rememberDecision(decision.startsWith("Allow"));
  }

  return {
    permitted: decision.startsWith("Allow"),
    remember: decision.includes("Always")
  };
}

When to Add Permissions

Add permission system when:

  1. Production use - Can't trust all operations
  2. Shared environments - Multiple users, need controls
  3. Sensitive codebases - Credentials, secrets, etc.
  4. MCP servers - External integrations need trust model

For simple personal use, dangerouslyAllowAll may be acceptable.


Configuration Example

{
  "permissions": [
    {
      "matches": { "tool": "Bash", "command": "rm -rf *" },
      "action": "reject"
    },
    {
      "matches": { "tool": "Bash", "command": "git push" },
      "action": "ask"
    },
    {
      "matches": { "tool": "edit_file", "path": "**/.env*" },
      "action": "ask"
    }
  ],
  "commands.allowlist": [
    "make build",
    "make test"
  ],
  "guardedFiles.allowlist": [
    ".env.example"
  ],
  "mcpPermissions": [
    {
      "matches": { "serverName": "github-mcp" },
      "action": "allow"
    }
  ]
}

Enhancement based on Amp Code v0.0.1769212917 patterns