Build a Command Security Pipeline for Your Agent
Replace scattered if-else safety checks with a layered validator pipeline: single-purpose checks, an allow/ask/deny/passthrough contract, and severity-aware ordering.
The route
4 steps to Done
- 01
Define the contract and pipeline skeleton
A uniform result type is what makes 20 checks composable.
Preview prompt + verify gate ▾ Hide ▴
Define the validator contract: each validator is a pure function taking the parsed command context and returning a result of allow (confident-safe: short-circuit the pipeline), ask (needs user confirmation, with reason), deny (block, with reason), or passthrough (no opinion: next validator). Add a severity flag distinguishing parser-mismatch findings (the command will execute differently than it parses - must interrupt) from informational ones (redirections, newlines - confirm but do not panic). Build the pipeline runner: run early validators first (allow can short-circuit), then the main chain collecting results, then apply selection - any deny wins, else the highest-severity ask, else allow. Wire ALL execution paths through it, including sub-agents and background tasks.
- ✓ Contract covers allow/ask/deny/passthrough plus severity
- ✓ Early-allow short-circuit implemented
- ✓ No execution path bypasses the pipeline
- 02
Implement early validators and input hygiene
Cheap checks first: catch garbage and fast-path the common safe cases.
Preview prompt + verify gate ▾ Hide ▴
Implement the front of the pipeline. Input hygiene (block outright): control characters and null bytes in commands, and known parser bugs in your shell-parsing library (test its edge cases - a quoting bug in the parser is a security hole in you). Preprocessing: strip heredoc bodies safely so later checks see structure rather than payload text; extract quote variants of the command for later multi-view checks. Early allows: empty commands, and your environment's most common verified-safe patterns (a well-formed commit with heredoc message, a plain build command) - these short-circuit to allow so the main chain's cost stays bounded. Early asks: malformed or incomplete commands (leading dash or tab) that suggest a truncated generation.
- ✓ Control characters and parser edge cases blocked with tests
- ✓ Heredoc stripping and quote extraction implemented
- ✓ Common safe patterns short-circuit measured by pipeline timing
- 03
Build the main validator chain and hard path constraints
One attack class per validator, plus constraints nothing can override.
Preview prompt + verify gate ▾ Hide ▴
Implement single-purpose main-chain validators, each with its own tests: shell metacharacters that split commands (';', '|', '&' outside quotes), IFS-injection, redirection detection (informational severity), sub-command extraction for compound commands so each piece is checked (a benign-looking second command reading credentials is the classic leak). Then implement the hard constraints layer: a protected-path denylist (system roots, credential directories) that blocks removal operations regardless of any permission rule, correct handling of the '--' separator so flags cannot smuggle paths past extraction, and for your riskiest allowed tool (e.g. sed-like editors), a whitelist of safe forms plus a denylist of dangerous sub-features (write-file and execute commands, non-ASCII lookalikes, multiline programs). Keep validators stateless and independent.
- ✓ Each validator targets exactly one attack class
- ✓ Compound commands are split and each part validated
- ✓ Protected paths block removal even with explicit permission
- 04
Order by severity and regression-test with an attack corpus
The most severe finding must win, and stay winning.
Preview prompt + verify gate ▾ Hide ▴
Implement result selection precisely: deny beats everything; among asks, parser-mismatch severity beats informational (a command flagged for both redirection and an escaped-operator trick must report the trick - the finding that means 'this will not execute the way it parses'); informational asks are deferred and only surface if nothing worse fired. Build the attack corpus: every attack class from your validators plus real transcripts, each labeled with expected outcome and expected reporting validator. Add benign-command cases asserting allow/passthrough to keep false positives visible. Run the corpus in CI; any change to a validator must keep the whole corpus green. Document the known-false-positive policy (e.g. find -exec's escaped semicolon asks once) - deliberate asks are fine, undocumented ones erode trust.
- ✓ Deny > misparse-ask > informational-ask ordering tested
- ✓ Corpus covers every validator's attack class plus benign cases
- ✓ CI blocks on any corpus regression
Research-backed
Sources behind this flow
Tier 1 · claude-code-internals
Deep Dive: Code-Layer Security - 20+ Validators Behind BashTool
Walks the layered validation pipeline that backs up the prompt when the model ignores its 'suggestions': control-character checks, multi-view quote extraction, obfuscated-flag detection (ANSI-C quoting, empty-quote concatenation, quote chains, triple quotes), brace-expansion and backslash-escaped-operator traps, hard path constraints, a whitelist-plus-denylist sed validator, and per-tool exit-code semantics. Every validator returns allow/ask/deny/passthrough, and misparsing-class findings outrank informational ones.
Tier 1 · claude-code-internals
Deep Dive: Dual-Defense Architecture - Prompt + Code
Argues that because LLMs are probabilistic, prompts alone are gambling and code alone wastes the model's flexibility. Maps how Claude Code pairs every soft prompt rule ('use Edit instead of sed', 'NEVER force-push') with an independent deterministic check (sed validators, destructive-git detection, sandbox gates), analyzes the four failure combinations of the two layers, and distills five reusable design principles for any tool-calling agent system.