Skip to content

fix(windows): improve argument escaping and error detection#90

Open
moffermann wants to merge 2 commits intotuannvm:mainfrom
moffermann:main
Open

fix(windows): improve argument escaping and error detection#90
moffermann wants to merge 2 commits intotuannvm:mainfrom
moffermann:main

Conversation

@moffermann
Copy link

@moffermann moffermann commented Dec 30, 2025

  • Extract escapeArgForWindowsShell to separate module for testability
  • Handle newlines in arguments (replace with spaces for cmd.exe safety)
  • Use caret (^) escaping for quotes when no spaces present
  • Fix error detection: don't treat stderr-only output as success
  • Improve comments to accurately describe shell usage purpose
  • Add comprehensive unit tests (14 cases) for Windows escaping

🤖 Generated with Claude Code

Summary

Changes

Testing

  • Tests pass locally
  • New tests added (if applicable)

Related Issues

Summary by CodeRabbit

  • New Features

    • Streaming command execution now supports progress callbacks with debounced updates and final summary.
  • Improvements

    • Safer Windows shell argument escaping (handles newlines, percent signs, quotes, and special chars).
    • 10MB output buffer limits with truncation warnings.
    • Exit-code handling refined to tolerate non-zero codes when stdout exists; stderr included in error messages.
    • Error messages now aggregate chained causes for clearer context.
  • Tests

    • Added comprehensive tests covering Windows argument escaping scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

- Extract escapeArgForWindowsShell to separate module for testability
- Handle newlines in arguments (replace with spaces for cmd.exe safety)
- Use caret (^) escaping for quotes when no spaces present
- Fix error detection: don't treat stderr-only output as success
- Improve comments to accurately describe shell usage purpose
- Add comprehensive unit tests (14 cases) for Windows escaping

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

Walkthrough

Adds a Windows cmd.exe argument-escaping utility, refactors command execution to use it, introduces streaming execution with onProgress callbacks and 10MB buffer limits, and updates exit-code and error-handling semantics for streaming and non-streaming paths.

Changes

Cohort / File(s) Summary
New Escape Utility
src/utils/escape.ts
Adds escapeArgForWindowsShell(arg: string): string to normalize newlines, escape percent signs, handle quotes, and wrap args containing whitespace or shell-special characters for cmd.exe.
Escape Utility Tests
src/__tests__/windows-escape.test.ts
New comprehensive tests for escapeArgForWindowsShell covering spaces, percent signs, newlines, quotes, special characters, complex real-world strings, empty strings, and combined cases.
Command Execution Refactor
src/utils/command.ts
Replaces inline Windows-escaping with the new helper; adds ProgressCallback and StreamingCommandOptions exports; enables spawn-with-shell on Windows; enforces 10MB max buffers with truncation warnings; updates exit-code acceptance rules and error construction; emits debounced progress from stdout/stderr and a final summary.
Error Handling
src/errors.ts
Enhances handleError to aggregate error.message with chained causes, deduplicate consecutive messages, and produce a composite contextual message.
Manifest
package.json
Minor manifest edits (lines changed: +23/-1).

Sequence Diagram(s)

sequenceDiagram
    actor Caller
    participant executeCommandStreaming
    participant ChildProcess
    participant ProgressCallback

    Caller->>executeCommandStreaming: executeCommandStreaming(cmd, args, { onProgress })
    executeCommandStreaming->>executeCommandStreaming: Escape Windows args if needed
    executeCommandStreaming->>ChildProcess: spawn(cmd, args, { shell: true? })
    
    rect rgb(200,220,240)
    Note over ChildProcess,ProgressCallback: Streaming phase
    ChildProcess-->>executeCommandStreaming: stdout / stderr chunks
    executeCommandStreaming->>executeCommandStreaming: Accumulate & buffer (max 10MB)
    executeCommandStreaming->>ProgressCallback: onProgress (debounced) with current output
    end

    ChildProcess-->>executeCommandStreaming: exit event (exitCode)

    rect rgb(220,200,240)
    Note over executeCommandStreaming,ProgressCallback: Completion & validation
    executeCommandStreaming->>executeCommandStreaming: Validate exit code (0 or has stdout)
    executeCommandStreaming->>ProgressCallback: onProgress (final summary)
    executeCommandStreaming-->>Caller: return { stdout, stderr, exitCode }
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hop through cmd.exe, careful and spry,

escaping percents as they flutter by.
Streams sing progress, buffers kept tight,
quotes doubled, newlines turned polite.
A rabbit's small patch—neat, safe, and bright.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: improving Windows argument escaping and error detection.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • 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

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@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: 0

🧹 Nitpick comments (2)
src/utils/escape.ts (1)

15-35: Empty string argument may be lost by cmd.exe.

When an empty string is passed to cmd.exe without quotes, it may be silently dropped as an argument. Consider wrapping empty strings in quotes to preserve them:

🔎 Proposed fix
 export function escapeArgForWindowsShell(arg: string): string {
+  // Empty strings need quotes to be preserved as arguments
+  if (arg === '') {
+    return '""';
+  }
+
   // Replace newlines with spaces - cmd.exe interprets \n as command separator
   let escaped = arg.replace(/[\r\n]+/g, ' ');
src/utils/command.ts (1)

186-194: Final progress message may mislead for short outputs.

The ... suffix is always appended regardless of output length, which could be misleading when the full output is less than 200 characters.

🔎 Proposed fix
       if (options.onProgress && (stdout || stderr)) {
         const finalOutput = stdout || stderr;
         const lastChunk = finalOutput.slice(-500); // Last 500 chars
         if (lastChunk.trim()) {
+          const trimmed = lastChunk.trim();
+          const display = trimmed.length > 200 
+            ? `${trimmed.slice(0, 200)}...` 
+            : trimmed;
           options.onProgress(
-            `[Completed] ${lastChunk.trim().slice(0, 200)}...`
+            `[Completed] ${display}`
           );
         }
       }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7aba98a and 7e0db12.

📒 Files selected for processing (3)
  • src/__tests__/windows-escape.test.ts
  • src/utils/command.ts
  • src/utils/escape.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/__tests__/windows-escape.test.ts (1)
src/utils/escape.ts (1)
  • escapeArgForWindowsShell (15-35)
src/utils/command.ts (2)
src/utils/escape.ts (1)
  • escapeArgForWindowsShell (15-35)
src/types.ts (1)
  • CommandResult (95-98)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: code-review
🔇 Additional comments (7)
src/utils/escape.ts (1)

1-35: Well-structured escape utility with good documentation.

The implementation correctly handles the key cmd.exe escaping concerns:

  • Newline normalization to prevent command injection
  • Percent sign doubling to prevent environment variable expansion
  • Proper quoting strategy with quote-doubling vs caret escaping

The separation into a dedicated module improves testability as intended.

src/__tests__/windows-escape.test.ts (2)

100-102: Empty string test expectation may need adjustment.

If the escape function is updated to wrap empty strings in quotes (to preserve them as arguments in cmd.exe), this test expectation would need to change to '""'.


1-119: Comprehensive test coverage for Windows escaping.

The test suite covers the key scenarios well:

  • Basic arguments and flags
  • Percent sign escaping for env var protection
  • Newline sanitization
  • Quote handling (caret vs doubling strategies)
  • Shell metacharacters
  • Real-world complex cases

Good use of descriptive test names and logical grouping.

src/utils/command.ts (4)

6-6: Correct integration of the escape utility.

Clean import of the centralized escaping function, maintaining single responsibility.


24-26: Appropriate Windows argument escaping.

The comment correctly notes that only arguments are escaped (not the command name), which aligns with the escape function's documented scope.


68-91: Exit code handling logic is sound.

The approach of accepting non-zero exit codes when stdout is present (with a warning) while rejecting non-zero with only stderr appropriately balances tolerance for quirky tools with proper error detection. The comment at lines 73-75 clearly documents this intentional behavior.


196-214: Consistent error handling between streaming and non-streaming paths.

The streaming variant now mirrors the non-streaming exit code logic, with appropriate comments explaining why only stdout (not stderr) counts for non-zero exit codes. This correctly prevents masking ENOENT errors when shell: true is used.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

No high-severity issues detected in this revision.

The handleError function now traverses the error cause chain to display
all error details instead of only the top-level message. This helps
users understand why commands failed (e.g., authentication errors,
command not found, network issues).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@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: 1

🤖 Fix all issues with AI agents
In @src/errors.ts:
- Around line 37-50: The cause-chain traversal starting at error.cause (variable
current) must guard against cycles and unbounded depth: add a Set (e.g., seen)
to record visited cause objects and check seen.has(current) at the top of the
while loop to break if a cycle is detected, and also enforce a sensible maxDepth
counter (e.g., 50–100) to prevent runaway traversal; when breaking for a cycle
or max depth, append a short message to messages (e.g., "[circular cause]" or
"[truncated cause chain]") and otherwise proceed as before pushing
current.message and advancing current = current.cause.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7e0db12 and 02eb54c.

📒 Files selected for processing (1)
  • src/errors.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: code-review
🔇 Additional comments (1)
src/errors.ts (1)

52-57: Clean implementation of consecutive deduplication.

The deduplication logic correctly removes only consecutive identical messages, which is appropriate for error cause chains where the same message at different levels might provide useful context.

Comment on lines +37 to +50
// Traverse the cause chain to get root cause details
let current: unknown = error.cause;
while (current) {
if (current instanceof Error) {
messages.push(current.message);
current = current.cause;
} else if (typeof current === 'string') {
messages.push(current);
break;
} else {
messages.push(String(current));
break;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add safeguards against circular references and unbounded traversal.

The cause chain traversal lacks protection against circular references (e.g., err1.cause = err2, err2.cause = err1), which would cause an infinite loop. While uncommon in typical usage, error-handling code should be defensive against edge cases.

🔒 Proposed fix with cycle detection
   // Traverse the cause chain to get root cause details
   let current: unknown = error.cause;
+  const seen = new Set<Error>();
+  const MAX_DEPTH = 10;
+  let depth = 0;
+  
-  while (current) {
+  while (current && depth < MAX_DEPTH) {
     if (current instanceof Error) {
+      if (seen.has(current)) break; // Circular reference detected
+      seen.add(current);
       messages.push(current.message);
       current = current.cause;
+      depth++;
     } else if (typeof current === 'string') {
       messages.push(current);
       break;
     } else {
       messages.push(String(current));
       break;
     }
   }
📝 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
// Traverse the cause chain to get root cause details
let current: unknown = error.cause;
while (current) {
if (current instanceof Error) {
messages.push(current.message);
current = current.cause;
} else if (typeof current === 'string') {
messages.push(current);
break;
} else {
messages.push(String(current));
break;
}
}
// Traverse the cause chain to get root cause details
let current: unknown = error.cause;
const seen = new Set<Error>();
const MAX_DEPTH = 10;
let depth = 0;
while (current && depth < MAX_DEPTH) {
if (current instanceof Error) {
if (seen.has(current)) break; // Circular reference detected
seen.add(current);
messages.push(current.message);
current = current.cause;
depth++;
} else if (typeof current === 'string') {
messages.push(current);
break;
} else {
messages.push(String(current));
break;
}
}
🤖 Prompt for AI Agents
In @src/errors.ts around lines 37 - 50, The cause-chain traversal starting at
error.cause (variable current) must guard against cycles and unbounded depth:
add a Set (e.g., seen) to record visited cause objects and check
seen.has(current) at the top of the while loop to break if a cycle is detected,
and also enforce a sensible maxDepth counter (e.g., 50–100) to prevent runaway
traversal; when breaking for a cycle or max depth, append a short message to
messages (e.g., "[circular cause]" or "[truncated cause chain]") and otherwise
proceed as before pushing current.message and advancing current = current.cause.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

No issues detected in the current diff; the Windows escaping helpers and error handling updates look solid.

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