Skip to content

Conversation

Corlzee
Copy link

@Corlzee Corlzee commented Aug 22, 2025

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 -rf or rm -fr commands
  • find with -delete option
  • Wildcard deletions (rm *.*)
  • Disk operations (dd, mkfs, format, fdisk)

Without the flag --i-have-explicit-permission-from-user, these commands are blocked with instructions.

Example:

# This will be BLOCKED:
rm -rf /important/directory

# This will execute (after user confirmation):
rm --i-have-explicit-permission-from-user -rf /important/directory

Testing:

Thoroughly tested with test-destructive.js:

  • ✅ Destructive commands blocked without flag
  • ✅ Commands execute correctly with flag
  • ✅ Normal commands unaffected
  • ✅ Clear error messages provided

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

  • New Features
    • Added optional settings for read-only directories, explicit permission requirement, and allowed sudo commands.
    • Destructive-command protection blocks risky commands unless explicitly permitted; commands are sanitized before execution; improved SSH invocation.
    • Enforced read-only restrictions for write operations with clear error messages.
  • Bug Fixes
    • Graceful handling when a spawned process lacks a PID; execution continues on config retrieval errors.
  • Documentation
    • Updated notes for path validation with write-awareness.
  • Tests
    • New scripts validate destructive-command protection and read-only directory enforcement.

Corlzee added 3 commits August 22, 2025 11:22
- 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 behavior

Prevents 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 execution

Prevents catastrophic data loss from accidental commands.
This single feature has prevented multiple disasters in production.
Copy link
Contributor

coderabbitai bot commented Aug 22, 2025

Walkthrough

Extends 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

Cohort / File(s) Summary
Config schema updates
src/config-manager.ts
Adds optional ServerConfig fields: readOnlyDirectories: string[], requireExplicitPermission: boolean, allowedSudoCommands: string[]; initializes defaults in getDefaultConfig.
Terminal execution guardrails
src/terminal-manager.ts
Adds destructive-command detection with explicit permission flag; sanitizes commands; auto-injects ssh -t; non-throwing PID error handling; logs config retrieval errors but continues.
Filesystem read-only enforcement
src/tools/filesystem.ts
Adds read-only path detection; updates validatePath(requestedPath, isWriteOperation=false); enforces on writeFile, createDirectory, moveFile; emits telemetry and throws on read-only writes.
Test scripts
test-destructive.js, test-readonly.js
Adds tests for destructive-command blocking/flag bypass and read-only directory write protection.

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)
  end
Loading
sequenceDiagram
  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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • serg33v

Poem

A bunny taps the terminal keys,
Guardrails rise with cautious ease.
Paths made safe, the writes restrained,
Destructive storms now well-contained.
With flags and fences neatly spun,
I thump approval—job well done! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 unused

readOnlyDirectories 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 effects

The 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 string

Relying 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 execution

To 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 separators

Consider 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 here

Calling 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 occurrence

String.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.

📥 Commits

Reviewing files that changed from the base of the PR and between 218531b and 177cef7.

📒 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 explicit

Given 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 — good

Marking 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, and moveFile each call validatePath(..., true) as intended for write operations.
  • Read-only APIs—listDirectory, getFileInfo, and the search functions—appropriately omit the true flag (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 pass isWriteOperation = true to validatePath.

Comment on lines +137 to +139
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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
readOnlyDirectories: [], // Empty by default - no directories are read-only
requireExplicitPermission: true, // Enabled by default for safety 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() currently
sets requireExplicitPermission to false which disables the destructive-guard by
default; change the default to true so fresh installs are protected. Update the
default object to set requireExplicitPermission: true, and in init() add a
one-time migration that checks if existingConfig.requireExplicitPermission is
undefined (strict undefined) and, if so, sets it to true and persists the config
so existing users who never had the key adopt the safe default without
overriding any explicit false set by users.

Comment on lines +12 to +18
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
];
Copy link
Contributor

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.

Suggested change
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,
];
🤖 Prompt for AI Agents
In src/terminal-manager.ts around lines 12 to 18 the destructive command regexes
are too strict (they only match when -rf/-fr immediately follow rm and are
order-sensitive), so replace them with more flexible, case-insensitive patterns
that detect -r and -f flags anywhere before operands (order-insensitive and
allowing spaced or combined flags and long options), broaden the wildcard rm
detection to catch glob patterns anywhere in the args, and make the find and
disk-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-dash
options and spaced flags, adding /i flag for case-insensitivity, and similarly
relaxing the find and disk-op patterns to match options and spacing variations.

Comment on lines 72 to +99
async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string): Promise<CommandExecutionResult> {
// Check for destructive commands if protection is enabled
try {
const config = await configManager.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
Copy link
Contributor

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.

Comment on lines 72 to +92
async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string): Promise<CommandExecutionResult> {
// Check for destructive commands if protection is enabled
try {
const config = await configManager.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
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string): Promise<CommandExecutionResult> {
// Check for destructive commands if protection is enabled
try {
const config = await configManager.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
};
}
async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string): Promise<CommandExecutionResult> {
// Check for destructive commands if protection is enabled
try {
const config = await configManager.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: true
};
}
🤖 Prompt for AI Agents
In src/terminal-manager.ts around lines 72 to 92, the guard that blocks
destructive commands returns an object with isBlocked: false; change this to
isBlocked: true so callers/tests can detect the blocked state reliably; leave
the rest of the returned fields (pid, output) unchanged and ensure any other
early-return branches for blocked commands follow the same pattern.

Comment on lines +217 to +245
/**
* 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
*/
async function isPathReadOnly(checkPath: string): Promise<boolean> {
const config = await configManager.getConfig();
const readOnlyDirs = config.readOnlyDirectories || [];

if (readOnlyDirs.length === 0) {
return false; // No read-only directories configured
}

const normalizedCheckPath = path.normalize(checkPath).toLowerCase();

for (const dir of readOnlyDirs) {
const expandedDir = expandHome(dir);
const normalizedDir = path.normalize(expandedDir).toLowerCase();

// Check if the path is within the read-only directory
if (normalizedCheckPath === normalizedDir ||
normalizedCheckPath.startsWith(normalizedDir + path.sep)) {
return true;
}
}

return false;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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 currently
compares normalized path strings which can be bypassed via symlinked parents;
update the function to resolve symlinks for the path’s parent before checking
against configured read-only directories: for a given checkPath, if the exact
target may not exist resolve the realpath of path.dirname(checkPath) (using
fs.promises.realpath) and fall back to path.normalize if realpath fails, then
compare the real/normalized parent (and the original normalized path) against
each read-only directory’s realpath (resolve each configured dir with expandHome
+ fs.promises.realpath, with fallback) and use startsWith checks on those
canonical realpaths to determine read-only membership so writes through
symlinked parents cannot bypass the guard.

Comment on lines +13 to +24
await configManager.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...');
await writeFile('/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);
}
Copy link
Contributor

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 absolute
path (/home/konverts/...), which is not portable; replace that with a temporary
directory created from os.tmpdir() and path.join (or fs.promises.mkdtemp with
path.join(os.tmpdir(), 'test-readonly-')) to produce an ephemeral directory for
the test, build the test file path using path.join(tempDir, 'test.txt'), update
the configManager.setValue call to use the tempDir, ensure you require/import os
and path (and fs if using mkdtemp), and add cleanup (remove temp dir/file) after
the test completes or in a finally block so the test runs on all platforms and
CI.

Comment on lines +26 to +33
// Try to write to a non-protected directory
try {
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');
console.log('✅ Write succeeded to non-protected directory');
} catch (error) {
console.log('❌ ERROR: Write failed:', error.message);
}
Copy link
Contributor

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 create
parent directories and can fail with ENOENT; ensure the parent directory
(OK_DIR) exists before calling writeFile by creating it (e.g., call
createDirectory(OK_DIR) or use fs.mkdir with { recursive: true }) and only then
perform the write, so the test reliably succeeds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant