Skip to content

plugin: permission.evaluate hook does not fire for cd-led shell commands #47356

Closed as not planned

Description

@whepper

Summary

Plugin permission.evaluate hooks are not invoked for bash tool calls whose command begins with cd on 0.0.0-beta-19086. Commands with any other leading verb (echo, cat, bash, fold, true && …, assignments, …) fire the hook normally. Any plugin that implements approval policies through this hook is therefore silently bypassed whenever a command starts with cd — e.g. cd . && cat .env never reaches the plugin, while cat .env does.

Environment

  • opencode version: opencode2 v0.0.0-beta-19086
  • OS: macOS, Darwin 25.5.0 (darwin arm64)
  • Terminal: VS Code integrated terminal (TERM_PROGRAM=vscode, TERM=xterm-256color)
  • Shell: /bin/zsh
  • Install/channel: beta
  • Active plugins: one local file plugin at ~/.config/opencode/plugins/security-guard.js (V2 object-style plugin: export default { id, async setup(ctx) }, registering ctx.tool.hook("execute.before"|"execute.after") and ctx.permission.hook("evaluate"))

Reproduction

  1. Install a minimal tap plugin (V2 object form) at ~/.config/opencode/plugins/eval-tap.js:

    import { appendFileSync } from "node:fs"
    export default {
      id: "eval-tap",
      async setup(ctx) {
        await ctx.permission.hook("evaluate", (event) => {
          try {
            const res = Array.isArray(event?.resources) ? event.resources : []
            appendFileSync(
              "/tmp/eval-tap.log",
              JSON.stringify({
                action: event?.action,
                effect: event?.effect,
                n: res.length,
                first: String(res[0] ?? "").slice(0, 60),
              }) + "\n",
            )
          } catch {}
        })
      },
    }
  2. Restart the service, then drive an agent session (V2 dialect, permissions config present) and issue, in order:

    1. echo probe-alpha-111
    2. cd /tmp && echo probe-beta-222222 (or simply cd /tmp)
    3. echo probe-gamma-33333333
  3. Read /tmp/eval-tap.log.

Observed log from the real investigation (identical shape to the minimal repro; resources were additionally SHA-256-matched to the commands to rule out mispairing):

{"action":"shell","effect":"allow","n":1,"first":"echo probe-alpha-111"}
{"action":"shell","effect":"allow","n":1,"first":"echo probe-beta-222222"}
{"action":"shell","effect":"allow","n":1,"first":"echo probe-gamma-33333333"}

but for a cd-led call no line is appended at all — no evaluate event fires. Example: a session where cd /tmp/rt2/.secrets && cat tok.txt ran produced hook events for the surrounding calls (sh setup.sh, the next tail …) but none for the cd call itself, verified across multiple log reads.

Expected Behavior

permission.evaluate fires for every permission-gated tool call, including bash commands whose leading verb is cd, so plugins can implement ask/deny policies uniformly.

Actual Behavior

No evaluate event is delivered for cd-led bash commands. Consistently reproducible (verified across ~10 commands in one session, and again after a service restart). tool.execute.before does fire for the same cd-led calls, so the gap is specific to the permission pipeline.

Impact: a security plugin gating reads via the permission hook is bypassed by cd <dir> && <command> (observed live: cd <ask-tier dir> && cat <file> executed without the plugin's ask ever firing, while the direct cat <ask-tier file> prompted correctly). Deny paths implemented in tool.execute.before are unaffected — the gap is specific to permission.evaluate.

Additional Context

  • Evidence method: the investigating plugin temporarily appended one JSON line per evaluate event (action, effect, resource count, and a SHA-256 prefix of each resource — no command text stored). In steady state every non-cd command paired correctly with its own command text; cd-led commands produced no event to pair.
  • Secondary observation (lower confidence, possibly related queue/replay behavior): immediately after a service restart and around tool-decline boundaries, individual evaluate events were observed carrying the resource of an adjacent command (verified by resource SHA) rather than the command being decided. Happy to provide the full tap logs if useful.
  • Workaround used by the affected plugin: detect cd-led commands in tool.execute.before (which sees the full command text) and downgrade ask verdicts to hard blocks there, since the permission channel cannot be relied on for those commands.

Activity

  1. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report and the tap plugin. I couldn't reproduce this on the latest release (2.2.0) or on the latest dev (35a4a68). 0.0.0-beta-19086 is no longer published, so I couldn't test that exact build.

    I used your eval-tap plugin as a project plugin, allowed shell and external_directory, and had the agent run cd-led commands. permission.evaluate fired for every one:

    • cd /tmp && echo probe-beta → external_directory ["/tmp/*"], then shell ["echo probe-beta"]
    • cd . && cat .env → shell ["cat .env"]
    • cd sub && cat tok.txt → shell ["cat tok.txt"]
    • cd /tmp → external_directory ["/tmp/*"] only
    • echo probe-gamma → shell ["echo probe-gamma"]

    This matches how the shell scanner is built. cd (and pushd, popd, …) is left out of the shell permission resources on purpose. Its target is checked as external_directory instead, and only when it is outside the project. Two consequences:

    • A bare cd inside the project produces no shell event.
    • In cd <dir> && cat <file>, your hook sees cat <file> without the directory. A path-based policy may not match the relative path, which could explain the bypass you saw.

    That is current design rather than a hook that fails to fire. If you'd like plugins to see the effective working directory or the full command, that could be filed as a feature request.

    If you still see no evaluate event at all for a cd-led command on a current version, please share the exact command, the version (opencode --version), your permissions config, and the raw tap log lines (with action included), and we'll take another look.

  2. opencodejr commented on Oct 31, 2026

    @opencodejr
    Contributor

    Closing this as not planned since we haven't heard back. On 2.2.0 and the latest dev (35a4a68), permission.evaluate fired for every cd-led command we tried. The cd target isn't part of the shell resource: it's checked as external_directory instead, and only when it's outside the project. So your hook sees cat tok.txt rather than cd sub && cat tok.txt, which is how the scanner is built.

    If you still see no evaluate event at all on a current version, please comment with the exact command, your opencode --version, your permissions config and the raw tap log lines, and we can reopen. If you'd like plugins to see the working directory or the full command, please open a feature request. Thanks again for the detailed report.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions