Summary
Tool access control enforces the principle of least privilege for AI sub-agents by restricting which tools each agent can use and which files it can access. A backend engineer gets Read, Write, Edit, and Bash for its domain. A code reviewer gets only Read, Grep, and Glob with no write permissions. By granting each agent the minimum capabilities needed for its role, you prevent domain violations, accidental overwrites, and the common mistake of reviewers introducing bugs while “fixing” issues.
The Problem
When sub-agents have unrestricted tool access, several problems emerge:
Agents Escape Their Domain
A frontend engineer with full write access might “helpfully” fix a backend bug it noticed:
// Frontend engineer sees an issue in the API response
// With full Write access, it decides to "fix" the backend:
// packages/api/src/handlers/users.ts
// Agent adds a change that breaks the authentication flow
export async function getUser(req: Request, res: Response) {
const user = await userService.getUser(req.params.id);
// Agent added this "fix" - but broke the auth pattern
delete user.password; // ❌ Service should handle this, not handler
return res.json(user);
}
The frontend agent meant well, but violated the layer architecture. Backend changes should come from the backend engineer who understands the domain patterns.
Write-Capable Reviewers Introduce Bugs
The most dangerous anti-pattern: giving code reviewers write access.
// Code reviewer finds an issue and decides to fix it directly:
// Original code (correct but reviewer misunderstands)
const users = await db.query(`SELECT * FROM users WHERE id = $1`, [userId]);
// Reviewer "fixes" it (introduces SQL injection!)
const users = await db.query(`SELECT * FROM users WHERE id = '${userId}'`);
// ❌ Reviewer introduced a vulnerability while trying to help
Reviewers lack the full context of implementation decisions. When they “fix” code, they often break things the original author understood.
Uncontrolled Side Effects
An agent with Bash access can bypass file restrictions:
# Agent restricted to packages/api/** for file writes
# But Bash isn't restricted...
# Agent runs:
cat > packages/ui/src/components/Header.tsx << 'EOF'
// Frontend component written by backend agent
// Doesn't follow frontend patterns
export function Header() {
return <div>Header</div>;
}
EOF
# ❌ Bypassed file restrictions through Bash
Security Risks
Overly permissive access creates security vulnerabilities:
// Agent has full Bash access
// Task: "Check if the API is running"
// Agent runs:
bash: curl -X POST http://admin:admin@localhost/admin/reset-database
// ❌ Destructive operation - no guardrails
The Solution
Apply the principle of least privilege: each agent receives only the tools and paths necessary for its specific role.
Core Restriction Dimensions
Tool access control operates on three dimensions:
interface AgentPermissions {
// 1. Which tools can the agent invoke?
allowedTools: Tool[];
// 2. Which paths can the agent access?
allowedPaths: string[];
// 3. What operations are permitted?
operations: {
canRead: boolean;
canWrite: boolean;
canDelete: boolean;
canExecute: boolean; // Bash, shell commands
};
}
Standard Role Configurations
Backend Engineer
Full development capabilities within the backend domain:
const backendEngineer: AgentPermissions = {
allowedTools: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob'],
allowedPaths: [
'packages/api/**',
'packages/domain/**',
'packages/database/**',
'prisma/**',
],
operations: {
canRead: true,
canWrite: true,
canDelete: false, // Prevent accidental file deletion
canExecute: true, // For migrations, schema generation
},
};
// Bash restrictions for backend
const backendBashWhitelist = [
'npm run',
'npx prisma',
'tsc --noEmit',
'jest',
];
Frontend Engineer
Development capabilities for UI work, typically no shell access needed:
const frontendEngineer: AgentPermissions = {
allowedTools: ['Read', 'Write', 'Edit', 'Grep', 'Glob'],
allowedPaths: [
'packages/ui/**',
'apps/web/**',
'apps/mobile/**',
],
operations: {
canRead: true,
canWrite: true,
canDelete: false,
canExecute: false, // No Bash - prevents build script manipulation
},
};
QA Engineer
Test-focused permissions with execution capabilities:
const qaEngineer: AgentPermissions = {
allowedTools: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob'],
allowedPaths: [
'**/*.test.ts',
'**/*.spec.ts',
'**/*.test.tsx',
'tests/**',
'e2e/**',
'__tests__/**',
],
operations: {
canRead: true, // Can read implementation to understand what to test
canWrite: true, // Can create test files
canDelete: false,
canExecute: true, // Must run tests
},
};
// QA can read any file but only write to test files
const qaReadPaths = ['**/*']; // Read everything
const qaWritePaths = ['**/*.test.*', '**/*.spec.*', 'tests/**']; // Write only tests
Code Reviewer (READ-ONLY)
The most restricted role, and for good reason:
const codeReviewer: AgentPermissions = {
allowedTools: ['Read', 'Grep', 'Glob'], // No Write, Edit, or Bash
allowedPaths: ['**/*'], // Can read everything
operations: {
canRead: true,
canWrite: false, // CRITICAL: Cannot modify code
canDelete: false,
canExecute: false,
},
};
Why read-only reviewers matter:
- No introduced bugs: Reviewer cannot accidentally break working code
- Clear responsibility: Issues are identified, not “fixed” in place
- Audit trail: All fixes go through the appropriate engineer
- No context loss: Original author makes informed fixes
Security Auditor
Similar to reviewer but with specific security focus:
const securityAuditor: AgentPermissions = {
allowedTools: ['Read', 'Grep', 'Glob'],
allowedPaths: ['**/*'],
operations: {
canRead: true,
canWrite: false,
canDelete: false,
canExecute: false,
},
};
// Security auditor gets additional context
const securityContext = `
Focus on:
- SQL injection vulnerabilities
- XSS attack vectors
- Authentication bypasses
- Secret exposure
- OWASP Top 10 issues
Report format:
- Severity: CRITICAL/HIGH/MEDIUM/LOW
- Location: file:line
- Issue: description
- Recommendation: how to fix
- DO NOT attempt to fix - report only
`;
Orchestrator
Coordinates work but minimizes direct file access:
const orchestrator: AgentPermissions = {
allowedTools: ['Read', 'Grep', 'Glob', 'Task'], // Can spawn sub-agents
allowedPaths: [
'CLAUDE.md',
'.claude/**',
'tasks/**',
'docs/**',
],
operations: {
canRead: true,
canWrite: false, // Orchestrator delegates, doesn't implement
canDelete: false,
canExecute: false,
},
};
Implementation Patterns
Pattern 1: Tool Whitelist at Agent Spawn
Pass allowed tools when creating sub-agents:
async function spawnSubAgent(
role: AgentRole,
task: string
): Promise<AgentResult> {
const permissions = getPermissionsForRole(role);
return await claude.complete({
system: buildAgentContext(role),
prompt: task,
allowedTools: permissions.allowedTools,
allowedPaths: permissions.allowedPaths,
});
}
// Usage
const reviewResult = await spawnSubAgent(
'code-reviewer',
'Review the payment implementation for security issues'
);
// Reviewer cannot accidentally modify code - tools restricted
Pattern 2: Path-Based Write Restrictions
Allow reads everywhere, restrict writes by domain:
interface PathPermissions {
readPaths: string[]; // Where agent can read
writePaths: string[]; // Where agent can write
}
const domainPermissions: Record<string, PathPermissions> = {
'backend-engineer': {
readPaths: ['**/*'], // Read everything for context
writePaths: ['packages/api/**', 'packages/domain/**'],
},
'frontend-engineer': {
readPaths: ['**/*'],
writePaths: ['packages/ui/**', 'apps/web/**'],
},
'qa-engineer': {
readPaths: ['**/*'],
writePaths: ['**/*.test.*', '**/*.spec.*', 'tests/**'],
},
};
Pattern 3: Bash Command Whitelist
Restrict shell access to specific commands:
const bashWhitelists: Record<string, string[]> = {
'backend-engineer': [
'npm run build',
'npm run lint',
'npx prisma generate',
'npx prisma migrate',
'tsc --noEmit',
'jest packages/api',
],
'qa-engineer': [
'npm test',
'npm run test:*',
'npx jest',
'npx playwright test',
],
'frontend-engineer': [], // No Bash access
'code-reviewer': [], // No Bash access
};
function validateBashCommand(role: string, command: string): boolean {
const whitelist = bashWhitelists[role] || [];
return whitelist.some(allowed =>
command.startsWith(allowed) ||
new RegExp(`^${allowed.replace('*', '.*')}$`).test(command)
);
}
Pattern 4: Role-Based Tool Registry
Centralize permission definitions:
// .claude/tool-permissions.ts
export const toolRegistry = {
roles: {
'backend-engineer': {
tools: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob'],
paths: {
read: ['**/*'],
write: ['packages/api/**', 'packages/domain/**', 'packages/database/**'],
},
bash: {
allowed: ['npm', 'npx', 'tsc', 'jest'],
blocked: ['rm -rf', 'curl', 'wget', 'ssh'],
},
},
'code-reviewer': {
tools: ['Read', 'Grep', 'Glob'],
paths: {
read: ['**/*'],
write: [], // Empty = no write access
},
bash: {
allowed: [],
blocked: ['*'], // Block all Bash
},
},
},
getPermissions(role: string): AgentPermissions {
const config = this.roles[role];
if (!config) {
throw new Error(`Unknown role: ${role}`);
}
return {
allowedTools: config.tools,
allowedPaths: config.paths.write,
readPaths: config.paths.read,
bashWhitelist: config.bash.allowed,
bashBlacklist: config.bash.blocked,
};
},
};
Advanced Patterns
Conditional Tool Access
Grant different tools based on task phase:
interface PhasePermissions {
planning: Tool[];
implementation: Tool[];
review: Tool[];
}
const backendPhases: PhasePermissions = {
planning: ['Read', 'Grep', 'Glob'], // Research only
implementation: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob'],
review: ['Read', 'Grep', 'Glob'], // Back to read-only
};
async function executeWithPhase(
phase: keyof PhasePermissions,
task: string
): Promise<AgentResult> {
const tools = backendPhases[phase];
return await claude.complete({
system: `Phase: ${phase}. Available tools: ${tools.join(', ')}`,
prompt: task,
allowedTools: tools,
});
}
Escalation Requests
Allow agents to request elevated permissions through the orchestrator:
interface EscalationRequest {
agent: string;
requestedTool: Tool;
reason: string;
scope: string; // Specific file or directory
}
async function handleEscalation(request: EscalationRequest): Promise<boolean> {
// Log the request for audit
await logEscalation(request);
// Orchestrator (or human) reviews
const approved = await reviewEscalation(request);
if (approved) {
// Grant temporary elevated access
await grantTemporaryAccess(request.agent, request.requestedTool, {
scope: request.scope,
duration: '5m', // Time-limited
});
}
return approved;
}
// Agent requests escalation
const escalation: EscalationRequest = {
agent: 'qa-engineer',
requestedTool: 'Write',
reason: 'Need to update test fixtures in packages/api/fixtures/',
scope: 'packages/api/fixtures/**',
};
Audit Logging
Track all tool usage for debugging and compliance:
interface ToolUsageLog {
timestamp: Date;
agent: string;
tool: Tool;
arguments: Record<string, unknown>;
path?: string;
result: 'success' | 'denied' | 'error';
denialReason?: string;
}
async function logToolUsage(log: ToolUsageLog): Promise<void> {
await appendToFile('.claude/audit.log', JSON.stringify(log));
// Alert on suspicious patterns
if (log.result === 'denied') {
await alertOnDenial(log);
}
}
// Log output example:
// {"timestamp":"2026-01-28T10:00:00Z","agent":"code-reviewer","tool":"Write","path":"packages/api/src/auth.ts","result":"denied","denialReason":"Role code-reviewer does not have Write permission"}
Common Pitfalls
Pitfall 1: Write-Capable Reviewers
The most common and dangerous mistake:
// ❌ WRONG: Reviewer can modify code
const badReviewer = {
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob'],
// Reviewer will "helpfully" fix issues directly
// Often introduces new bugs or breaks patterns
};
// ✅ CORRECT: Read-only reviewer
const goodReviewer = {
tools: ['Read', 'Grep', 'Glob'],
// Can only report issues, not modify code
// Fixes go through appropriate engineer
};
Pitfall 2: Overly Broad Path Patterns
// ❌ WRONG: Too broad
const badPaths = {
backendEngineer: {
writePaths: ['**/*'], // Can write anywhere!
},
};
// ✅ CORRECT: Domain-specific
const goodPaths = {
backendEngineer: {
writePaths: ['packages/api/**', 'packages/domain/**'],
},
};
Pitfall 3: Unrestricted Bash Access
// ❌ WRONG: Agent can bypass file restrictions via Bash
const badConfig = {
tools: ['Read', 'Write', 'Bash'],
writePaths: ['packages/api/**'],
// But Bash can write anywhere with shell redirection!
};
// ✅ CORRECT: Whitelist Bash commands
const goodConfig = {
tools: ['Read', 'Write', 'Bash'],
writePaths: ['packages/api/**'],
bashWhitelist: ['npm run', 'tsc', 'jest'],
// Shell commands that don't involve file writing
};
Pitfall 4: Forgetting Task Tool Restrictions
Sub-agents can spawn their own sub-agents unless restricted:
// ❌ WRONG: Agent can spawn unrestricted sub-agents
const badConfig = {
tools: ['Read', 'Write', 'Task'], // Can spawn any sub-agent
};
// ✅ CORRECT: Restrict Task tool or remove it
const goodConfig = {
tools: ['Read', 'Write'], // No Task tool
// Or restrict spawnable agent types:
taskRestrictions: {
allowedAgentTypes: ['qa-engineer'], // Can only spawn QA
},
};
Benefits
1. Domain Integrity
Agents stay in their lane:
Backend changes: Backend engineer only
Frontend changes: Frontend engineer only
Test files: QA engineer only
Code reviews: Read-only reviewer
Result: No cross-domain contamination
2. Reduced Bug Introduction
Read-only reviewers catch issues without adding new ones:
Without restrictions:
- Reviewer finds 5 issues
- Reviewer "fixes" 3 issues
- Reviewer introduces 2 new bugs
- Net improvement: +1 (barely)
With restrictions:
- Reviewer finds 5 issues
- Reviewer reports all 5
- Original author fixes all 5 correctly
- Net improvement: +5
3. Clear Audit Trail
All modifications are traceable to the appropriate agent:
packages/api/src/auth.ts:
- Line 15: Backend engineer (commit abc123)
- Line 30: Backend engineer (commit def456)
Clear accountability, easy debugging
4. Security Posture
Principle of least privilege limits blast radius:
If backend agent is compromised:
- Can only affect packages/api/**, packages/domain/**
- Cannot access secrets in other directories
- Cannot execute arbitrary shell commands
- Damage is contained
Related
- Sub-Agent Architecture – Team structure that tool access supports
- Sub-Agent Context Hierarchy – Context isolation complements tool restrictions
- Actor-Critic Adversarial Coding – Read-only critics are essential
- Making Invalid States Impossible – Tool restrictions prevent invalid state transitions
- Boundary Enforcement with Layered Architecture – Tool restrictions enforce layer boundaries
- Hierarchical Context Patterns – Context and tools work together for isolation
- DDD Bounded Contexts for LLMs – Domain boundaries map to tool permissions
References
- Principle of Least Privilege – Security concept underlying tool restrictions
- Claude Code Tool Documentation – Official tool documentation
- OWASP Access Control Design Principles – Security best practices

