vet_command_chain
Scans chained shell commands for destructive fragments (e.g., rm -rf) hidden after &&, ||, or ;, escalating severity because operators may miss them on quick review.
Instructions
Vet a chained / multi-statement shell command — same rules as vet_command, but escalates LOW→MEDIUM and MEDIUM→HIGH because destructive fragments nested deep inside a chain (after &&, ;, or |) are easier for the operator to overlook on a quick read. Use this for any command containing &&, ||, ;, or piped subshells. The exact failure mode this targets: r/LocalLLaMA 'one bash permission slipped' (1.5k upvotes) — agent proposed a chained command, operator pattern-matched the lede, missed rm -rf deep in the chain.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| command | Yes | The chained shell command to vet |
Implementation Reference
- src/bash_vet_mcp/server.py:85-108 (registration)Tool registration in list_tools() — defines the 'vet_command_chain' tool with its name, description, and inputSchema (command string required).
Tool( name="vet_command_chain", description=( "Vet a chained / multi-statement shell command — same rules as " "`vet_command`, but escalates LOW→MEDIUM and MEDIUM→HIGH because " "destructive fragments nested deep inside a chain (after `&&`, `;`, " "or `|`) are easier for the operator to overlook on a quick read. " "Use this for any command containing &&, ||, ;, or piped subshells. " "The exact failure mode this targets: r/LocalLLaMA 'one bash " "permission slipped' (1.5k upvotes) — agent proposed a chained " "command, operator pattern-matched the lede, missed `rm -rf` deep " "in the chain." ), inputSchema={ "type": "object", "properties": { "command": { "type": "string", "description": "The chained shell command to vet", }, }, "required": ["command"], }, ), - src/bash_vet_mcp/server.py:134-136 (handler)Tool dispatch handler in call_tool() — routes 'vet_command_chain' to vet_command(command, command_chain=True).
if name == "vet_command_chain": command = str(arguments.get("command", "")) return _serialize(vet_command(command, command_chain=True)) - src/bash_vet_mcp/scanner.py:420-491 (handler)Core scanner function vet_command() with command_chain parameter — when True, escalates LOW→MEDIUM and MEDIUM→HIGH severity findings.
def vet_command(command: str, *, command_chain: bool = False) -> CommandVetReport: """Scan a shell command for destructive patterns. Returns a CommandVetReport. `command_chain=True` raises severity by one level for chained commands (because nested destructive fragments are easier to overlook on quick read). """ if not command.strip(): return CommandVetReport( verdict=Verdict.UNVERIFIED, risk_score=0, finding_count=0, findings=[], summary="No command provided.", parse_error=None, ) parsed_ok, parse_error = _try_bashlex_parse(command) findings = _scan_with_regex(command) # If chain mode + we have findings, escalate any LOW/MEDIUM by one tier (because # nested patterns in chained commands are easier to overlook on quick read). if command_chain and findings: escalated: list[CommandFinding] = [] for f in findings: if f.severity == Severity.LOW: escalated.append(f.model_copy(update={"severity": Severity.MEDIUM})) elif f.severity == Severity.MEDIUM: escalated.append(f.model_copy(update={"severity": Severity.HIGH})) else: escalated.append(f) findings = escalated # Sort by severity desc, then position asc severity_rank = {Severity.CRITICAL: 4, Severity.HIGH: 3, Severity.MEDIUM: 2, Severity.LOW: 1, Severity.INFO: 0} findings.sort(key=lambda f: (-severity_rank[f.severity], f.position or 0)) score = _risk_score(findings) verdict = _verdict_from_findings(findings) if not parsed_ok and not findings: # Can't parse + nothing matched regex — be honest return CommandVetReport( verdict=Verdict.UNVERIFIED, risk_score=0, finding_count=0, findings=[], summary="Could not parse the input as bash; no regex rules matched either. Inspect manually.", parse_error=parse_error, ) if not findings: summary = "No destructive patterns detected. Command appears safe to execute." elif verdict == Verdict.BLOCK: worst = findings[0] summary = ( f"BLOCK — {len(findings)} finding(s); worst is {worst.severity.upper()} " f"({worst.rule_id}): {worst.description}" ) elif verdict == Verdict.REVIEW: summary = f"REVIEW — {len(findings)} medium-severity finding(s). Sandbox-test or pair-review before running." else: # CAUTION summary = f"CAUTION — {len(findings)} low-severity finding(s). Likely safe but document if intentional." return CommandVetReport( verdict=verdict, risk_score=score, finding_count=len(findings), findings=findings, summary=summary, parse_error=parse_error if not parsed_ok else None, ) - src/bash_vet_mcp/types.py:58-70 (schema)CommandVetReport model — response schema shared by vet_command and vet_command_chain.
class CommandVetReport(BaseModel): """Response for `vet_command` and `vet_command_chain`.""" model_config = ConfigDict(frozen=True) verdict: Verdict risk_score: int """0–100. Severity-weighted: CRITICAL=40, HIGH=15, MEDIUM=5, LOW=1, INFO=0; capped at 100.""" finding_count: int findings: list[CommandFinding] summary: str parse_error: str | None = None """Set if the input wasn't parseable as bash. Verdict will be UNVERIFIED.""" - src/bash_vet_mcp/scanner.py:442-451 (helper)Chain-mode escalation logic — iterates findings and bumps LOW→MEDIUM, MEDIUM→HIGH when command_chain=True.
if command_chain and findings: escalated: list[CommandFinding] = [] for f in findings: if f.severity == Severity.LOW: escalated.append(f.model_copy(update={"severity": Severity.MEDIUM})) elif f.severity == Severity.MEDIUM: escalated.append(f.model_copy(update={"severity": Severity.HIGH})) else: escalated.append(f) findings = escalated