anomalyco / anomalyco/opencode

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

Open
#47,356 0 comments 0 reactions 1 assignee View on GitHub

@neriousy is already working on this.

Since Sep 4, 2026.

2.0
Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.