macOS: a path that differs only in letter case from the workspace root is flagged as outside the workspace

Ouverte
#881 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Évaluation

Difficulté
3/5
Temps estimé
1-2 jours
Accessibilité débutants
78/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
Active
Stack technique
node.js, typescript
Domaine
cli, security

Piste de recherche

Commencez dans src/permissions/workspace.ts en retraçant getCanonicalPath, canonicalizeForLookup et isWithinRoot ; examinez ensuite la branche allowExternalDirectory et formatOutsideWorkspaceMessage dans le moteur de permissions. Reproduisez le chemin avec une casse différente sur macOS et ajoutez la couverture de tests unitaires suggérée : un chemin de workspace existant avec une casse différente doit être considéré comme interne, tandis qu’un chemin réellement externe doit rester externe et ne pas créer de root autorisé en double.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

Summary

On macOS (case-insensitive filesystem), a file tool call whose absolute path differs only in
letter case from the workspace root is treated as outside the workspace and raises the
external-directory prompt, even though the path resolves to a file inside the project. Every
occurrence costs a human approval, and approving one grants the case-variant directory, so the
session root set slowly accumulates duplicates of the real root (visible in the prompt's
allowed: list).

Expected Behavior

A path that names an existing file inside the workspace — regardless of letter case, on a
case-insensitive volume — should be recognized as inside the workspace: no external-directory
prompt, no directory grant.

Actual Behavior

Workspace root /Users/dev/Projects/my-app (capital P, macOS). A grep call for
/Users/dev/projects/my-app/apps/web/README.md (lowercase projects) triggers:

Allow Command Code to use GREP on this file?
This action accesses a path outside the current project.
/Users/dev/projects/my-app/apps/web/README.md is outside workspace
(allowed: /Users/dev/Projects/my-app, /Users/dev/.commandcode, /tmp, /private/tmp, /var/folders, …)

Both spellings resolve to the same inode.

Steps to reproduce the issue

  1. On macOS, start a session in a workspace whose absolute path contains at least one uppercase
    letter, e.g. /Users/dev/Projects/my-app.
  2. Trigger a file tool call on the same path spelled in lowercase (this happens organically
    because the model occasionally re-types absolute paths instead of copying them):
    • grep with path: "/Users/dev/projects/my-app/apps/web/README.md", or
    • read_file / edit_file with the same lowercased path.
  3. The external-directory gate fires (risk: { kind: "outside-workspace" }) and the call waits
    for human approval.
  4. Approving grants the lowercased directory, so the allowed-roots list grows with case-variant
    entries that duplicate the real root.

Root cause

src/permissions/workspace.ts (bundled in @commandcode/harness):

function getCanonicalPath(dirPath: string) {
	try {
		return process.platform === 'win32' ? fs.realpathSync.native(dirPath) : fs.realpathSync(dirPath);
	} catch {
		return path.normalize(dirPath);
	}
}

canonicalizeForLookup() feeds this into isWithinRoot(), which decides containment with
path.relative(canonicalRoot, canonicalPath) — a case-sensitive string comparison.

On darwin, fs.realpathSync (the JS implementation — .native is only used for win32)
preserves the case of the input for every component that is not a symlink, so a lowercased path
stays lowercased. The native variant goes through libuv's uv_fs_realpathrealpath(3) and
returns the on-disk spelling.

Reproduction without Command Code (Node on macOS):

$ node -e 'const fs=require("fs"),path=require("path");const p="/users/dev/projects/my-app/apps/web/README.md";
console.log("js    ", fs.realpathSync(p));
console.log("native", fs.realpathSync.native(p));
console.log("rel   ", path.relative("/Users/dev/Projects/my-app", p));'
js     /users/dev/projects/my-app/apps/web/README.md
native /Users/dev/Projects/my-app/apps/web/README.md
rel    ../../../../users/dev/projects/my-app/apps/web/README.md   # → isWithinRoot() === false

Notes: no user-side config fully fixes this

  • deny/ask rule matching already folds case on macOS/Windows, but the workspace boundary does not.
  • The external-directory gate runs before the allow rules, so an allow rule cannot silence it.
  • permissions.additionalDirectories is the only workaround, and it is itself case-sensitive —
    one entry per observed case variant.
  • Mods cannot help either: cmd.hooks.beforeToolCall runs after permissions.check.

Suggested fix

Use the native realpath wherever it exists, or fold case only when the volume is
case-insensitive:

return fs.realpathSync.native?.(dirPath) ?? fs.realpathSync(dirPath);

The requirement is that two spellings of the same existing path land on the same side of the
workspace boundary.

Command Code Version

1.53.1

Operating System

macOS

Additional context

  • macOS 27.0 (APFS, case-insensitive), Node v24.19.0, Command Code 1.53.1.
  • Code locations: getCanonicalPath / canonicalizeForLookup / isWithinRoot in
    src/permissions/workspace.ts; the gate is the allowExternalDirectory branch of the
    permission engine (risk: { kind: 'outside-workspace' }, message built by
    formatOutsideWorkspaceMessage).
  • A fix would also keep the prompt's allowed: list clean: today it mixes both spellings of the
    same directory once a case-variant path has been approved.

Fix prompt (optional)

In the harness permission engine, make workspace containment case-insensitive for existing paths
on case-insensitive volumes: getCanonicalPath should call fs.realpathSync.native whenever it
is available (not only on win32), so /users/dev/projects/my-app/... canonicalizes to
/Users/dev/Projects/my-app/.... Add a unit test asserting that a case-variant spelling of an
existing workspace path returns isPathInWorkspace() === true, while a genuinely external path
still returns false.

Langage dominant
Aucune donnée de langage
Étoiles
4k
Forks
350
Métriques de merge des PR
Aucune PR mergée en 30 j

Guide de contribution

Aucun guide de contribution indexé pour ce dépôt

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de CommandCodeAI/command-code

Toutes les issues de CommandCodeAI/command-code

Issues similaires

Plus d'issues CLI

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.