Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Re-linked to current knowledge entries (version 2). The original 4 source IDs (098926ef, 2c1e4689, 54c33fa4, 4f51d11a) are no longer present in the active knowledge store. The skill body and behavior are unchanged. New source_knowledge_ids reference current lessons about lint-script false positives (testing) and refactoring guards to separate functions (architecture), both directly relevant to guardrail patterns.
Load this skill before modifying src/hooks/guardrails.ts — adding, removing, or changing guardrail blocks in checkDestructiveCommand(). It documents the pattern structure, known bypass surfaces, regex anti-patterns, and test conventions used across 41 guardrail test files and the ~3900-line guardrails.ts file.
When to load this skill
Load before any change to:
src/hooks/guardrails.ts — especially checkDestructiveCommand() or dcNormalizeCommand()
tests/unit/hooks/guardrails*.test.ts — any guardrail test file
Adding a new section (currently Sections 1–22) to checkDestructiveCommand()
Adding regex patterns for shell command blocking
Architecture overview
checkDestructiveCommand() — the shell command guard
Located at src/hooks/guardrails.ts (line 1304). This is the only function that blocks destructive shell commands. It is invoked by the toolBefore hook in guardrails.ts before every bash or shell tool call.
Per-segment loop — each segment evaluated against 22+ guardrail sections
dcValidateTargets() — runtime lstat-ancestor walk on destructive targets
Key normalization functions
Function
Line
What it normalizes
dcNormalizeCommand
~627
NFKC, caret escapes (^), backtick escapes, collapsed "" and ''
dcStripOneWrapper
~664
Detects/strips a single shell wrapper (bash/sh/zsh/pwsh/cmd/powershell/wsl etc)
dcUnwrapWrappers
~737
Loops dcStripOneWrapper until no more wrappers (max depth 10)
dcSplitSegments
~753
Splits on &&, ;, `
Known wrapper unwrapping limitation:sh -c and bash -c with single-quoted inner commands (sh -c 'mv ...') are NOT unwrapped because dcStripOneWrapper uses "? (optional double-quote). Only double-quoted inner commands are properly stripped.
Adding a new guardrail block
Step 1 — Determine the pattern placement
Inside checkDestructiveCommand(), the per-segment for loop evaluates each segment against sections 1–22. A new section should be added after the last existing section and before the closing } of the for loop (currently after Section 22 at approximately line 1733).
Step 2 — Choose the regex pattern structure
There are three patterns used in the codebase:
Pattern A — Simple inline regex (single condition):
// Good for: single-command blocking with no complex extractionif (/^blockedcommand\b.*\.swarm[\x5c/\s]?/i.test(seg)) {
thrownewError(`BLOCKED: "blockedcommand" targeting .swarm/ detected — ...`);
}
Pattern B — Multi-condition (flag check + path check):
// Good for: archive tools with flags + .swarm/ path (prevents argument-order bypass)if (
/^toolname\b.*--dangerous-flag\b/i.test(seg) &&
/\.swarm(?:[\x5c/\s]|$)/i.test(seg)
) {
This is recommended because it handles both tool --flag .swarm/path and tool .swarm/path --flag argument orders.
Pattern C — Argument extraction + stripped check:
// Good for: commands where you need argument isolation (e.g., `mv` with arg capture)if (/^\\?command\s/i.test(seg)) {
const match = seg.match(/^\\?command\s+(.+)$/i);
if (match) {
const argsStr = match[1].replace(/["']/g, '');
if (/\.swarm(?:[\x5c/\s]|$)/.test(argsStr)) {
thrownewError(`BLOCKED: ...`);
}
}
}
Step 3 — Handle all platform variants
POSIX, Windows cmd.exe, and PowerShell often use different commands for the same operation. All three must be covered:
Always use \x5c (backslash) for cross-platform path matching — \ alone is the regex escape character.
// Correct: matches both / and \
/\.swarm[\x5c/]/
// More complete: also matches .swarm followed by whitespace or end-of-string// (catches whole-directory targeting like `mv .swarm /tmp/`)/\.swarm(?:[\x5c/\s]|$)/
// Correct: catches both mv and \mvif (/^\\?mv\s/i.test(seg)) { ... }
// Correct for rm (uses \b instead of \s)if (/^\\?rm\b/i.test(seg)) { ... }
Known bypass surfaces (must document in adversarial tests)
These are documented bypass vectors that the current regex-based approach cannot fully close. Every new guardrail section should include adversarial tests for these patterns:
Evasion
Example
Status
Mitigation
Backslash prefix
\mv .swarm/file
CLOSED
Add ^\\? to command anchor
Quote splicing
m'v' .swarm/file
OPEN
Requires NFKC normalization change
Quoted command name
"mv" .swarm/file
OPEN
Requires NFKC normalization change
Shell wrapper (double-quoted)
sh -c "mv .swarm/file"
CLOSED
dcUnwrapWrappers handles "
Shell wrapper (single-quoted)
sh -c 'mv .swarm/file'
OPEN
dcUnwrapWrappers regex uses "?
Relative path prefix
mv ./swarm/file
OPEN
Requires path normalization
Env var expansion
mv $SWARM_DIR/file
OPEN
Requires variable resolution
Unicode fullwidth
mv .swarm/file
OPEN
Requires NFKC normalization
Regex anti-patterns (from prior bugs)
Anti-pattern 1: [^-] consuming path characters
// WRONG — [^-] consumes the first character of the pathif (/^rm\s+(?!\s*-)(?!-)[^-].*\.swarm/i.test(seg)) {
// "rm .swarm/file" → [^-] consumes '.' → "swarm/file" doesn't match "\.swarm"
}
// CORRECT — use negative lookahead for flag exclusionif (
/^rm\b/i.test(seg) &&
!/^rm\s+(?:-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)\b/i.test(seg) &&
/\.swarm(?:[\x5c/\s]|$)/i.test(seg)
) {
// "rm .swarm/file" → BLOCKED ✓// "rm -rf .swarm/" → Section 3 handles ✓// "rm -v .swarm/file" → BLOCKED ✓
}
Anti-pattern 2: Negative lookahead too broad ((?!-\S))
// WRONG — (?!-\S) excludes ALL flag-prefixed rm commands,// but Section 3 only catches recursive/force flagsif (/^rm\s+(?!-\S).*\.swarm/i.test(seg)) {
// "rm -v .swarm/file" → NOT blocked by S19 (excluded by lookahead)// "rm -v .swarm/file" → NOT blocked by S3 (no -r/-f flags)
}
// CORRECT — use three-part condition
Anti-pattern 3: .exec() confused by SAST
// WRONG — SAST confuses RegExp.prototype.exec() with child_process.exec()const match = /^command\s+(.+)$/i.exec(seg); // SAST false positive// CORRECT — use String.prototype.match()const match = seg.match(/^command\s+(.+)$/i); // No SAST false positive
Anti-pattern 4: Argument-order dependent patterns
// WRONG — .swarm/ must appear AFTER the flag in the command stringif (/^tool\b.*--flag\b.*\.swarm/i.test(seg)) {
// "tool --flag .swarm/" → BLOCKED ✓// "tool .swarm/ --flag" → NOT BLOCKED ✗
}
// CORRECT — split flag check and path checkif (/^tool\b.*--flag\b/i.test(seg) && /\.swarm(?:[\x5c/\s]|$)/i.test(seg)) {
// Both argument orders BLOCKED ✓
}