How DesktopCommanderMCP Parses and Validates Shell Commands: A Deep Dive into CommandManager
DesktopCommanderMCP validates shell commands by recursively tokenizing command strings to extract base executables, normalizing them to remove paths and environment variables, and checking them against a configurable block-list that fails closed on any parsing error.
DesktopCommanderMCP is a Model Context Protocol (MCP) implementation that exposes desktop automation capabilities through secure shell execution. At the heart of its security model sits the CommandManager class in src/command-manager.ts, which implements a recursive descent parser capable of handling complex shell syntax—including subshells, command substitution, and chained commands—before validating them against user-defined restrictions.
The Two-Stage Parsing Pipeline
When a command string enters the system, CommandManager processes it through two distinct phases: extraction and normalization. This separation allows the validator to handle sophisticated shell constructs while maintaining a simple, predictable security interface.
Stage 1: Tokenization and Extraction with extractCommands()
The extractCommands() method (implemented in src/command-manager.ts) performs recursive tokenization to identify discrete executable units within a command string. It handles:
- Command separators: Semi-colons, double ampersands (
&&), double pipes (||), single pipes (|), and background operators (&) - Quoting rules: Single and double quotes with proper escape character handling
- Command substitution: Both
$()and backtick (`) syntax are recursively parsed to extract nested commands - Subshells: Parentheses
()are detected and parsed recursively when they appear outside quoted contexts
The parser iterates character-by-character while tracking state variables for quote characters (quoteChar), escape sequences (escaped), and depth for nested structures. When it encounters a separator, it flushes the accumulated currentCmd buffer through the normalization stage before proceeding.
Stage 2: Base Command Normalization with extractBaseCommand()
Once extracted, each command fragment passes through extractBaseCommand() to isolate the actual executable name. This method performs several normalization steps:
- Strip environment variables: Removes
KEY=valueassignments using the regex/\w+=\S+\s*/g - Tokenize: Splits on whitespace to identify the executable token
- Handle special prefixes: Skips dollar-prefixed tokens that are not command substitutions, ignores leading opening parentheses from subshell fragments, and recursively extracts commands from within
$()constructs - Extract basename: Uses
path.basenameto remove directory prefixes (e.g.,/usr/bin/sudobecomessudo) - Normalize case: Converts the final command name to lowercase for case-insensitive comparison
The method returns a deduplicated array of base command names ready for security validation.
Security Validation Logic
The validateCommand() method (lines 29-62 of src/command-manager.ts) serves as the security gate. It implements a fail-closed design pattern where any exception during validation results in denial of execution.
The validation flow proceeds as follows:
- Load configuration: Retrieves the
blockedCommandsarray fromconfigManager.getConfig() - Extract candidates: Calls
this.extractCommands(command)to get the normalized base commands - Fallback handling: If extraction returns an empty array, falls back to
getBaseCommand()for simple command strings - Block-list comparison: Iterates through extracted commands; if any match an entry in
blockedCommands, the method returnsfalse - Error handling: Any thrown exceptions are caught, logged via the
captureutility fromsrc/utils/capture.js, and result in afalsereturn value to prevent unauthorized execution
This architecture ensures that complex injection attempts—such as nested subshells or command substitutions containing blocked utilities—are caught regardless of how deeply they are obfuscated.
Practical Implementation Example
The following example demonstrates how to use the CommandManager for direct validation and debugging:
import { commandManager } from './command-manager.js';
// Simple command validation
await commandManager.validateCommand('ls -la');
// Returns: true (if "ls" is not in blockedCommands)
// Complex chained commands with substitution
await commandManager.validateCommand('git pull && echo $(whoami)');
// Returns: true unless "git" or "whoami" is blocked
// Blocked command inside nested subshell
await commandManager.validateCommand('$(rm -rf /tmp)');
// Returns: false if "rm" appears in blockedCommands
// Direct extraction for debugging
const cmds = commandManager.extractCommands('npm install && (git status)');
console.log(cmds);
// Output: ['npm', 'git']
The extraction logic correctly identifies both npm and git as separate executables despite the parenthetical grouping and chained execution operators.
Summary
extractCommands()recursively parses shell syntax—including quotes, escapes,$()substitutions, and subshells—to isolate discrete command strings.extractBaseCommand()normalizes each fragment by stripping environment variables, removing path prefixes, and extracting the lowercase basename of the executable.validateCommand()compares normalized commands against theblockedCommandsarray fromsrc/config-manager.ts, implementing a fail-closed security model that denies execution on any parsing error.- All validation errors are captured via
src/utils/capture.jsfor centralized telemetry and debugging.
Frequently Asked Questions
How does DesktopCommanderMCP handle command substitution like $() and backticks?
The parser detects both $() and backtick syntax during the character-by-character iteration in extractCommands(). When encountered, it recursively processes the content inside these delimiters, extracting any nested commands and subjecting them to the same validation rules as the parent command.
What happens if a command is not present in the block-list?
If an extracted base command does not match any entry in the blockedCommands configuration array, validateCommand() returns true, allowing the command to proceed to execution. The validation is explicitly opt-in blocking rather than opt-in allowing.
Where is the block-list configured, and what format does it use?
The block-list is managed by ConfigManager in src/config-manager.ts and retrieved via configManager.getConfig(). The expected format is an array of lowercase command strings (e.g., ["rm", "sudo", "mkfs"]) against which normalized base commands are compared.
Does the parser respect shell quoting rules for validation?
Yes. The extractCommands() method tracks quoting state using inQuote and quoteChar variables, ensuring that separators and special characters inside single or double quotes are treated as literal text rather than command delimiters. This prevents attackers from bypassing validation through clever quoting techniques.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →