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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions