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:
- Tool permissions - Allow/reject/ask for specific tools
- Safe commands - Auto-approved command prefixes
- Guarded files - Protected file patterns
- 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:
- Production use - Can't trust all operations
- Shared environments - Multiple users, need controls
- Sensitive codebases - Credentials, secrets, etc.
- 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