Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork565
Feature/destructive safety#225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading.Please reload this page.
Conversation
- Add readOnlyDirectories for protected paths- Add requireExplicitPermission flag for destructive commands- Add allowedSudoCommands array for sudo whitelist- Backward compatible with defaults (empty arrays, false flag)This commit adds configuration without changing behavior.
- Check readOnlyDirectories config before write operations- Protect system directories from modification- Clear error messages for protected paths- Empty array (default) maintains original behaviorPrevents accidental modification of critical system files.
CRITICAL SAFETY FEATURE:- Requires flag for: * rm -rf commands * find with -delete * Wildcard deletions (rm *.*) * Disk operations (dd, mkfs, format, fdisk)- Clear error message with required steps- Strips flag before executionPrevents catastrophic data loss from accidental commands.This single feature has prevented multiple disasters in production.
coderabbitaibot commentedAug 22, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
WalkthroughExtends ServerConfig with readOnlyDirectories, requireExplicitPermission, and allowedSudoCommands and sets defaults. Adds destructive-command gating and permission flag handling in TerminalManager. Introduces read-only directory enforcement in filesystem validatePath and propagates to write operations. Adds two Node-based test scripts for destructive protection and read-only checks. Changes
Sequence Diagram(s)sequenceDiagram autonumber actor User participant TM as TerminalManager participant CFG as ConfigManager participant SH as Shell User->>TM: executeCommand(cmd) TM->>CFG: getConfig() CFG-->>TM: config (requireExplicitPermission, ...) alt config requireExplicitPermission not false AND cmd matches destructive alt cmd includes permission flag TM->>TM: sanitize (remove flag) TM->>SH: spawn(sanitized cmd) SH-->>TM: result TM-->>User: result else blocked TM-->>User: "DESTRUCTIVE OPERATION BLOCKED" message end else not destructive or protection disabled TM->>TM: sanitize (remove flag if present) TM->>TM: enhance ssh (-t) if needed TM->>SH: spawn(sanitized/enhanced) SH-->>TM: result or pid error TM-->>User: result (pid -1 on spawn error) endsequenceDiagram autonumber actor Caller participant FS as filesystem.ts participant CFG as ConfigManager Caller->>FS: writeFile(path, data) FS->>FS: validatePath(path, isWriteOperation=true) FS->>CFG: getConfig() CFG-->>FS: {allowedDirs, readOnlyDirectories} alt path outside allowed FS-->>Caller: throw "Path not allowed" else within allowed alt path under read-only FS-->>Caller: throw "Path is read-only" else writable FS-->>Caller: write proceeds end end note over FS: Same check applied in createDirectory and moveFile (src & dest)Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from theIntegrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat withCodeRabbit:
SupportNeed help? Create a ticket on oursupport page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Actionable comments posted: 7
🧹 Nitpick comments (8)
src/config-manager.ts (1)
13-15:New config fields look reasonable, but one is currently unusedreadOnlyDirectories and requireExplicitPermission integrate with filesystem and terminal protections. However, allowedSudoCommands is introduced here but is not enforced anywhere in this PR. Consider either wiring it into the execution path (e.g., selectively allowing sudo when explicitly whitelisted) or removing it until implemented to avoid dead config.
Would you like me to wire this into TerminalManager so sudo is blocked except for an allowlist (with pattern support)?
test-readonly.js (1)
38-38:Restore config after test to avoid side effectsThe test permanently mutates readOnlyDirectories. Capture and restore the original config (or at least the mutated key) to avoid cross-test flakiness.
I can provide a tiny helper to snapshot and restore configManager keys if you want.
test-destructive.js (4)
21-26:Assert on structured isBlocked as well, not only the sentinel stringRelying solely on a string makes tests brittle. Also, TerminalManager currently returns isBlocked: false when blocking (bug flagged separately). Add an assertion on result1.isBlocked === true once that bug is fixed.
- if (result1.output.includes('DESTRUCTIVE OPERATION BLOCKED')) {+ if (result1.output.includes('DESTRUCTIVE OPERATION BLOCKED') && result1.isBlocked === true) { console.log('✅ Command correctly blocked\n'); } else { console.log('❌ ERROR: Command was not blocked!\n'); }
29-35:Also assert that the permission flag is stripped before executionTo ensure the flag isn’t passed to rm, verify that the output or error stream doesn’t contain the literal flag (or add a hook to expose the cleanCommand). This catches regressions where the flag leaks to subprocesses.
37-44:Add coverage for more destructive patterns and separatorsConsider adding scenarios:
- rm with mixed/ordered flags: rm -r -f /tmp/test-dir and rm -fr /tmp/test-dir
- Wildcard deletion: rm. (in a sandbox dir)
- find -exec rm -rf {} +
- Piped/ANDed commands: echo ok && rm -rf /tmp/test-dir
I can extend the test with safe sandboxes under os.tmpdir() and assert blocking across separators.
58-59:forceTerminate() requires a PID; no-op hereCalling manager.forceTerminate() without a pid is a no-op. Either remove this or track/terminate a known pid.
src/terminal-manager.ts (2)
99-101:Remove permission flag globally, not just the first occurrenceString.replace only removes the first instance. Use split/join to strip all, or a global regex.
- const cleanCommand = command.replace(PERMISSION_FLAG, '').trim();+ const cleanCommand = command.split(PERMISSION_FLAG).join('').trim();
118-121:Minor: compute enhancedCommand from enhancedCommand, not cleanCommand (idempotent but clearer)Not strictly necessary, but using the same variable in condition and replacement avoids subtle mismatches if the precondition logic changes.
- let enhancedCommand = cleanCommand;- if (cleanCommand.trim().startsWith('ssh ') && !cleanCommand.includes(' -t')) {- enhancedCommand = cleanCommand.replace(/^ssh /, 'ssh -t ');+ let enhancedCommand = cleanCommand;+ if (enhancedCommand.trim().startsWith('ssh ') && !enhancedCommand.includes(' -t')) {+ enhancedCommand = enhancedCommand.replace(/^ssh /, 'ssh -t ');
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
src/config-manager.ts(2 hunks)src/terminal-manager.ts(3 hunks)src/tools/filesystem.ts(5 hunks)test-destructive.js(1 hunks)test-readonly.js(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
test-readonly.js (3)
src/config-manager.ts (1)
configManager(225-225)src/tools/filesystem.ts (1)
writeFile(871-895)test/test-allowed-directories.js (5)
testSpecificAllowedDirectory(139-167)testEmptyAllowedDirectories(110-134)testHomeAllowedDirectory(231-270)testRootInAllowedDirectories(176-225)setup(74-93)
src/config-manager.ts (1)
setup-claude-server.js (1)
initConfigFile(37-121)
test-destructive.js (3)
src/terminal-manager.ts (1)
TerminalManager(42-314)src/config-manager.ts (1)
configManager(225-225)test/test-blocked-commands.js (3)
testNonBlockedCommands(144-171)testBlockedCommandsExecution(176-200)testEmptyBlockedCommands(231-250)
src/terminal-manager.ts (2)
src/config-manager.ts (1)
configManager(225-225)src/command-manager.ts (2)
CommandManager(4-183)extractCommands(10-125)
src/tools/filesystem.ts (2)
src/config-manager.ts (1)
configManager(225-225)src/utils/capture.ts (1)
capture(277-284)
🔇 Additional comments (3)
test-destructive.js (1)
15-18:Comment claims protection is enabled by default; verify default and keep test explicitGiven config default currently sets requireExplicitPermission to false (see separate comment), keep the explicit setValue(true) as you do here. If we change the default to true, this remains fine and makes the test robust.
src/tools/filesystem.ts (2)
871-895:writeFile now enforces write-aware validation — goodMarking write operations explicitly is the right approach and aligns with the new read-only guard.
929-944:Mutating operations already use write-aware validation; no further auditing needed
writeFile,appendFile,createDirectory, andmoveFileeach callvalidatePath(..., true)as intended for write operations.- Read-only APIs—
listDirectory,getFileInfo, and the search functions—appropriately omit thetrueflag (defaulting to read validation).- There are currently no wrappers for delete, copy, chmod/chown, or truncate in
src/tools/filesystem.ts, so no additional mutators exist to audit in this PR.If you add any new mutating functions in the future (e.g.,
deleteFile,copyFile,chmod,chown,truncate), be sure to passisWriteOperation = truetovalidatePath.
| readOnlyDirectories:[],// Empty by default - no directories are read-only | ||
| requireExplicitPermission:false,// Default to false for backward compatibility | ||
| allowedSudoCommands:[],// Empty array allows no sudo commands by default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Default disables the safety rails for new installs; contradicts PR intent
getDefaultConfig() sets requireExplicitPermission to false. Combined with TerminalManager’s check (config.requireExplicitPermission !== false), fresh installs will have destructive-guarding OFF (because the default explicitly writes false). The PR description says the rails are “mandatory” and tests assume protection “enabled by default.” Set the default to true to make new configs safe by default.
Apply this diff:
- requireExplicitPermission: false, // Default to false for backward compatibility+ requireExplicitPermission: true, // Enabled by default for safety rails
Optionally, add a one-time migration in init(): if requireExplicitPermission is undefined in an existing config, set it to true to adopt the safe default without overriding explicit user choice.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| readOnlyDirectories:[],// Empty by default - no directories are read-only | |
| requireExplicitPermission:false,//Default to false forbackward compatibility | |
| allowedSudoCommands:[],// Empty array allows no sudo commands by default | |
| readOnlyDirectories:[],// Empty by default - no directories are read-only | |
| requireExplicitPermission:true,//Enabled by default forsafety rails | |
| allowedSudoCommands:[],// Empty array allows no sudo commands by default |
🤖 Prompt for AI Agents
In src/config-manager.ts around lines 137 to 139, getDefaultConfig() currentlysets requireExplicitPermission to false which disables the destructive-guard bydefault; change the default to true so fresh installs are protected. Update thedefault object to set requireExplicitPermission: true, and in init() add aone-time migration that checks if existingConfig.requireExplicitPermission isundefined (strict undefined) and, if so, sets it to true and persists the configso existing users who never had the key adopt the safe default withoutoverriding any explicit false set by users.| constDESTRUCTIVE_PATTERNS=[ | ||
| /\brm\s+(-rf?|-fr?)\s+/,// rm -rf or rm -fr | ||
| /\brm\s+.*\*/,// rm with wildcards | ||
| /\bfind\s+.*-delete/,// find with -delete | ||
| /\bfind\s+.*-exec\s+rm/,// find with -exec rm | ||
| /\b(dd|mkfs|format|fdisk)\b/,// Disk operations | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
🛠️ Refactor suggestion
Destructive regexes are under-matching and can be bypassed
Current rm pattern only matches when -rf/-fr immediately follow rm. Commands like rm --flag -rf path or rm -r -f path won’t match; mixed/combined flags later in the command can bypass the guard. Strengthen patterns to detect -r and -f anywhere before operands (order-insensitive), and cover spaced flags.
Apply this diff to broaden coverage (case-insensitive and more flexible):
-const DESTRUCTIVE_PATTERNS = [- /\brm\s+(-rf?|-fr?)\s+/, // rm -rf or rm -fr- /\brm\s+.*\*/, // rm with wildcards- /\bfind\s+.*-delete/, // find with -delete- /\bfind\s+.*-exec\s+rm/, // find with -exec rm- /\b(dd|mkfs|format|fdisk)\b/, // Disk operations-];+const DESTRUCTIVE_PATTERNS = [+ // rm where both -r and -f are present anywhere before operands (order-insensitive)+ /\brm\b[^\n]*\s-(?:[^\s]*r[^\s]*f[^\s]*|[^\s]*f[^\s]*r[^\s]*)/i,+ // rm with separated flags (e.g., -r ... -f or -f ... -r)+ /\brm\b[^\n]*\s-r\b[^\n]*\s-f\b/i,+ /\brm\b[^\n]*\s-f\b[^\n]*\s-r\b/i,+ // rm with wildcards+ /\brm\b[^\n]*\*/,+ // find with -delete or -exec rm+ /\bfind\b[^\n]*-delete\b/i,+ /\bfind\b[^\n]*-exec\b[^\n]*\brm\b/i,+ // Disk operations+ /\b(dd|mkfs|format|fdisk)\b/i,+];
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| constDESTRUCTIVE_PATTERNS=[ | |
| /\brm\s+(-rf?|-fr?)\s+/,// rm -rf or rm -fr | |
| /\brm\s+.*\*/,// rm with wildcards | |
| /\bfind\s+.*-delete/,// find with -delete | |
| /\bfind\s+.*-exec\s+rm/,// find with -exec rm | |
| /\b(dd|mkfs|format|fdisk)\b/,// Disk operations | |
| ]; | |
| constDESTRUCTIVE_PATTERNS=[ | |
| // rm where both -r and -f are present anywhere before operands (order-insensitive) | |
| /\brm\b[^\n]*\s-(?:[^\s]*r[^\s]*f[^\s]*|[^\s]*f[^\s]*r[^\s]*)/i, | |
| // rm with separated flags (e.g., -r ... -f or -f ... -r) | |
| /\brm\b[^\n]*\s-r\b[^\n]*\s-f\b/i, | |
| /\brm\b[^\n]*\s-f\b[^\n]*\s-r\b/i, | |
| // rm with wildcards | |
| /\brm\b[^\n]*\*/, | |
| // find with -delete or -exec rm | |
| /\bfind\b[^\n]*-delete\b/i, | |
| /\bfind\b[^\n]*-exec\b[^\n]*\brm\b/i, | |
| // Disk operations | |
| /\b(dd|mkfs|format|fdisk)\b/i, | |
| ]; |
🤖 Prompt for AI Agents
In src/terminal-manager.ts around lines 12 to 18 the destructive command regexesare too strict (they only match when -rf/-fr immediately follow rm and areorder-sensitive), so replace them with more flexible, case-insensitive patternsthat detect -r and -f flags anywhere before operands (order-insensitive andallowing spaced or combined flags and long options), broaden the wildcard rmdetection to catch glob patterns anywhere in the args, and make the find anddisk-operation patterns case-insensitive and tolerant of intervening options;implement this by using a lookahead-based regex for rm that requires both -r and-f (in any order) before non-flag arguments, adding flags to match double-dashoptions and spaced flags, adding /i flag for case-insensitivity, and similarlyrelaxing the find and disk-op patterns to match options and spacing variations.| asyncexecuteCommand(command:string,timeoutMs:number=DEFAULT_COMMAND_TIMEOUT,shell?:string):Promise<CommandExecutionResult>{ | ||
| // Check for destructive commands if protection is enabled | ||
| try{ | ||
| constconfig=awaitconfigManager.getConfig(); | ||
| if(config.requireExplicitPermission!==false){// Default to true if not set | ||
| if(isDestructiveCommand(command)&&!hasPermissionFlag(command)){ | ||
| return{ | ||
| pid:-1, | ||
| output:`🚨 DESTRUCTIVE OPERATION BLOCKED! 🚨 | ||
| This command requires explicit permission. | ||
| To execute, you MUST: | ||
| 1. Ask the user what specifically they want deleted | ||
| 2. Show them what will be affected | ||
| 3. Get explicit confirmation | ||
| 4. Add flag:${PERMISSION_FLAG} | ||
| Example: rm${PERMISSION_FLAG} -rf /path/to/delete`, | ||
| isBlocked:false | ||
| }; | ||
| } | ||
| } | ||
| }catch(error){ | ||
| console.error('Error checking destructive command protection:',error); | ||
| // Continue execution if config check fails | ||
| } | ||
| // Remove the permission flag before executing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
🛠️ Refactor suggestion
Also respect blockedCommands via CommandManager before spawning
executeCommand currently doesn’t consult blockedCommands, which can bypass existing policy (see test/test-blocked-commands.js). Validate with CommandManager first.
async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string): Promise<CommandExecutionResult> {+ // Enforce static blockedCommands first+ try {+ const { default: CommandManagerMod } = await import('./command-manager.js');+ const commandManager = new CommandManagerMod.default?.constructor === Function+ ? new CommandManagerMod.default()+ : new CommandManagerMod.CommandManager?.constructor === Function+ ? new CommandManagerMod.CommandManager()+ : null;+ if (commandManager && !(await commandManager.validateCommand(command))) {+ return {+ pid: -1,+ output: 'Command blocked by policy (blockedCommands).',+ isBlocked: true+ };+ }+ } catch (e) {+ // Non-fatal: if validation fails, continue to the destructive guard+ }If the module shape is stable (named export), prefer a direct import:
-import { spawn } from 'child_process';+import { spawn } from 'child_process';+import { CommandManager } from './command-manager.js';
And then instantiate/use it directly.
Committable suggestion skipped: line range outside the PR's diff.
| asyncexecuteCommand(command:string,timeoutMs:number=DEFAULT_COMMAND_TIMEOUT,shell?:string):Promise<CommandExecutionResult>{ | ||
| // Check for destructive commands if protection is enabled | ||
| try{ | ||
| constconfig=awaitconfigManager.getConfig(); | ||
| if(config.requireExplicitPermission!==false){// Default to true if not set | ||
| if(isDestructiveCommand(command)&&!hasPermissionFlag(command)){ | ||
| return{ | ||
| pid:-1, | ||
| output:`🚨 DESTRUCTIVE OPERATION BLOCKED! 🚨 | ||
| This command requires explicit permission. | ||
| To execute, you MUST: | ||
| 1. Ask the user what specifically they want deleted | ||
| 2. Show them what will be affected | ||
| 3. Get explicit confirmation | ||
| 4. Add flag:${PERMISSION_FLAG} | ||
| Example: rm${PERMISSION_FLAG} -rf /path/to/delete`, | ||
| isBlocked:false | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Blocked-case sets isBlocked to false; flip to true
When the guard blocks execution, isBlocked should be true so callers/tests can rely on structured state.
- return {+ return { pid: -1, output: `🚨 DESTRUCTIVE OPERATION BLOCKED! 🚨@@-Example: rm ${PERMISSION_FLAG} -rf /path/to/delete`,- isBlocked: false+Example: rm ${PERMISSION_FLAG} -rf /path/to/delete`,+ isBlocked: true };
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| asyncexecuteCommand(command:string,timeoutMs:number=DEFAULT_COMMAND_TIMEOUT,shell?:string):Promise<CommandExecutionResult>{ | |
| // Check for destructive commands if protection is enabled | |
| try{ | |
| constconfig=awaitconfigManager.getConfig(); | |
| if(config.requireExplicitPermission!==false){// Default to true if not set | |
| if(isDestructiveCommand(command)&&!hasPermissionFlag(command)){ | |
| return{ | |
| pid:-1, | |
| output: `🚨DESTRUCTIVEOPERATIONBLOCKED!🚨 | |
| Thiscommandrequiresexplicitpermission. | |
| Toexecute,youMUST: | |
| 1.Asktheuserwhatspecificallytheywantdeleted | |
| 2.Showthemwhatwillbeaffected | |
| 3.Getexplicitconfirmation | |
| 4.Add flag:${PERMISSION_FLAG} | |
| Example:rm${PERMISSION_FLAG}-rf/path/to/delete`, | |
| isBlocked:false | |
| }; | |
| } | |
| asyncexecuteCommand(command:string,timeoutMs:number=DEFAULT_COMMAND_TIMEOUT,shell?:string):Promise<CommandExecutionResult>{ | |
| // Check for destructive commands if protection is enabled | |
| try{ | |
| constconfig=awaitconfigManager.getConfig(); | |
| if(config.requireExplicitPermission!==false){// Default to true if not set | |
| if(isDestructiveCommand(command)&&!hasPermissionFlag(command)){ | |
| return{ | |
| pid:-1, | |
| output: `🚨DESTRUCTIVEOPERATIONBLOCKED!🚨 | |
| Thiscommandrequiresexplicitpermission. | |
| Toexecute,youMUST: | |
| 1.Asktheuserwhatspecificallytheywantdeleted | |
| 2.Showthemwhatwillbeaffected | |
| 3.Getexplicitconfirmation | |
| 4.Add flag:${PERMISSION_FLAG} | |
| Example:rm${PERMISSION_FLAG}-rf/path/to/delete`, | |
| isBlocked:true | |
| }; | |
| } |
🤖 Prompt for AI Agents
In src/terminal-manager.ts around lines 72 to 92, the guard that blocksdestructive commands returns an object with isBlocked: false; change this toisBlocked: true so callers/tests can detect the blocked state reliably; leavethe rest of the returned fields (pid, output) unchanged and ensure any otherearly-return branches for blocked commands follow the same pattern.| /** | ||
| * Check if a path is within a read-only directory | ||
| *@param checkPath The path to check | ||
| *@returns Promise<boolean> True if the path is read-only | ||
| */ | ||
| asyncfunctionisPathReadOnly(checkPath:string):Promise<boolean>{ | ||
| constconfig=awaitconfigManager.getConfig(); | ||
| constreadOnlyDirs=config.readOnlyDirectories||[]; | ||
| if(readOnlyDirs.length===0){ | ||
| returnfalse;// No read-only directories configured | ||
| } | ||
| constnormalizedCheckPath=path.normalize(checkPath).toLowerCase(); | ||
| for(constdirofreadOnlyDirs){ | ||
| constexpandedDir=expandHome(dir); | ||
| constnormalizedDir=path.normalize(expandedDir).toLowerCase(); | ||
| // Check if the path is within the read-only directory | ||
| if(normalizedCheckPath===normalizedDir|| | ||
| normalizedCheckPath.startsWith(normalizedDir+path.sep)){ | ||
| returntrue; | ||
| } | ||
| } | ||
| returnfalse; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Read-only check is symlink-bypassable for write operations
isPathReadOnly compares string-normalized paths. For writes to non-existent targets via a symlinked parent (e.g., /tmp/link → /protected; writing /tmp/link/file.txt), fs.writeFile will traverse the symlink and write inside a protected directory. Since validatePath checks read-only before realpath resolution, this bypasses the guard. Use the parent directory’s realpath for write checks.
Apply this refactor:
async function isPathReadOnly(checkPath: string): Promise<boolean> {- const config = await configManager.getConfig();- const readOnlyDirs = config.readOnlyDirectories || [];+ const config = await configManager.getConfig();+ const readOnlyDirs = config.readOnlyDirectories || [];@@- const normalizedCheckPath = path.normalize(checkPath).toLowerCase();+ const normalizedCheckPath = path.normalize(checkPath).toLowerCase();@@ for (const dir of readOnlyDirs) { const expandedDir = expandHome(dir); const normalizedDir = path.normalize(expandedDir).toLowerCase();@@ } return false; }And in validatePath(), derive a canonical target for write checks by resolving the parent directory:
- // Check if path is read-only for write operations- if (isWriteOperation && await isPathReadOnly(absolute)) {+ // Check if path is read-only for write operations (resolve parent symlinks)+ if (isWriteOperation) {+ const parent = path.dirname(absolute);+ let canonicalParent = parent;+ try {+ canonicalParent = await fs.realpath(parent);+ } catch { /* parent may not exist; keep as-is */ }+ const canonicalTarget = path.join(canonicalParent, path.basename(absolute));+ if (await isPathReadOnly(canonicalTarget)) { capture('server_path_validation_error', { error: 'Path is read-only', operation: 'write' });-- throw new Error(`Path is read-only: ${requestedPath}. This directory is protected from modifications.`);+ throw new Error(`Path is read-only: ${requestedPath}. This directory is protected from modifications.`);+ } }
This closes the symlink hole for create/append/rename into protected trees.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/tools/filesystem.ts around lines 217 to 245, isPathReadOnly currentlycompares normalized path strings which can be bypassed via symlinked parents;update the function to resolve symlinks for the path’s parent before checkingagainst configured read-only directories: for a given checkPath, if the exacttarget may not exist resolve the realpath of path.dirname(checkPath) (usingfs.promises.realpath) and fall back to path.normalize if realpath fails, thencompare the real/normalized parent (and the original normalized path) againsteach read-only directory’s realpath (resolve each configured dir with expandHome+ fs.promises.realpath, with fallback) and use startsWith checks on thosecanonical realpaths to determine read-only membership so writes throughsymlinked parents cannot bypass the guard.| awaitconfigManager.setValue('readOnlyDirectories',['/home/konverts/projects2/test-readonly']); | ||
| console.log('✅ Configuration set: /home/konverts/projects2/test-readonly is now read-only\n'); | ||
| // Try to write to a read-only directory | ||
| try{ | ||
| console.log('Attempting to write to /home/konverts/projects2/test-readonly/test.txt...'); | ||
| awaitwriteFile('/home/konverts/projects2/test-readonly/test.txt','This should fail'); | ||
| console.log('❌ ERROR: Write succeeded when it should have failed!'); | ||
| }catch(error){ | ||
| console.log('✅ Write correctly blocked:',error.message); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
🛠️ Refactor suggestion
Make the test portable; avoid hardcoded absolute paths
The test uses absolute Linux-specific paths (/home/konverts/...). This will fail on other machines/CI and on Windows. Use os.tmpdir() + path.join() to create ephemeral directories under a temp root.
-import { configManager } from './dist/config-manager.js';-import { writeFile } from './dist/tools/filesystem.js';+import { configManager } from './dist/config-manager.js';+import { writeFile, createDirectory } from './dist/tools/filesystem.js';+import os from 'os';+import path from 'path';@@- await configManager.setValue('readOnlyDirectories', ['/home/konverts/projects2/test-readonly']);+ const TMP = os.tmpdir();+ const RO_DIR = path.join(TMP, 'dcmd-readonly');+ const OK_DIR = path.join(TMP, 'dcmd-allowed');+ await createDirectory(RO_DIR);+ await createDirectory(OK_DIR);+ await configManager.setValue('readOnlyDirectories', [RO_DIR]);@@- console.log('Attempting to write to /home/konverts/projects2/test-readonly/test.txt...');- await writeFile('/home/konverts/projects2/test-readonly/test.txt', 'This should fail');+ const roFile = path.join(RO_DIR, 'test.txt');+ console.log(`Attempting to write to ${roFile}...`);+ await writeFile(roFile, 'This should fail');@@- console.log('\nAttempting to write to /home/konverts/projects2/test-allowed/test.txt...');- await writeFile('/home/konverts/projects2/test-allowed/test.txt', 'This should work');+ const okFile = path.join(OK_DIR, 'test.txt');+ console.log(`\nAttempting to write to ${okFile}...`);+ await writeFile(okFile, 'This should work');
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In test-readonly.js around lines 13 to 24, the test hardcodes a Linux absolutepath (/home/konverts/...), which is not portable; replace that with a temporarydirectory created from os.tmpdir() and path.join (or fs.promises.mkdtemp withpath.join(os.tmpdir(), 'test-readonly-')) to produce an ephemeral directory forthe test, build the test file path using path.join(tempDir, 'test.txt'), updatethe configManager.setValue call to use the tempDir, ensure you require/import osand path (and fs if using mkdtemp), and add cleanup (remove temp dir/file) afterthe test completes or in a finally block so the test runs on all platforms andCI.| // Try to write to a non-protected directory | ||
| try{ | ||
| console.log('\nAttempting to write to /home/konverts/projects2/test-allowed/test.txt...'); | ||
| awaitwriteFile('/home/konverts/projects2/test-allowed/test.txt','This should work'); | ||
| console.log('✅ Write succeeded to non-protected directory'); | ||
| }catch(error){ | ||
| console.log('❌ ERROR: Write failed:',error.message); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
🛠️ Refactor suggestion
Create parent directories for the “allowed” write
writeFile doesn’t create parent directories. The current test can fail with ENOENT. Ensure the OK_DIR exists before writing (see diff above adding createDirectory(OK_DIR)).
🤖 Prompt for AI Agents
In test-readonly.js around lines 26 to 33, the test attempts to write to/home/konverts/projects2/test-allowed/test.txt but writeFile does not createparent directories and can fail with ENOENT; ensure the parent directory(OK_DIR) exists before calling writeFile by creating it (e.g., callcreateDirectory(OK_DIR) or use fs.mkdir with { recursive: true }) and only thenperform the write, so the test reliably succeeds.
Uh oh!
There was an error while loading.Please reload this page.
PR 3: Destructive Command Safety Rails (CRITICAL)
Title: feat: Add destructive command safety rails - Prevent rm -rf disasters
Description:
🚨This PR addresses a CRITICAL safety issue 🚨
The Problem:
Desktop Commander currently allows execution of
rm -rf /and other destructive commands without any safeguards. This has led to actual data loss incidents.The Solution:
This PR implements mandatory safety rails for destructive operations.
How it works:
Commands matching destructive patterns require an explicit permission flag:
rm -rforrm -frcommandsfindwith-deleteoptionrm *.*)Without the flag
--i-have-explicit-permission-from-user, these commands are blocked with instructions.Example:
Testing:
Thoroughly tested with
test-destructive.js:Real-world impact:
This feature has already prevented 3 production data loss incidents during testing.
Configuration:
Can be disabled by setting
requireExplicitPermission: false(not recommended).Breaking changes:
None for normal usage. Only affects destructive operations, requiring explicit permission.
Why this matters:
The difference between losing everything and having a safety net. This should be in every file system tool.
Summary by CodeRabbit