Skip to content

Permission rules match rewritten shell commands after tool.execute.before hooks #35882

Open

Description

@kfstorm

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

OpenCode bash permission rules currently appear to be evaluated against the shell command after tool.execute.before plugins mutate args.command.

This makes permission rules written for the original command stop matching when a plugin transparently wraps or rewrites that command.

Repro

  1. Configure a bash permission rule for the original command:
{
  "permission": {
    "bash": {
      "grep *": "allow"
    }
  }
}
  1. Install a plugin that rewrites shell commands in tool.execute.before, for example:
return {
  "tool.execute.before": async (_input, output) => {
    const args = output?.args
    if (!args || typeof args !== "object") return
    const command = (args as Record<string, unknown>).command
    if (typeof command === "string") {
      ;(args as Record<string, unknown>).command = `rtk ${command}`
    }
  },
}
  1. Ask OpenCode to run:
grep foo .

Actual behavior

The permission matcher sees the rewritten command:

rtk grep foo .

So the existing rule does not match:

"grep *": "allow"

Users need to add a second rule for the plugin-mutated command:

"rtk grep *": "allow"

Expected behavior

Permission evaluation should be able to use the original tool input:

grep foo .

Execution can still use the plugin-mutated command:

rtk grep foo .

That preserves the user's permission policy while still allowing plugins to optimize, wrap, trace, or otherwise transform execution.

Why this matters

Plugins that transparently wrap shell commands should not invalidate existing permission policies. This is especially noticeable for command-optimizing plugins such as RTK, but the same issue applies to any tool.execute.before plugin that mutates bash args.command.

Without this separation, users must duplicate permission rules for every original command and every rewritten wrapper form, which is harder to maintain and easier to get wrong.

Possible design

One possible execution flow:

  1. Capture the original tool input before plugin mutation.
  2. Run tool.execute.before hooks and allow mutation.
  3. Evaluate permissions against the original input, or expose both original and rewritten inputs to permission evaluation.
  4. Execute the rewritten input.

For bash, the permission resource could remain the original command while the actual process execution uses the mutated command.

Related issues

This is related to, but different from:

Those would improve the prompt-generated pattern, but they do not preserve existing permission rules like grep * after a tool.execute.before plugin rewrites grep foo . into rtk grep foo ..

Real-world example

RTK's official OpenCode integration installed via:

rtk init -g --opencode

creates a plugin that uses tool.execute.before and rewrites commands such as:

grep foo .

into:

rtk grep foo .

After that rewrite, permission patterns like grep * no longer auto-allow the command.

Activity

  1. 7emotions commented on Aug 30, 2026

    @7emotions

    Just hit this same root cause in a real setup: the rtk CLI rewrites git push --force origin main → rtk git push --force origin main in tool.execute.before, so permission rules written for git push --force * stop matching the rewritten command. Filed #46250 with the arity-specific angle.

    Two complementary fixes would resolve this whole class of issue:

    1. Evaluate permission rules against the original command (before tool.execute.before mutation), or expose the original command alongside the rewritten one when matching. This fixes the root cause for any wrapper/rewriter plugin, not just rtk.

    2. Make the arity dictionary configurable (per [FEATURE]: Make bash arity dictionary (BashArity) configurable #46250): the "Always allow" pattern is BashArity.prefix(tokens) + " *", and for unknown wrappers it falls back to the first token (rtk *), which is dangerously broad — approving one rtk command silently approves every rtk command, including rtk gh pr review --comment. A config knob (e.g. "rtk git": 3, "rtk gh": 4) or a prefix() that resolves arity past wrapper tokens would scope it to rtk git push * / rtk gh pr review *.

    Happy to consolidate the discussion here and close #46250 as a duplicate of this.

  2. llucax commented on Sep 14, 2026

    @llucax

    Same setup here (rtk plugin + permission.bash rules), and I'd add one more consequence beyond the rules not matching: every gh */git push * rule has to be duplicated as rtk gh */rtk git push *, and the duplication silently goes stale whenever the rewriter changes what it wraps.

    Working around it inside the plugin isn't an option for me either, because the rewrite should stay visible in the recorded command (you may want to rerun a rewritten command for its full output), so the fix really has to be on the matching side: evaluate against the pre-hook command, or add a post-permission hook. At v1.18.30 the only hook after ask() is shell.env (https://github.com/anomalyco/opencode/blob/v1.18.30/packages/opencode/src/tool/shell.ts#L417-L421), which can't touch the command.

    The "always" arity fallback to rtk * mentioned above makes this worse: one "always" approves every rtk command.

    Related:

  3. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed write-up! The request: when a tool.execute.before plugin (like RTK's) rewrites a shell command, check permission rules against the original command (grep foo .) but still run the rewritten one (rtk grep foo .), so you don't have to keep two copies of every rule.

    Your example uses the v1 plugin API. v2 currently works the same way: both the tool execute.before hook and the shell create.before hook run before the permission check. Rules are matched against the rewritten command there too, and no plugin hook can change the command after approval. So a plugin can't work around this today, and I've passed it on as a v2 feature request. Maintainers will consider it, but I can't promise when or how it will be handled.

    Related issues:

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