Description
After #16651, Instance.directory is always the canonical (symlink-resolved) path. But the filepath argument to containsPath() comes from various tool callers (bash.ts, external-directory.ts, file/index.ts) and is not canonicalized. Similarly, Instance.worktree may not be canonical.
This means a symlinked path passed to containsPath() won't match the canonical Instance.directory, causing:
bash.ts:89,129 — false negatives on the boundary check, triggering unnecessary external_directory permission prompts for paths that are actually inside the project
external-directory.ts:17 — same false negative, prompting for permission when none is needed
file/index.ts:503,585 — File.read() and File.tree() incorrectly reject symlinked paths within the project
The existing TODO comments at file/index.ts:501 and :583 acknowledge this as a known limitation ("lexical containment check"). #16651 makes it slightly more pronounced because Instance.directory is now always canonical while inputs remain uncanonicalized.
A fix would be to canonicalize the filepath argument inside containsPath():
containsPath(filepath: string) {
const resolved = Filesystem.resolve(filepath)
if (Filesystem.contains(Instance.directory, resolved)) return true
if (Instance.worktree === "/") return false
return Filesystem.contains(Instance.worktree, resolved)
}
Steps to reproduce
- Create a symlink to a project directory:
ln -s ~/myproject ~/myproject-link
- Run
opencode from the symlinked path: cd ~/myproject-link && opencode
- Use a tool that accesses a file via its symlinked path (e.g., bash tool with
cwd set to the symlink path, or file read via symlink)
- Observe unnecessary
external_directory permission prompts for paths that are actually inside the project
Operating System
macOS, Linux (anywhere symlinks are common)
Description
After #16651,
Instance.directoryis always the canonical (symlink-resolved) path. But thefilepathargument tocontainsPath()comes from various tool callers (bash.ts,external-directory.ts,file/index.ts) and is not canonicalized. Similarly,Instance.worktreemay not be canonical.This means a symlinked path passed to
containsPath()won't match the canonicalInstance.directory, causing:bash.ts:89,129— false negatives on the boundary check, triggering unnecessaryexternal_directorypermission prompts for paths that are actually inside the projectexternal-directory.ts:17— same false negative, prompting for permission when none is neededfile/index.ts:503,585—File.read()andFile.tree()incorrectly reject symlinked paths within the projectThe existing TODO comments at
file/index.ts:501and:583acknowledge this as a known limitation ("lexical containment check"). #16651 makes it slightly more pronounced becauseInstance.directoryis now always canonical while inputs remain uncanonicalized.A fix would be to canonicalize the
filepathargument insidecontainsPath():Steps to reproduce
ln -s ~/myproject ~/myproject-linkopencodefrom the symlinked path:cd ~/myproject-link && opencodecwdset to the symlink path, or file read via symlink)external_directorypermission prompts for paths that are actually inside the projectOperating System
macOS, Linux (anywhere symlinks are common)