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

未关闭
#881 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
3/5
预计耗时
1-2 天
新手友好度
78/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
node.js, typescript
领域
cli, security

调研方向

从 src/permissions/workspace.ts 开始,跟踪 getCanonicalPath、canonicalizeForLookup 和 isWithinRoot;然后检查 permission engine 中的 allowExternalDirectory 分支和 formatOutsideWorkspaceMessage。在 macOS 上复现大小写不同的路径,并添加建议的单元测试覆盖:现有的大小写不同的 workspace 路径应位于内部,而真正的外部路径应保持在外部,并且不会创建重复的允许 root。

由索引模型根据 Issue 内容生成。

描述

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.

主要语言
没有语言数据
星标
4k
派生
350
PR 合并指标
30 天内没有已合并 PR

贡献指南

这个仓库没有索引到贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

CommandCodeAI/command-code 的其他 Issue

查看 CommandCodeAI/command-code 的全部 Issue

相似的 Issue

更多 CLI Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。