Repository navigation
Description
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
- Configure a bash permission rule for the original command:
{
"permission": {
"bash": {
"grep *": "allow"
}
}
}- 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}`
}
},
}- 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:
- Capture the original tool input before plugin mutation.
- Run
tool.execute.beforehooks and allow mutation. - Evaluate permissions against the original input, or expose both original and rewritten inputs to permission evaluation.
- 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:
- [FEATURE]: add rtk to bash arity dictionary #29311, which requests adding
rtkto the bash arity dictionary so generatedalways allowpatterns are less broad. - [FEATURE]: Allow editing the "always" permission pattern before confirming #24795, which requests editing generated
always allowpatterns before confirming.
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 --opencodecreates 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
7emotions commentedon Aug 30, 2026
Just hit this same root cause in a real setup: the
rtkCLI rewritesgit push --force origin main→rtk git push --force origin mainintool.execute.before, so permission rules written forgit push --force *stop matching the rewritten command. Filed #46250 with the arity-specific angle.Two complementary fixes would resolve this whole class of issue:
Evaluate permission rules against the original command (before
tool.execute.beforemutation), or expose the original command alongside the rewritten one when matching. This fixes the root cause for any wrapper/rewriter plugin, not justrtk.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 onertkcommand silently approves everyrtkcommand, includingrtk gh pr review --comment. A config knob (e.g."rtk git": 3,"rtk gh": 4) or aprefix()that resolves arity past wrapper tokens would scope it tortk git push */rtk gh pr review *.Happy to consolidate the discussion here and close #46250 as a duplicate of this.
llucax commentedon Sep 14, 2026
Same setup here (
rtkplugin +permission.bashrules), and I'd add one more consequence beyond the rules not matching: everygh */git push *rule has to be duplicated asrtk 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()isshell.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 everyrtkcommand.Related:
opencodejr commentedon Oct 28, 2026
Thanks for the detailed write-up! The request: when a
tool.execute.beforeplugin (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
toolexecute.beforehook and theshellcreate.beforehook 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:
tool.execute.beforerewritesrtk, so "always allow" doesn't fall back tortk *