Skip to content

protectedFilePatterns never match on Windows: normalizePath searches for a doubled backslash #592

Description

@LHMQ878

Summary

normalizePath in lib/protected-patterns.ts is a no-op on Windows, because its search string is two backslashes rather than one:

function normalizePath(input: string): string {
    return input.replaceAll("\\\\", "/")
}

In TypeScript source, "\\\\" is the two-character string \\. A real Windows path contains single separators (C:\repo\src\secrets.ts), so nothing is ever replaced. The function exists only to make Windows paths comparable to forward-slash glob patterns, and it does not do that on the one platform that needs it.

The result is that protectedFilePatterns protects a file on macOS/Linux and silently fails to protect the same file on Windows.

Reproduction

import { matchesGlob, isFilePathProtected, getFilePathsFromParameters } from "./lib/protected-patterns"

const winPath = "C:\\repo\\src\\config\\secrets.ts"
const posixPath = "C:/repo/src/config/secrets.ts"

matchesGlob(posixPath, "**/secrets.ts")   // -> true
matchesGlob(winPath,   "**/secrets.ts")   // -> false   <-- same file

// End to end, through the shape a `read` tool call actually has:
const patterns = ["**/secrets.ts", "**/.env"]
isFilePathProtected(getFilePathsFromParameters("read", { filePath: posixPath }), patterns)  // -> true
isFilePathProtected(getFilePathsFromParameters("read", { filePath: winPath }),   patterns)  // -> false

Measured behaviour for one file spelled two ways:

pattern POSIX path Windows path
**/secrets.ts true false
**/config/*.ts true false
C:/repo/src/** true false
**/*.ts true true

The last row is why this is easy to miss: a pattern with no separator after the ** still matches, because ** compiles to .* and happily spans backslashes. So a user testing with **/*.ts sees protection working, while every pattern that names a directory or an exact file quietly does nothing.

Impact

isFilePathProtected is the guard that keeps configured files out of pruning. It is consulted from four places:

  • lib/compress/protected-content.ts:138
  • lib/commands/sweep.ts:184 and :201
  • lib/strategies/deduplication.ts:60
  • lib/strategies/purge-errors.ts:62

On Windows all five call sites see false for the paths the user explicitly asked to protect, so those tool outputs are eligible for pruning and compression like any other. The failure is silent — the config parses, the schema validates, and nothing logs a mismatch. A user cannot tell from the outside that the setting is inert.

README.md:115 and dcp.schema.json:129 both document these patterns with forward slashes (**/*.config.ts), which is the correct spelling; the defect is purely that Windows paths never get converted to it.

Root cause

An escaping mistake, nothing more. replaceAll takes a string (not a regex) as its first argument, so no regex-level escaping is wanted there — "\\" is already the single backslash. The extra pair makes it search for a doubled separator, which only appears in an already-escaped string, never in a path from the filesystem.

Proposed fix

return input.replaceAll("\\", "/")

One character. Both the path and the pattern go through normalizePath, so this also makes a pattern written with Windows separators work, which is what a user copying a path out of Explorer would naturally produce.

Worth noting what must not change: separators have to be converted, not removed. * compiles to [^/]* and is expected not to cross a directory boundary, so src/* must still not match src\config\secrets.ts. That property is easy to break with a fix that strips backslashes instead of translating them, so it is worth a test of its own.

lib/protected-patterns.ts currently has no test file. PR to follow with one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions