CommandCodeAI / CommandCodeAI/command-code

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

Aperta
#881 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Lingua principale
Nessun dato sulla lingua
Stelle
4k
Fork
350
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Descrizione

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.

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia in src/permissions/workspace.ts tracciando getCanonicalPath, canonicalizeForLookup e isWithinRoot; poi esamina il ramo allowExternalDirectory e formatOutsideWorkspaceMessage nel motore delle autorizzazioni. Riproduci il percorso con variazioni di maiuscole e minuscole su macOS e aggiungi la copertura di unit test suggerita: un percorso di workspace esistente con variazioni di maiuscole e minuscole dovrebbe essere considerato interno, mentre un percorso realmente esterno deve rimanere esterno e non creare una root consentita duplicata.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
node.js, typescript
Ambito
cli, security
Tipo di issue
Bug
Difficoltà
3/5
Tempo stimato
1-2 giorni
Stato di attività
Attiva
Chiarezza
Specificata chiaramente
Idoneità per principianti
78/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.