Skip to content

resolveEncodedProjectPath fails when directory names contain underscores or hyphens #17

@scrambled2

Description

@scrambled2

Bug Description

resolveEncodedProjectPath in scanner.mjs fails to resolve many project directories, causing them to silently disappear from the sidebar. On a machine with 45 project directories in ~/.claude/projects/, only 9 were displayed.

Root Cause

Claude Code's path encoding replaces both / (path separators) and _ (underscores) with - — but inconsistently across versions. Some older project dirs preserve underscores, newer ones replace them with hyphens. This makes the encoding lossy and ambiguous.

The current greedy resolver (scanner.mjs:161) splits the encoded name on - and tries to reconstruct the path by joining segments with hyphens and checking exists(). This fails in two ways:

1. Underscore directories never match

A directory named My_Projects gets encoded as My-Projects. The resolver tries My-Projects against the filesystem — but the actual directory is My_Projects. No match, resolution fails.

2. Greedy matching dead-ends without backtracking

For a path like DriveRoot/Parent_Dir/my-repo, encoded as X--DriveRoot-Parent-Dir-my-repo:

  • Segments: ["DriveRoot", "Parent", "Dir", "my", "repo"]
  • Resolver matches DriveRoot/ at level 1 ✓
  • At level 2, tries Parent-Dir-my-repo, Parent-Dir-my, Parent-Dir — none exist (actual name is Parent_Dir)
  • Falls back to single segment Parent — doesn't exist either
  • Path reconstruction fails, exists() returns false, project is hidden

Suggested Fix

Replace the greedy algorithm with one that:

  1. Lists actual directory entries at each level instead of guessing paths
  2. Normalizes both candidate and directory names (replace _ with -, case-insensitive) before comparing
  3. Uses DFS with backtracking so ambiguous splits don't dead-end

Here's a working replacement for resolveEncodedProjectPath:

async function resolveEncodedProjectPath(encoded) {
  const segments = encoded.replace(/^-/, "").split("-");
  let rootPath = "/";
  let startIdx = 0;

  if (platform() === "win32" && segments.length >= 2 && segments[0].length === 1 && segments[1] === "") {
    rootPath = segments[0].toUpperCase() + ":\\";
    startIdx = 2;
  }

  // Normalize for comparison: lowercase, replace _ with -
  const norm = (s) => s.toLowerCase().replace(/_/g, "-");

  // DFS resolver with backtracking
  async function resolve(currentPath, i) {
    if (i >= segments.length) {
      return (await exists(currentPath)) ? currentPath : null;
    }

    let entries;
    try {
      entries = await readdir(currentPath, { withFileTypes: true });
      entries = entries.filter(e => e.isDirectory());
    } catch {
      return null;
    }

    // Map normalized names → actual names
    const entryMap = new Map();
    for (const e of entries) {
      const key = norm(e.name);
      if (!entryMap.has(key)) entryMap.set(key, []);
      entryMap.get(key).push(e.name);
    }

    // Try longest match first, backtrack on failure
    for (let end = segments.length; end > i; end--) {
      const candidate = norm(segments.slice(i, end).join("-"));
      const matches = entryMap.get(candidate);
      if (matches) {
        for (const actualName of matches) {
          const nextPath = join(currentPath, actualName);
          const result = await resolve(nextPath, end);
          if (result) return result;
        }
      }
    }

    return null;
  }

  return resolve(rootPath, startIdx);
}

This resolves all 45 project directories correctly on the affected machine. The readdir calls add minimal overhead since project paths are typically only 3-5 levels deep.

Environment

  • OS: Windows 11
  • CCO version: 0.10.3
  • Node: v20.19.0

Reproduction

Any Windows user whose repositories live in directories containing underscores (e.g., My_Projects, CORE_REPOS) or hyphens in folder names will see those projects silently missing from the sidebar. The issue also affects Linux/macOS for the same encoding ambiguity.

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