Skip to content

Bug #11: Linux Path Corruption + Stale Directory Cache

Status: RESOLVED in v3.14.1 Category: Path Normalization / Cache Correctness Severity: High (silent data loss, incorrect filesystem state) Resolution Date: 2026-02-17

Two independent bugs triggered together in a common workflow: copying files from a WSL container path to a Windows destination.

Bug A — copy_file corrupts Linux source paths on Windows

Section titled “Bug A — copy_file corrupts Linux source paths on Windows”

When copy_file received a pure Linux path (e.g. /tmp/package/dist/css/bootstrap.min.css) as the source while running on Windows, the operation failed with a misleading error:

Error: "source does not exist: \tmp\package\dist\css\bootstrap.min.css"

NormalizePath() handled two cases explicitly:

Input formatConversion
/mnt/c/Users/...C:\Users\...
C:\Users\...→ normalized Windows path ✅

But paths starting with / that were not /mnt/<drive>/ (i.e. true Linux/WSL filesystem paths like /tmp/, /home/, /opt/) fell through to filepath.Clean(). On Windows, filepath.Clean("/tmp/package/...") converts forward slashes to backslashes and produces \tmp\package\... — a broken path that looks like a relative-from-root Windows path and doesn’t point to anything real.

/tmp/package/dist/css/bootstrap.min.css
↓ filepath.Clean() on Windows
\tmp\package\dist\css\bootstrap.min.css ← NOT a valid Windows path

The correct Windows-accessible path for a WSL file is the UNC form:

\\wsl.localhost\Ubuntu\tmp\package\dist\css\bootstrap.min.css

Bug B — Directory cache not invalidated after external writes

Section titled “Bug B — Directory cache not invalidated after external writes”

After bash cp (or any process outside the MCP server) wrote files into a directory, subsequent calls to mcp_list returned a stale cached listing showing the directory as empty:

bash cp /tmp/package/dist/css/bootstrap.min.css \
/mnt/c/Users/DAVID/.../lib/bootstrap/ → success (files exist on disk)
mcp_list C:\Users\DAVID\...\lib\bootstrap\ → shows empty ← BUG
bash ls /mnt/c/Users/DAVID/.../lib/bootstrap/ → bootstrap.min.css (232 KB)
bootstrap.min.css.map (589 KB)

Root cause: SetDirectory() stored only the listing string. There was no way for ListDirectoryContent() to know whether the directory had been modified since the entry was cached. The 3-minute TTL was the only expiry mechanism — external writes were invisible.

The two bugs typically appeared together:

  1. copy_file with a Linux source path failed (Bug A) → user fell back to bash cp
  2. bash cp succeeded, but mcp_list still showed the old (empty) state (Bug B)
  3. The directory appeared empty to the AI even though the files were present on disk

Bug A — WSL UNC path conversion in NormalizePath()

Section titled “Bug A — WSL UNC path conversion in NormalizePath()”

Added a cached WSL distro lookup (getDefaultWSLDistro() in core/path_detector.go) that runs wsl.exe -l --quiet once and stores the default distro name. The distro name is resolved with sync.Once so the exec call happens at most once per server lifetime.

NormalizePath() (core/engine.go) now handles the missing case before the final fallback:

// Handle pure Linux/WSL paths (e.g. /tmp/..., /home/...) when running on Windows.
// filepath.Clean() would corrupt them to \tmp\... which is not a valid Windows path.
if os.PathSeparator == '\\' && len(path) > 0 && path[0] == '/' {
if distro := getDefaultWSLDistro(); distro != "" {
// /tmp/package/dist/... → \\wsl.localhost\Ubuntu\tmp\package\dist\...
return `\\wsl.localhost\` + distro + filepath.FromSlash(path)
}
// WSL not available: return unchanged so error messages stay meaningful
// ("source does not exist: /tmp/...") instead of the corrupted backslash form.
return path
}

Before:

copy_file source=/tmp/package/dist/css/bootstrap.min.css
NormalizePath() → \tmp\package\dist\css\bootstrap.min.css
os.Stat() → "source does not exist: \tmp\package\dist\css\bootstrap.min.css"

After:

copy_file source=/tmp/package/dist/css/bootstrap.min.css
NormalizePath() → \\wsl.localhost\Ubuntu\tmp\package\dist\css\bootstrap.min.css
os.Stat() → file found, copy proceeds ✓

If WSL is not installed or no distro is found, the path is returned unchanged with its original forward slashes, giving a meaningful error message instead of a cryptic backslash path.

Bug B — Mtime validation on directory cache reads

Section titled “Bug B — Mtime validation on directory cache reads”

The cache entry for directories was changed from a bare string to a struct that also stores the directory’s modification time at cache time:

cache/intelligent.go
type dirCacheEntry struct {
Listing string
Mtime time.Time // directory mtime when the cache was populated
}

SetDirectory() now accepts the mtime, and GetDirectory() returns it alongside the listing:

SetDirectory(path string, listing string, mtime time.Time)
GetDirectory(path string) (listing string, cachedMtime time.Time, hit bool)

ListDirectoryContent() (core/engine.go) validates the cache before returning it:

dirInfo, statErr := os.Stat(path)
if cached, cachedMtime, hit := e.cache.GetDirectory(path); hit {
// Accept cache only if the directory hasn't changed on disk since it was cached.
if statErr == nil && !dirInfo.ModTime().After(cachedMtime) {
return cached, nil // cache is fresh
}
// External write detected — invalidate and re-read from disk.
e.cache.InvalidateDirectory(path)
}

The os.Stat() call adds one syscall (~1 µs) on every cache hit, which is negligible compared to the correctness benefit.

Before:

mcp_list (dir is empty) → cached listing: ""
bash cp writes 2 files → cache unaware
mcp_list (cache hit) → returns "" ← stale

After:

mcp_list (dir is empty) → cached listing: "", mtime=T0
bash cp writes 2 files → directory mtime=T1 > T0
mcp_list (cache hit) → os.Stat() mtime=T1 > T0 → invalidate → re-read → returns 2 files ✓

FileChange
core/path_detector.goAdded getDefaultWSLDistro() with sync.Once caching
core/engine.goAdded Linux-path branch in NormalizePath(); mtime validation in ListDirectoryContent()
cache/intelligent.godirCacheEntry struct; updated SetDirectory() and GetDirectory() signatures

go build ./... → OK (no errors)
go test ./tests/... ./core/ → PASS (all suites)

  1. filepath.Clean() is not path-format-agnostic: On Windows it converts forward slashes to backslashes, silently transforming a valid Linux path into a broken Windows path. Path format detection must happen before calling any filepath function.
  2. TTL alone is not enough for external-write scenarios: Any process outside the MCP server can invalidate the cache. Mtime comparison is a cheap, reliable way to detect this without needing a file watcher registration for every cached directory.
  3. sync.Once for expensive environment detection: WSL distro lookup involves an exec call — caching it once avoids per-call overhead on a hot path like NormalizePath().