Repository navigation
Description
Summary
The save scope shown by the external_directory permission prompt is taken from Project.root, which searches upward for a .git/.hg marker with no upper bound. When the marker is found above the project, always allow persists a grant covering the entire repository — which for a repository in $HOME means the whole home directory.
The prompt asks for one directory, but offers a much wider scope to store.
Where
packages/core/src/file-access.ts:
save: slash(path.join((yield* Project.root(fs, directory)) ?? directory, "*")),and packages/core/src/project.ts:
export const root = Effect.fn("Project.root")(function* (
fs: FSUtil.Interface,
input: AbsolutePath,
markers: readonly string[] = [".git", ".hg"],
) {
return yield* fs.up({ targets: [...markers], start: input, mode: "first" }).pipe(
Effect.map((matches) => (matches[0] ? AbsolutePath.make(path.dirname(matches[0])) : undefined)),
Effect.orElseSucceed(() => undefined),
)
})fs.up walks to the filesystem root without stopping. There is no check that the resulting scope is within the project that owns the session.
Reproduction
-
Create a git repository in the home directory:
cd ~ && git init
(any enclosing repository works — a dotfile repo, a VCS repo in
$HOME, an accidentally initialised directory) -
Create a project somewhere below it, also a git repository:
mkdir -p ~/kit/org/project && cd ~/kit/org/project && git init
-
Start OpenCode in
~/kit/org/projectand have the agent touch a path outside the project but still below home, e.g. a sibling checkout:ls ~/kit/org/sibling -
The prompt shows:
Permission required ← Access external directory ~/kit/org/sibling This will always allow the following patterns for this project. - /home/user/*
Accepting it writes a saved external_directory grant of /home/user/* for that project. rm -rf ~ would then no longer require approval.
Note that this is also why the ask is repeated: the stored grant covers the wrong scope, so narrower paths still need their own decision, and a later prompt may again offer the same over-wide pattern.
Why config rules cannot prevent it
save is computed in resolve() before any rule is evaluated, so a permissions entry in opencode.json can only decide whether the prompt appears at all — never what it would store. Permission.evaluate (packages/core/src/permission.ts:87-97) uses findLast, which has no notion of specificity, so a narrow allow also cannot narrow a later prompt.
Suggested fix
Widen to the repository only when it is at or below the project's own directory. A repository above the project is not part of the project's trust boundary, so the ask should stay exactly as narrow as the tool requested.
const saveScope = Effect.fn("FileAccess.saveScope")(function* (fs: FSUtil.Interface, directory: AbsolutePath) {
const root = yield* Project.root(fs, directory)
if (root === undefined) return directory
if (FSUtil.contains(root, location.project.directory)) return directory
return AbsolutePath.make(root)
})and at the call site:
- save: slash(path.join((yield* Project.root(fs, directory)) ?? directory, "*")),
+ save: slash(path.join(yield* saveScope(fs, directory), "*")),The widening behaviour inside a project is deliberately preserved: a repository at or below location.project.directory is still used, so repeated edits within a project keep the existing one-time prompt.
An alternative worth considering is clamping inside Project.root itself, but that function is also used for project identity resolution, where the enclosing-repository behaviour is presumably intended. Clamping at the permission boundary keeps the two concerns separate.
Notes
packages/core/src/filesystem/protected.tsalready has anisHome()helper that would cover the$HOMEcase directly. The direction-aware check above is more general, but aisHomeshort-circuit on top would be cheap.- Observed on Linux; the same walk applies on macOS and Windows via
fs.up, so a repository in$HOME(or%USERPROFILE%) reproduces it there too.
Environment
OpenCode v2 from origin/v2, commit 1243d09dbc ("tui: update OpenTUI to 0.5.13").
Activity
github-actions commentedon Oct 2, 2026
This issue does not fully meet our contributing guidelines.
What needs to be fixed:
Please edit this issue to address the above within 72 hours, or it will be automatically closed.
This issue might be a duplicate of existing issues. Please check:
alwayspermission scopes; confirm whether it also covers thisexternal_directoryrepository-root case.If you believe this was flagged incorrectly, please let a maintainer know.
github-actions commentedon Oct 5, 2026
This issue has been automatically closed because it was not updated to meet our contributing guidelines within the 72-hour window.
Feel free to open a new issue that follows our issue templates.