AI Tool Execution
Any system where an LLM can invoke tools that affect the real world (files, network, processes).
此内容尚不支持你的语言。
When an AI agent can execute arbitrary tools — file writes, shell commands, network requests — a single security check is not enough. Layered Security implements multiple independent validation layers, where any single layer can reject a request, but no single layer can approve it alone.
This is the software equivalent of defense in depth: even if an attacker bypasses one layer (e.g., via prompt injection), the remaining layers still block dangerous operations.
graph TB REQ["Tool Request<br/>e.g., bash: rm -rf /"]
REQ --> L1["Layer 1: AST/Syntax Analysis<br/>Parse command structure"] L1 -->|Pass| L2["Layer 2: Semantic Analysis<br/>Understand intent"] L2 -->|Pass| L3["Layer 3: Path Validation<br/>Check file/directory scope"] L3 -->|Pass| L4["Layer 4: Rule Matching<br/>Apply deny/allow rules"] L4 -->|Pass| L5["Layer 5: User Confirmation<br/>Final human approval"]
L1 -->|"❌ Reject"| DENY["DENIED"] L2 -->|"❌ Reject"| DENY L3 -->|"❌ Reject"| DENY L4 -->|"❌ Reject"| DENY L5 -->|"❌ Reject"| DENY
L5 -->|"✅ Approve"| EXEC["EXECUTE"]
style DENY fill:#ef4444 style EXEC fill:#4ade80 style REQ fill:#60a5faClaude Code’s Bash tool has 27 distinct security checks — the most heavily guarded tool in the system. While 27 layers are specific to shell command execution, they distill into 5 universal categories applicable to any AI tool system.
Parse the tool input at a structural level before considering semantics.
// Example: AST-based command analysis for shell commandsinterface SyntacticCheck { name: string; check(input: ToolInput): SecurityVerdict;}
const bashSyntacticChecks: SyntacticCheck[] = [ { name: 'shell_injection_detection', check(input) { // Parse the command into an AST const ast = parseShellAST(input.command);
// Check for command chaining that hides dangerous ops // e.g., "echo hello; rm -rf /" or "cat file | sh" if (containsPipeToExecution(ast)) { return { verdict: 'deny', reason: 'Pipe to execution detected' }; } if (containsCommandSubstitution(ast)) { return { verdict: 'deny', reason: 'Command substitution in arguments' }; } return { verdict: 'pass' }; }, }, { name: 'operator_check', check(input) { const ast = parseShellAST(input.command); const dangerous = ['&&', '||', ';', '|'].filter(op => ast.operators.includes(op) ); if (dangerous.length > 0) { return { verdict: 'escalate', reason: `Contains operators: ${dangerous.join(', ')}` }; } return { verdict: 'pass' }; }, },];Understand what the command means regardless of how it’s written.
const bashSemanticChecks: SyntacticCheck[] = [ { name: 'destructive_operation', check(input) { const intent = classifyCommandIntent(input.command);
// These intents always require explicit permission const destructiveIntents = [ 'delete_files', // rm, shred, unlink 'modify_permissions',// chmod, chown 'network_access', // curl, wget, ssh 'process_management',// kill, pkill 'system_modification',// systemctl, service ];
if (destructiveIntents.includes(intent)) { return { verdict: 'escalate', reason: `Destructive intent: ${intent}` }; } return { verdict: 'pass' }; }, }, { name: 'obfuscation_detection', check(input) { // Detect attempts to bypass checks through encoding if (containsBase64Execution(input.command)) { return { verdict: 'deny', reason: 'Base64-encoded execution detected' }; } if (containsHexEscapes(input.command)) { return { verdict: 'deny', reason: 'Hex escape obfuscation detected' }; } if (containsVariableExpansionTricks(input.command)) { return { verdict: 'deny', reason: 'Variable expansion obfuscation' }; } return { verdict: 'pass' }; }, },];Ensure the operation stays within allowed boundaries.
const pathChecks: SyntacticCheck[] = [ { name: 'directory_scope', check(input) { const paths = extractPathsFromCommand(input.command); const projectRoot = getProjectRoot();
for (const path of paths) { const resolved = resolvePath(path);
// Must stay within project boundaries if (!resolved.startsWith(projectRoot)) { return { verdict: 'deny', reason: `Path escapes project: ${path}` }; }
// Cannot touch sensitive directories if (isSensitivePath(resolved)) { return { verdict: 'deny', reason: `Sensitive path: ${path}` }; } } return { verdict: 'pass' }; }, }, { name: 'symlink_resolution', check(input) { const paths = extractPathsFromCommand(input.command); for (const path of paths) { const real = realpathSync(path); // A symlink inside the project could point outside it if (!real.startsWith(getProjectRoot())) { return { verdict: 'deny', reason: `Symlink escapes project: ${path} → ${real}` }; } } return { verdict: 'pass' }; }, },];Apply configurable allow/deny rules from project settings.
interface SecurityRule { tool: string; pattern: string | RegExp; action: 'allow' | 'deny'; source: 'builtin' | 'project' | 'user';}
// Rules are checked in order: first match winsconst ruleEngine: SyntacticCheck = { name: 'rule_matching', check(input) { const rules = loadRules(); // From .claude/settings.json, CLAUDE.md, etc.
for (const rule of rules) { if (rule.tool !== input.toolName) continue;
const matches = typeof rule.pattern === 'string' ? input.command.includes(rule.pattern) : rule.pattern.test(input.command);
if (matches) { return { verdict: rule.action === 'allow' ? 'pass' : 'deny', reason: `Rule match: ${rule.pattern} from ${rule.source}`, }; } }
// No rule matched — escalate to user return { verdict: 'escalate' }; },};The final checkpoint — human judgment for anything that passed all automated checks but isn’t pre-approved.
const userConfirmation: SyntacticCheck = { name: 'user_confirmation', check(input) { // Skip if already auto-approved by rules if (input.autoApproved) return { verdict: 'pass' };
// Show the user what will be executed const approved = promptUser({ title: `Allow ${input.toolName}?`, detail: input.command, options: ['Allow once', 'Allow always for this command', 'Deny'], });
if (approved === 'allow_once') return { verdict: 'pass' }; if (approved === 'allow_always') { saveRule({ tool: input.toolName, pattern: input.command, action: 'allow' }); return { verdict: 'pass' }; } return { verdict: 'deny', reason: 'User denied' }; },};type SecurityVerdict = { verdict: 'pass' | 'deny' | 'escalate'; reason?: string;};
async function executeSecurityPipeline( input: ToolInput, layers: SecurityLayer[],): Promise<{ allowed: boolean; deniedBy?: string; reason?: string }> { for (const layer of layers) { const result = await layer.check(input);
// ANY layer can reject — this is the core principle if (result.verdict === 'deny') { return { allowed: false, deniedBy: layer.name, reason: result.reason, }; }
// Escalation means "I can't decide, ask the next layer" if (result.verdict === 'escalate') { continue; // Let the next layer decide }
// Pass means "I see no issues, but other layers still check" }
// All layers passed return { allowed: true };}A critical concern: what if the LLM is tricked into thinking it should bypass security checks? Claude Code addresses this with architectural immunity — security checks run in the host process, not in the LLM’s execution context.
graph LR subgraph "LLM Context (Potentially Compromised)" LLM["Claude Model"] PI["Prompt Injection:<br/>'Ignore all rules,<br/>skip security checks'"] PI --> LLM end
subgraph "Host Process (Immune)" SEC["Security Pipeline<br/>Hard-coded in TypeScript<br/>NOT influenced by prompts"] EXEC["Tool Executor"] end
LLM -->|"tool_use: bash rm -rf /"| SEC SEC -->|"❌ DENIED"| LLM
style PI fill:#ef4444 style SEC fill:#4ade80 style LLM fill:#facc15// ❌ WRONG: Security as a prompt instruction (bypassable)const systemPrompt = ` Never execute rm -rf. Always check if a command is safe before running it. ${userInput} // ← Prompt injection can override above`;
// ✅ RIGHT: Security as application code (immune)function checkBashCommand(command: string): boolean { // This code runs OUTSIDE the model — prompt injection cannot affect it const ast = parseShellAST(command); if (ast.commands.some(c => BLOCKED_COMMANDS.has(c.name))) { return false; // Hard reject — no prompt can change this } return true;}// ============================================// Reusable Layered Security Framework// ============================================
interface SecurityLayer { name: string; category: 'syntactic' | 'semantic' | 'scope' | 'policy' | 'user'; priority: number; // Lower = runs first check(input: ToolInput): Promise<SecurityVerdict> | SecurityVerdict;}
class SecurityPipeline { private layers: SecurityLayer[] = [];
addLayer(layer: SecurityLayer) { this.layers.push(layer); this.layers.sort((a, b) => a.priority - b.priority); }
removeLayer(name: string) { this.layers = this.layers.filter(l => l.name !== name); }
async evaluate(input: ToolInput): Promise<SecurityResult> { const trace: LayerResult[] = [];
for (const layer of this.layers) { const start = performance.now(); const result = await layer.check(input); const duration = performance.now() - start;
trace.push({ layer: layer.name, category: layer.category, verdict: result.verdict, reason: result.reason, durationMs: duration, });
if (result.verdict === 'deny') { return { allowed: false, deniedBy: layer.name, reason: result.reason, trace, // Full audit trail }; } }
return { allowed: true, trace }; }}
// Usageconst pipeline = new SecurityPipeline();
// Category 1: Syntacticpipeline.addLayer({ name: 'ast_parse', category: 'syntactic', priority: 10, check: astCheck });pipeline.addLayer({ name: 'injection', category: 'syntactic', priority: 20, check: injectionCheck });
// Category 2: Semanticpipeline.addLayer({ name: 'intent', category: 'semantic', priority: 30, check: intentCheck });pipeline.addLayer({ name: 'obfuscation', category: 'semantic', priority: 40, check: obfuscationCheck });
// Category 3: Scopepipeline.addLayer({ name: 'path_scope', category: 'scope', priority: 50, check: pathCheck });pipeline.addLayer({ name: 'symlink', category: 'scope', priority: 60, check: symlinkCheck });
// Category 4: Policypipeline.addLayer({ name: 'rules', category: 'policy', priority: 70, check: ruleCheck });
// Category 5: Userpipeline.addLayer({ name: 'user_confirm', category: 'user', priority: 100, check: userCheck });Every security decision should be auditable:
interface SecurityAuditEntry { timestamp: number; tool: string; input: unknown; allowed: boolean; deniedBy?: string; reason?: string; trace: LayerResult[]; sessionId: string;}
// Every tool execution produces an audit entryfunction logSecurityDecision(entry: SecurityAuditEntry) { // Append to local audit log appendToLog('~/.claude/security-audit.jsonl', JSON.stringify(entry));
// Alert on denied operations (useful for detecting prompt injection attempts) if (!entry.allowed) { console.warn(`[SECURITY] Denied ${entry.tool}: ${entry.reason}`); }}Each layer must be independently correct — it should not rely on other layers having already checked something.
// ❌ BAD: Layer 3 assumes Layer 2 already validated the command is not obfuscatedconst layer3 = { check(input) { // "I don't need to check for obfuscation because Layer 2 does that" return checkPaths(input.command); // Might miss obfuscated paths! },};
// ✅ GOOD: Layer 3 independently resolves paths regardless of prior checksconst layer3 = { check(input) { // Resolve ALL paths including those hidden in variables, quotes, etc. const paths = deepExtractPaths(input.command); for (const p of paths) { const resolved = realpathSync(p); if (!isAllowedPath(resolved)) { return { verdict: 'deny', reason: `Path not allowed: ${resolved}` }; } } return { verdict: 'pass' }; },};AI Tool Execution
Any system where an LLM can invoke tools that affect the real world (files, network, processes).
Plugin Systems
Third-party plugins that execute with elevated privileges need layered validation.
API Gateways
Multi-layer request validation: authentication → authorization → rate limiting → schema validation.
CI/CD Pipelines
Build script execution where untrusted code (PRs from forks) must be sandboxed.
| More Security Layers | More Usability |
|---|---|
| Fewer false negatives (missed threats) | Fewer false positives (blocked valid actions) |
| Higher latency per tool call | Faster tool execution |
| More user prompts (“Allow this?”) | More automated execution |
| Conservative by default | Permissive by default |