Skip to content

permissions: external_directory save scope widens to a repository above the project, offering $HOME as a standing grant #52715

Closed as not planned

Description

@lenucksi

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

  1. 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)

  2. Create a project somewhere below it, also a git repository:

    mkdir -p ~/kit/org/project && cd ~/kit/org/project && git init
  3. Start OpenCode in ~/kit/org/project and have the agent touch a path outside the project but still below home, e.g. a sibling checkout:

    ls ~/kit/org/sibling
  4. 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.ts already has an isHome() helper that would cover the $HOME case directly. The direction-aware check above is more general, but a isHome short-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

  1. github-actions commented on Oct 2, 2026

    @github-actions
    Contributor

    This issue does not fully meet our contributing guidelines.

    What needs to be fixed:

    • Please use the Bug Report template and place the report in its required Description field. The current body uses a custom Summary section, so it does not include the required template field.

    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:

    • #37283: related report about broad always permission scopes; confirm whether it also covers this external_directory repository-root case.

    If you believe this was flagged incorrectly, please let a maintainer know.

  2. github-actions commented on Oct 5, 2026

    @github-actions
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions