Skip to content

fix(hooks): judge OpenCode against its own permission rules - #4349

Merged
KuSh merged 4 commits into
developfrom
fix/opencode-native-permission-host
Oct 5, 2026
Merged

KuSh merged 4 commits into
developfrom
fix/opencode-native-permission-host

Conversation

@aeppling

@aeppling aeppling commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Alternative to #4306 for #4195.

Why not #4306

#4306 injects rtk <pattern> rules into the user's effective config until the rewritten command passes the gate.

  • It edits the user's security policy with rules they never wrote and can't see in their config.
  • It can't cover structural rewrites (head -n 5 f → rtk read f --head-lines 5), where no rtk -prefixed pattern matches.
  • A deny on such a command therefore stays bypassed, while the added docs claim permissions are preserved.
  • opencode stays on ViaRewrite, judged against ~/.claude's rules rather than its own.

Direction

Decide on the command as typed, against OpenCode's own rules, and skip the rewrite whenever it would change that verdict. The user's config is never touched.

Before

git status → rtk git status → evaluated against "*": deny → BLOCKED

After

git status → verdict("git status") = allow, verdict("rtk git status") = deny
           → rtk hook opencode answers {} → OpenCode runs git status under the user's rule

opencode moves from ViaRewrite(ApprovalOwner::Rtk) to InProcess(Host::OpenCode) — the same shape as Claude, Codex, Cursor, Gemini, Droid, Trae and Vibe. Approval ownership is unchanged.

OpenCode's rules are an ordered list where the last match wins, which the deny/ask/allow buckets cannot express, so permissions_opencode reproduces its resolution for the global config (or OPENCODE_CONFIG) and the nearest project config. The existing evaluator is untouched — the other nine hosts are byte-identical.

OpenCode judges whatever command the plugin hands back, and from 1.1.4 on a plugin cannot change that verdict (there is no permission.ask plugin hook). So RTK returns a rewrite only when it leaves the verdict unchanged: an allow stays an allow, an ask stays a prompt, a deny stays denied. A command that loses its rewrite runs as typed, without the token savings.

Verified against real OpenCode installs

Plugin sessions on 1.1.4, 1.3.4 and 1.18.34 (rtk hook opencode on the pushed code for the rows below):

policy command rtk hook opencode OpenCode
"*": deny + "git status": allow git status {} runs git status
same rm -rf / {} denied
same git status && git push --force {} denied (#1213 holds)
"git push *": ask git push origin main {} prompts
"*": allow + "git push *": deny git push --force {} denied
"*": allow + "head *": deny head -n 5 f {} denied (no structural bypass via rtk read)
"git status": allow only cargo test {"command":"rtk cargo test"} no rule changes, rewrite kept
no rules ls -la {"command":"rtk ls -la"} rewritten

Open

@CLAassistant

CLAassistant commented Sep 29, 2026 •

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ KuSh
❌ Adrien EPPLING


Adrien EPPLING seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@rtk-wshm-sync-bot

Copy link
Copy Markdown

wshm · Automated triage by AI

📊 Automated PR Analysis

🐛 Type bug-fix
🔴 Risk high

Summary

Replaces the OpenCode command hook's approach of rewriting commands to satisfy ~/.claude permission rules with one that evaluates commands directly against OpenCode's own permission config (reimplementing its last-match-wins evaluator), then reports allow/ask/deny back to OpenCode via a new permission.ask handler. This moves opencode from ViaRewrite(ApprovalOwner::Rtk) to InProcess(Host::OpenCode), aligning it with the other native-hook hosts.

Review Checklist

  • Tests present
  • Breaking change
  • Docs updated

Linked issues: #4195, #1213, #1155


Analyzed automatically by wshm · This is an automated analysis, not a human review.

@KuSh KuSh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 1 of 3 — CHANGES REQUESTED

Claim: rtk hook opencode judges a bash command against OpenCode's own permission rules, and the plugin hands that verdict back to OpenCode through permission.ask, so a rewrite never changes what the user's rules decide (#4195).
Scope: accept if narrowed to "the OpenCode plugin judges the command against OpenCode's own root and project rules, and skips the rewrite whenever the rewrite would change that verdict". Cut: the status/permission.ask channel (no OpenCode release calls that hook, see 1) and agent-scoped rules (#4195 stays open for them). #4306 is an alternative fix for the same issue, so only one of the two should land. #4187 rewrites the same plugin body and lands after this one. #4331 moves crate::discover::lexer, so whichever lands second updates the import. (round 1, frozen)
Ran: OpenCode 1.1.4, 1.3.4 and 1.18.34 (plus a load check on 2.0.22), develop plugin vs PR plugin, 9 policies. A mock model issues one real bash call per session. Also ran rtk hook opencode on the PR table's 14 rows and rtk hook check --agent opencode, and counted permission.ask trigger sites in 6 OpenCode binaries (1.1.4 to 2.0.22): 0. Invariants: every decision (allow, ask, deny, no rule), under the hook (the plugin is the hook), merge onto develop. Savings: n/a (no filter change).
Pins: 33 decisions, 33 mutated, 19 survived (2 of them equivalent; the 17 that matter are listed in 3)
Matrix: never run before, executed this round; pin check: never run before, executed this round
CI @48b15a9a: all green (15/15), CLA pending (the commits' author email is not linked to a GitHub account). Merged onto develop 4f9015f: cargo test fails 1 (see 2).

Blocks merge (3, frozen at round 1)

  1. hooks/opencode/rtk.ts:55: the status channel is inert, so #4195's allowed command is still blocked. OpenCode never calls a permission.ask plugin hook: the binaries of 1.1.4, 1.3.4, 1.4.17, 1.14.41 and 1.18.34 contain no trigger site for it (their triggers are tool.execute.before, chat.params, shell.env, …). Evidence on 1.18.34, 1.3.4 and 1.1.4 with {"*": "deny", "git status": "allow"}:

    • The PR plugin receives {"command":"rtk git status","status":"allow"}.
    • OpenCode logs evaluated permission=bash pattern="rtk git status" … action.pattern=* action.action=deny and the tool returns the rule error, the same as develop.
    • An instrumented copy of the plugin recorded no permission.ask call. It recorded none on an ask either.

    With {"git push *": "ask"} the PR answers status: "ask", and OpenCode runs rtk git push origin main without prompting (develop does the same). On 2.0.22 neither plugin loads (Missing key default, pre-existing, #4187). What has to change is the fallback the PR body names, widened from "would downgrade" to "would change": when the rewrite's verdict differs from the typed command's verdict, return no rewrite. An ask turning into "no rule" is an upgrade to a silent run, and that is the ask bypass above. Rows a correct fix passes on 1.18.34:

    • the #4195 policy runs the typed git status
    • the reporter's policy at root level ("*": "deny", "npx ts-node*task-cli*": "allow") runs the typed npx …
    • git push *: ask prompts for git push origin main
    • *: allow with git push *: deny still denies
    • no rules, *: ask and *: allow still rewrite to rtk …

    permission.ask, the pending map and status then go.

  2. src/hooks/permissions_opencode.rs:170: the merge onto develop fails the test-isolation guards (#3758 landed after this branch's base). only_user_dirs_resolves_user_locations flags dirs::home_dir, env::current_dir and var_os("OPENCODE_CONFIG"). Once those go through user_dirs::home, user_dirs::current_dir and user_dirs::env_path, every_variable_rtk_reads_is_redirected_for_a_child asks for OPENCODE_CONFIG in INHERITED_VARS (src/core/test_isolation/scratch.rs). Evidence: the merge with 4f9015f gives 3859 passed, 1 failed.

  3. src/hooks/hook_cmd.rs:1200: unpinned behaviour. These mutations all pass the suite:

    • opencode_answer (no test at all): dropping the empty-command guard; {} from the Deny arm; a rewrite from the Defer arm; swapping the arguments of opencode_status(verdict, after); None instead of agent in the second lookup.
    • Routing: opencode back to ViaRewrite, or skipping the OpenCode branch of check_command_for_agent. Either way the plugin and rtk hook check --agent opencode can drift apart unnoticed.
    • Segment aggregation: the unattestable check moved above deny; Default returned for an ask; empty segments kept.
    • Config discovery: no project config; no agent block; agent block before root; OPENCODE_CONFIG added on top of global; .jsonc before .json; project before global. Nothing calls opencode_configs or load_opencode_rules in a test. After the rebase, user_dirs's test scratch makes that possible.

    Tests must cover:

    • opencode_answer after the change in 1: the rows in 1, as rules in and answer JSON out.
    • Discovery: a project opencode.json found from a subdirectory; global before project, with the project rule winning; OPENCODE_CONFIG.
    • The agent block after root, or drop --agent and the agent block until the plugin has an agent to pass.

    Each mutation above must turn one of them red.

Optional — will not hold merge

  • src/hooks/hook_cmd.rs:1194: /// Run the Factory Droid PreToolUse hook natively. now documents run_opencode, and run_droid lost its doc.
  • opencode_answer reloads every config file twice per call. Load the rules once and evaluate both commands against them.
  • !rules.is_empty() && in check_command_with_opencode_rules is redundant: with no rules, every action is None.
  • Several places still say the plugin calls rtk rewrite: hooks/opencode/README.md:8, the decision.rs module table and its identity-rewrite note (decision.rs:195), and the plugin's catch comment. The plugin's "Requires: rtk >= 0.23.0" is stale too: an older rtk on PATH now silently stops rewriting.

Filed as follow-ups

  • #4422 OpenCode permission rules: rtk's config loader diverges from OpenCode's config resolution — outside the narrowed claim; under it, each gap falls back to develop's behaviour. Filed (related to #4282).
  • #4195: agent-scoped rules (the reporter's setup) are still blocked with this PR, because tool.execute.before carries no agent. Comment added with the transcript.
  • OpenCode 2.x plugin loading: #4187 (pre-existing).

Checked and correct — no need to re-verify

  • wildcard_match matches 1.18.34's Wildcard.match: backslash normalisation, optional * tail, s flag. Last match wins, and the permission name is matched as a wildcard. 11 mutations on this code are killed.
  • {"*": "allow", "git push *": "deny"}, live on 1.18.34: develop rewrote to rtk git push --force and OpenCode ran it. This PR leaves the command as typed and OpenCode denies it.
  • Every row of the PR-body table reproduces through rtk hook opencode.
  • rtk hook check --agent opencode now reports OpenCode's verdict (develop reported Claude Code's).
  • With no rules, the rewrite still happens (rtk ls -la output reaches the model).
  • serde_json has preserve_order, so declaration order survives.
  • fmt, clippy and test on the head; CI 15/15.

Your questions

  • "The plugin can't pass --agent yet … Agent-scoped rules fall back to root config until that's sourced." → Deferred: #4195 stays open for agent-scoped rules. In this PR, drop --agent or pin it (3).
  • "permission.ask lifting a deny is not yet confirmed … the fallback is to skip the rewrite when it would downgrade the verdict" → Confirmed inert on every 1.x release, ask included. Fold in the fallback, using "would change" instead of "would downgrade" (1).

Next

Make three changes, then this merges:

  • Replace the status channel with skipping any rewrite that would change the verdict (rows in 1).
  • Rebase on develop with the three user_dirs reads and OPENCODE_CONFIG in INHERITED_VARS (2).
  • Add tests that kill the mutations in 3.

Rounds: 1/3. Threads: 3 open (waiting on the author), 0 resolved this round.

Comment thread hooks/opencode/rtk.ts Outdated
Comment thread src/hooks/permissions_opencode.rs Outdated
Comment thread src/hooks/hook_cmd.rs Outdated
Adrien EPPLING and others added 3 commits October 5, 2026 11:16
OpenCode never consults plugins through permission.ask — no release from
1.1.4 to 2.0.22 has a trigger site for it — so the status channel could
not deliver the verdict and #4195's allowed command stayed blocked, while
an ask rule could be silently bypassed by the rewrite un-matching it.

Replace the channel with the one mechanism that works on every version:
judge both the typed command and its rtk form against OpenCode's own
rules, and answer {} whenever the rewrite would change the verdict.
OpenCode then runs the typed command under the user's own policy; an
allow stays an allow, an ask stays a prompt, a deny stays denied. The
plugin loses permission.ask, the pending map and the status field.

Also post-rebase on develop: config discovery goes through user_dirs
(home, current_dir, env_path) and OPENCODE_CONFIG joins INHERITED_VARS,
so the test-isolation guards hold; rules load once per answer instead of
twice; and tests pin the answer rows, config discovery order, agent-block
ordering, in-process routing and the deny-before-unattestable aggregation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aeppling
aeppling force-pushed the fix/opencode-native-permission-host branch from 48b15a9 to 4ddc78d Compare October 5, 2026 09:16
@aeppling

aeppling commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

@KuSh — round 1 addressed in 4ddc78d5:

  1. Status channel replaced with skip-on-verdict-change ("would change", not "would downgrade"). permission.ask, the pending map and status are gone from the plugin. Cross-checked against the source: the hook is declared in the plugin SDK but has no plugin.trigger site at v1.18.34 or dev. Your five rows reproduce through rtk hook opencode.
  2. Rebased on develop — discovery goes through user_dirs, OPENCODE_CONFIG added to INHERITED_VARS. cargo test --all -- --include-ignored: 4153 passed, 0 failed.
  3. Mutations pinned — answer rows, config discovery and order, agent-after-root (kept --agent), routing, deny-before-unattestable. The empty-segment filter was dead code and is deleted.

Optional items done too. CLA: author email fixed in this push.

Note for #4187/#4422: 2.x merges .json+.jsonc, defaults unmatched to ask, and matches arity prefixes — permissions_opencode.rs will need a pass when 2.x plugin loading is fixed.

…aims

Reading the topmost project config instead of the nearest passed every test.
Name the OPENCODE_CONFIG test for what rtk does (OpenCode loads that file on
top of the global one), and limit the permission.ask note to the releases
without that hook: OpenCode 1.0.142 has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@KuSh KuSh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2 of 3 — APPROVED (after fixup bb6361f)

Claim: the OpenCode plugin judges a command against OpenCode's own root and project permission rules, and skips the rewrite whenever the rewrite would change that verdict (#4195).
Scope: accept if narrowed to "the OpenCode plugin judges the command against OpenCode's own root and project rules, and skips the rewrite whenever the rewrite would change that verdict" — the head now makes exactly this claim. (round 1, frozen)
Ran: OpenCode 1.1.4, 1.3.4 and 1.18.34, develop plugin vs this head's plugin. A mock model issues one real bash call per session, across 10 policies:

  • *: deny + git status: allow, on all three versions
  • the reporter's npx ts-node*task-cli* allow, at root level
  • git push *: ask
  • *: allow + git push *: deny
  • *: allow + head *: deny (a structural rewrite to rtk read)
  • no rules, *: ask, bash: allow
  • allow rules with no catch-all

Also: rtk hook opencode and rtk hook check --agent opencode on 15 rows. Invariants: every decision (allow, ask, deny, no rule), under the hook (the plugin is the hook), merge with develop (the head is on c356374). Savings: n/a (no filter change).
Pins: 21 decisions in the new code, 21 mutated, 4 survived. 3 are equivalent: the empty-command guard, empty actions → Allow, joining the args. The 4th, nearest vs topmost project config, is pinned by fixup bb6361f, whose 3 mutations are all killed. The matcher is unchanged since round 1, where its 11 mutations were killed.
Matrix: executed this round on 4ddc78d; pin check: executed this round
CI @bb6361f7: 16/16 green (4ddc78d was 16/16 too), CLA pending.

Blocks merge (3, frozen at round 1) — all closed

  1. rtk.ts:55, status channel: fixed. Live on 1.18.34, 1.3.4 and 1.1.4, the #4195 policy now runs the typed git status. The reporter's root-level npx policy runs the typed command. git push *: ask prompts for git push origin main; develop ran rtk git push origin main silently. Every row passes on this head:
    • git push *: deny still denies; develop ran rtk git push --force.
    • head *: deny denies head -n 1 a.txt; develop ran rtk read a.txt --head-lines 1.
    • No rules, *: ask and bash: allow still get the rewrite.
  2. permissions_opencode.rs:170, test isolation: fixed. fmt, clippy (0 warnings) and cargo test --all (4146 passed) are green on the head, which already sits on the develop tip.
  3. hook_cmd.rs:1200, unpinned behaviour: fixed. 17 of the 21 new mutations are killed. 3 of the 4 survivors cannot change an answer, and fixup bb6361f pins the last one.

Fixup bb6361f (pushed to the branch)

This commit changes only tests and one doc note: the release binary is byte-identical to 4ddc78d's.

  • New test: the_nearest_project_config_is_the_one_read. Reading the topmost project config instead of the nearest passed every test.
  • OPENCODE_CONFIG test renamed to opencode_config_env_is_read_instead_of_the_global_file, with a corrected message. OpenCode 1.18.34 loads the global file, then OPENCODE_CONFIG, then the project file (opencode debug config), so "as OpenCode does" was not true. That difference is part of #4422.
  • permission.ask note: OpenCode from 1.1.4 on has no such plugin hook, but 1.0.142 has Plugin.trigger("permission.ask", …). My round-1 line "OpenCode never calls a permission.ask plugin hook" was broader than what I had checked (1.1.4 on), and the note followed it.

Follow-up PR, right after this merges

We'll open it from the maintainer side. It will do the following:

  • One rule set per host, loaded once. OpenCode's ordered list currently sits beside load_rules_for(host) and overrides it: check_command_for_agent short-circuits it, and it returns empty lists for OpenCode. The bucket hosts and OpenCode will go through the same abstraction, loaded once and used for both the typed command and its rewrite.

  • rtk hook check --agent opencode will give the plugin's answer. On this head it prints rtk <cmd> with exit 0 where rtk hook opencode answers {}, in 5 cases:

    • {"*":"deny","git status":"allow"} with git status
    • {"git push *":"ask"} with git push origin main
    • the reporter's npx policy
    • {"git status":"allow"} with git status
    • {"rtk *":"ask"} with git status

    A table test comparing the two will pin it.

Filed as follow-ups

  • #4422: added a comment. rtk does not model OpenCode's built-in *: allow, so allow rules with no catch-all lose the rewrite for exactly the commands they allow. That costs savings, never a verdict.
  • #4195 stays open for agent-scoped rules (round 1).
  • OpenCode 2.x: #4187. Your note that 2.x merges .json + .jsonc, asks on unmatched commands and matches arity prefixes is the pass that PR has to make on permissions_opencode.rs.

Checked and correct — no need to re-verify

  • The skip is in the right place: 0 rows where the plugin changes a verdict, on 3 OpenCode versions.
  • split_for_permissions returns trimmed, non-empty segments (both push sites), so removing the filter is safe.
  • The plugin's logic is the round-1 narrowing exactly; only its comments differ.
  • The head is on the current develop tip, so it is its own merge check.

Your questions

  • "2.x merges .json+.jsonc, defaults unmatched to ask, and matches arity prefixes — permissions_opencode.rs will need a pass when 2.x plugin loading is fixed." → Agreed, and deferred to #4187, which lands after this.

Next

Fixup bb6361f is pushed and code-reviewed, and CI is green on it. It adds a test for the nearest project config and corrects one test message and one note, with no behaviour change. Approving. The CLA check still has to pass before merge.
The PR description is updated to match the narrowed design.
Rounds: 2/3. Threads: 0 open, 3 resolved this round.

@KuSh
KuSh merged commit cf018af into develop Oct 5, 2026
17 of 18 checks passed
@rtk-release-bot rtk-release-bot Bot mentioned this pull request Oct 5, 2026
@KuSh
KuSh deleted the fix/opencode-native-permission-host branch October 5, 2026 17:31
amnesiaof added a commit to amnesiaof/rtk that referenced this pull request Oct 5, 2026
…51.1

KuSh's review of rtk-ai#2426 named two gaps that also apply here: the plugin
decided usability from the file's existence, and set no minimum version.

existsSync only proves a file is there. A wrong-arch binary, a broken
install, or a stale RTK_BIN all pass that check and then fail on every
single tool call. Spawning `rtk --version` once per session is the check
that actually means something, and the answer is cached per binary.

The floor is 0.51.1, not 0.51.0: `rtk hook opencode` was added in rtk-ai#4349
and exists in no released version -- v0.51.0's src/main.rs has no
HookCommands::Opencode variant. Without the gate a 0.51.0 user gets a
plugin that registers hooks, delegates to a missing subcommand, and
silently passes every command through.

A develop build reports 0.49.0 because release-please only bumps
Cargo.toml on master, and the warning it gets is accurate: such a build
has no release version. An unparseable banner is not evidence of an old
rtk, so it passes and the delegation call still fails open.
amnesiaof added a commit to amnesiaof/rtk that referenced this pull request Oct 5, 2026
…51.1

KuSh's review of rtk-ai#2426 named two gaps that also apply here: the plugin
decided usability from the file's existence, and set no minimum version.

existsSync only proves a file is there. A wrong-arch binary, a broken
install, or a stale RTK_BIN all pass that check and then fail on every
single tool call. Spawning `rtk --version` once per session is the check
that actually means something, and the answer is cached per binary.

The floor is 0.51.1, not 0.51.0: `rtk hook opencode` was added in rtk-ai#4349
and exists in no released version -- v0.51.0's src/main.rs has no
HookCommands::Opencode variant. Without the gate a 0.51.0 user gets a
plugin that registers hooks, delegates to a missing subcommand, and
silently passes every command through.

A develop build reports 0.49.0 because release-please only bumps
Cargo.toml on master, and the warning it gets is accurate: such a build
has no release version. An unparseable banner is not evidence of an old
rtk, so it passes and the delegation call still fails open.
amnesiaof added a commit to amnesiaof/rtk that referenced this pull request Oct 6, 2026
…ward the agent

KuSh's review round 1, items 1a, 1b, 1c and 1e.

1c — the version floor was the wrong instrument, and my own floor made it worse.
`--version` cannot tell the two populations apart: a develop build reports
`rtk 0.49.0` because release-please only bumps Cargo.toml on master, and so
does every pre-rtk-ai#4349 release. The 0.51.1 floor therefore disabled the plugin on
the very binary that had just installed it, while letting a real 0.51.1-rc
through. `rtk hook opencode --help` separates them: exit 0 with the subcommand,
exit 2 without it. One call, and it subsumes "does this binary even run" — a
broken or wrong-arch binary fails to spawn at all. The floor and the version
comparison go with it, and with them the warning that printed `rtk rtk …`,
lost the leading zero, and claimed a present subcommand was missing.

1a/1b — binary discovery narrows to PATH. The RTK_BIN, ~/.cargo/bin,
~/.local/bin, Homebrew and expandHome search was the original Windows fix, but
OpenCode's shell tool runs commands with OpenCode's own PATH and never sources
.profile, .bashrc or .bash_profile — an rtk found anywhere else gets rewritten
into a command that comes back `rtk: command not found`. Finding it anyway is
worse than not rewriting. KuSh triaged that as rtk-ai#4462 rather than blocking on it;
this commit implements the narrowed claim. A PATH entry that is a directory is
skipped instead of frozen as the answer, which used to disable the plugin for
the whole session.

1e — OpenCode 2.x sends event.agent; rtk already reads agent.<name>.permission
on top of the root rules, which is what OpenCode itself applies. Without it the
rewrite silenced an agent-scoped ask on `ls *` and an agent-scoped deny on
`ls -la` never saw the command. 1.x sends no agent field, so that path stays
root-only and the README says so.

The tests are rewritten around the two things the old suite never did: it never
invoked a registered hook, and it mocked rtk by writing into process.cwd().
node is now copied into a temp directory as `rtk`/`rtk.EXE` with a `hook`
script beside it and the suite chdirs there, so the shipped code path runs
end to end — PATH discovery, the probe, argv construction, and the callbacks
OpenCode 2.0.22 and 1.18.34 actually call. Both entrypoints are invoked with
their real event shapes, an unusable rtk is pinned to register nothing, the
agent is asserted in the argv, and the probe cache and RTK_DISABLED checks are
pinned against a live rtk so deleting either line fails the suite. 13 of 13
mutations from the review die on it.

`hooks/README.md`'s OpenCode snippet assigned `args.command` from inside a
non-awaited execFile callback, so anyone copying it got a plugin that rewrites
nothing. It awaits now.
amnesiaof added a commit to amnesiaof/rtk that referenced this pull request Oct 6, 2026
KuSh's review round 1, item 1f, and the handoff note aeppling left on rtk-ai#4349.

rtk only read the legacy `permission` map, so a 2.x user who wrote the rules
OpenCode 2 actually documents had them silently ignored — the plugin then judged
rewrites against an empty rule set and returned rewrites that OpenCode would
have asked about or denied. Three shapes were missing:

- `permissions: [{action, resource, effect}]`, the 2.x native list. Entries for
  other tools are dropped; one without a `resource` covers every command.
- `permission.shell`, the 2.x name of the `bash` axis. Rules are stored under
  the axis the evaluator matches on, so both spellings land on `bash`.
- `agents.<name>.permissions`, the 2.x agent block. 1.x's `agent.<name>` still
  loads, after it.

Ordering is load-bearing and is now explicit: the legacy map first, the 2.x list
after it, within each scope, regardless of the order the file declares them. A
legacy allow plus a list deny has to come out denied — otherwise a user who
loosened the old map and then denied in the new one would be handed the allow.
Global before project, and the agent block last, both unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants