Repository navigation
Conversation
a151413 to
9995652
Compare
📊 Automated PR Analysis
SummaryRefactors the OpenCode RTK hook plugin to support both OpenCode 2.0's { id, setup } / execute.before API and the legacy 1.x tool.execute.before API, replaces the Bun-only zx Review Checklist
Linked issues: #3463, #3326, #2516, #1993 Analyzed automatically by wshm · This is an automated analysis, not a human review. |
Context & Comparison with other open OpenCode PRsThere are several open PRs addressing various OpenCode integration issues (#3463, #3330, #2819, #2705, #2566, #2153). This PR supersedes them by providing a single, zero-dependency, universal implementation that resolves all open failure modes at once:
Key advantages of this approach:
|
tomas-barros1
left a comment
There was a problem hiding this comment.
AI code review — solid fix; two hardening items + tests to address
Reviewed hooks/opencode/rtk.ts and the doc changes against the linked issues and the OpenCode V1/V2 plugin APIs. All four issues are addressed in substance, and the key claims verify:
- #3326 (Desktop /
$ is not a function) — eliminated: no Bun$, no zx;node:child_process.execFileruns in Electron/Node. - #2516 (missing default export) — fixed (
export default RtkOpenCodePlugin), and the old named export is kept so existing installs don't break. - #1993 (Windows /
which) — fixed viapath.delimiter+PATHEXTprobing + extra dirs (~/.cargo/bin,~/.local/bin, Homebrew);exts = [""]on POSIX keeps behavior sane.windowsHide: trueis a nice touch. - #3463 (V2 API) —
id/setup+ctx.tool.hook("execute.before", ...)matches the OpenCode V2 plugin docs, including mutatingevent.input.commandon the mutable event. - Exit code 3 — verified against
src/hooks/rewrite_cmd.rs: Allow→0, Ask→3, Default→3, Deny→2, Defer→1. Treating 3 as success is real and necessary. rtk initwiring —src/hooks/init.rsembeds this exact file (include_str!("../../hooks/opencode/rtk.ts")), so the fix lands inrtk init -g --opencodeon the next build. Worth a release note (existing installs must re-run init / upgrade).- Security: array argv to
execFile⇒ no shell interpolation; missing/unresponsive binary still passes through ("never block" preserved); zero dependencies is a real win for a file-based plugin.
Please address
- Timeout partial-stdout hazard (inline) — a child killed by
timeout: 3000can yield truncated stdout that this code would apply as the rewrite. Gate stdout on the error/exit code instead of ignoring it. - V1/V2 dual-shape verification (inline) — the hybrid (callable function with attached
id/setup) is clever, but you're shipping the only hook that doesn't prove it through the documented shape. One smoke test through the real V2 loader would settle it. - No tests — this is the only agent hook in
hooks/without a test (hooks/hermes/tests/test_rtk_rewrite_plugin.py,hooks/claude/test-rtk-rewrite.sh,hooks/copilot/test-rtk-rewrite.shall ship them). A zero-depnode:testsuite covering path resolution, exit-3 acceptance, timeout discard, tool-name filter, and never-block pass-through would close the "Tests present" gap flagged by the wshm bot and protect the exit-code protocol from regressions.
Nits
- (inline)
cachedRtkPathfreezes a failed lookup asnullfor the session;RTK_BINwith~/$HOMEisn't expanded. - The V2
setupwarns when the binary is missing but the V1 path stays silent — minor asymmetry. hooks/README.md'sexecFilesnippet omits thebash/shelltool-name filter the real code applies (illustrative, but easy to copy wrongly).- Missing trailing newline at EOF (inline).
No hard logic blockers found — this does what it claims. With the two inline items addressed and a small test file, this is ready to merge.
|
Thanks for the thorough review and great catches! All points have been addressed in the latest commit:
|
|
I tested the installed rtk.ts from this PR against OpenCode 2.0.16 (stable, opencode serve) and the plugin fails to load. The V2 loader rejects the hybrid shape: level=WARN message="failed to load plugin" target=~/.config/opencode/plugins/rtk.ts
cause="PluginModule.LoadError: Plugin must export a default definition with an id and an
effect or setup function. (cause: SchemaError(Expected object at [\"default\"]))"Root cause: the 2.0.16 plugin loader validates module.default against a schema that accepts only plain objects — { id, setup } or { id, effect } (verified by reading the loader code inside the compiled binary). A function with .id/.setup bolted on as properties is not an object to that guard, so validation fails before setup() is ever invoked. The V2 branch of the dual-shape therefore never activates — the hook is silently absent on all current 2.x releases. This is consistent with the official V1→V2 migration docs, which state that V2 plugins "default-export a definition with an id and setup(ctx)" and show the dual-support shape as an object that spreads Plugin.define({...}) and adds a server() method — not a callable function with attached properties. export default {
id: "rtk",
async setup(ctx) {
if (!resolveRtkPath()) {
console.warn("[rtk] rtk binary not found — plugin disabled")
return
}
await ctx?.tool?.hook("execute.before", async (event) => {
const input = event?.input
if (!input || typeof input !== "object") return
const rewritten = await tryRewriteCommand(event?.tool, input.command)
if (rewritten) input.command = rewritten
})
},
}Since 2.x is current, I'd suggest making the object definition the default export and dropping the callable-function path (or, if 1.x support must be kept, exporting the object above while leaving the V1 function as a named export for 1.x loaders — though per the docs, 1.x V1 object entrypoints are supported in 1.18.29+, so a plain object with both setup and server keys is the sanctioned dual-shape). This also addresses the smoke-test gap flagged in review: a test that feeds the default export through the actual 2.x loader schema (or a simple |
|
Thanks for testing this against Everything is now updated, hardened, and verified locally:
Ready for re-test! |
…up logic - Consolidate command mutation logic between V1 and V2 entrypoints into a shared helper - Unify missing binary warning between setup() and server() - Simplify expandHome and path discovery loops
Carries rtk-ai#4187 (OpenCode V2, Desktop, and Windows support) until it merges upstream. Drop this commit when syncing after the PR lands; the eventual upstream merge must be a no-op against these files.
|
This PR also addresses #3898. I originally opened #3899 for the same OpenCode 2.0 compatibility issue, but this PR now covers the problem more completely. I closed #3899 and further work on #3898 will be tracked here. If appropriate, please add Closes #3898 so the issue is closed when this PR is merged. |
Thanks for the support and for consolidating the efforts here! I've added |
|
I need this ! |
|
Thanks but is it gonna merge anytime soon? |
|
@pszymkowiak @FlorianBruniaux @aeppling @KuSh @TaKO8Ki Could you please take a look? |
|
+1 Please... |
# Conflicts: # docs/guide/getting-started/supported-agents.md # hooks/opencode/README.md # hooks/opencode/rtk.ts
5c9e798 to
96a538f
Compare
…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.
KuSh, on rtk-ai#2426: "skipping commands that already start with `rtk `". `rtk git status` reaching the hook means the agent already used rtk -- either carried over from an earlier rewrite or typed by the user. Handing it back to `rtk hook opencode` cannot improve it, and it costs a process spawn on every such call. hooks/pi/rtk.ts already does this with a bare `startsWith("rtk ")`. The regex here additionally tolerates leading whitespace, since the shell ignores it and the command may arrive that way.
KuSh, on rtk-ai#2426: "honouring RTK_DISABLED". `RTK_DISABLED=1` is the escape hatch hooks/README.md documents for users who want a run without rtk, and hooks/pi/rtk.ts already checks it. Without it the OpenCode plugin was the one TS hook that could not be turned off short of editing the file. Checked per call rather than at setup, for the same reason pi does: the variable is inherited from whatever launched OpenCode, so a user can start a session with it set and get a clean baseline. Only the exact string "1" disables rewriting -- "0" and "true" are not the documented form and must not silently turn the plugin off.
b4ef46b to
bedcc2c
Compare
KuSh
left a comment
There was a problem hiding this comment.
Round 1 of 3 — CHANGES REQUESTED
Claim: hooks/opencode/rtk.ts loads and rewrites on OpenCode 2.x and 1.x, in Desktop (no Bun $), on Windows (no which), delegating to rtk hook opencode (#4349).
Scope: accept if narrowed to "OpenCode 2.x and 1.x, with rtk resolved from PATH (PATHEXT on Windows); 1.x ≤ 1.3.3 served by a legacy plugin". The V2 fix is real and needed: develop's plugin fails to load on 2.0.22. Finding rtk outside PATH becomes a follow-up issue (below). (round 1, frozen)
Ran: real OpenCode driven by a mock model that issues one bash/shell tool call, with the plugin installed by each build's own rtk init -g --opencode: develop df39e33d7 (#4349 merged) vs develop + bedcc2cf7, binaries verified to differ. OpenCode 2.0.22, 1.18.34, 1.18.10, 1.3.4, 1.3.3, 1.1.4 (1.0.142 to 1.18.28 on the earlier head).
- Rows:
ls -la;rtk ls && ls -la; OpenCode rules where the rewrite would flip the verdict, in V1permission.bash,permission.shell, the V2permissionslist (also with a wildcard action and mixed with the legacy map), and agent-scopedagent.build.permissionandagents.build.permissions; rtk only in~/.cargo/bin;RTK_BINset to a directory;RTK_DISABLED=1; the develop build's realrtk 0.49.0banner and a release-like0.51.1one. - Invariants: non-default targets, every decision. Savings: n/a, no filter changes.
Pins: 24 mutations, 10 killed, 14 survived (1 of them equivalent against the real rtk), listed in blocker 3.
Matrix: executed this round. Pin check: executed this round.
CI @bedcc2cf7: all green.
Blocks merge (3, frozen at round 1)
-
hooks/opencode/rtk.ts:52, :186, :142, :116— the plugin's own checks disagree with what actually runs. Six sites. c and d implement what I suggested when closing #2426. After testing, my first proposal doesn't work in some cases and needs a better solution, so please read those two as a correction of my suggestion, not as something you got wrong. Evidence on 1.18.10 and 2.0.22, develop vs this PR:-
a.
RTK_BINand the fallback dirs. With rtk only in~/.cargo/bin, develop disables itself andls -laruns. This PR rewrites it, and the bash tool returns/usr/bin/bash: line 1: rtk: command not found. The tool doesn't source.profile/.bashrc/.bash_profile; I tried all three exporting the directory. -
b.
RTK_BINpointing at a directory, with rtk onPATH. Develop rewrites. This PR disables itself for the session. -
c. The 0.51.1 floor. It disables the plugin on every develop build, including the binary that installed it. Develop reports
rtk 0.49.0, which is also whatcargo install --git https://github.com/rtk-ai/rtkbuilds, since develop is the default branch. OpenCode logs[rtk] rtk rtk 0.49.0 has no \hook opencode` subcommand (need >= 51.1) — plugin disabledandls -laruns unrewritten. That message is wrong three ways: the binary does have the subcommand, the version is0.51.1, not51.1, andrtkis doubled. A pre-#4349 binary reports the samertk 0.49.0, so no version number can tell the two apart.rtk hook opencode --helpcan: exit 0 on develop, exit 2 on the pre-#4349 binary. So a probe of the subcommand is the better form of the minimum-version check I suggested, and one call also covers the binary-runs check that--version` was for. -
d. The
rtkskip. It drops rewrites develop makes.rtk hook opencode 'rtk ls && git status'answersrtk ls && rtk git status, andrtk ls && ls -lacomes back with a rawls -lahalf here but rewritten on develop. A barertk lsalready answers{}, so the skip only saves one process. This is the other part of my #2426 suggestion that testing showed doesn't hold: it should go. -
e. The agent is never passed. On 2.0.22,
execute.beforearrives as{tool, sessionID, agent, messageID, id, input}, andrtk hook opencode --agent <name>already readsagent.<name>.permissionon top of the root rules. ButrunHookOpencodespawns["hook", "opencode", command], so rtk judges the rewrite against root rules only, while OpenCode applies the agent's. This is the agent-scoped case #4195 left open after #4349. Withagent.build.permission.bash={"*": "allow", "ls *": "ask"}, OpenCode alone prompts forls -la. This PR rewrites it tortk ls -la, which the agent's*allows, so the prompt is gone. With{"*": "deny", "ls -la": "allow"}, OpenCode alone runsls -la, and this PR'srtk ls -lais denied. Passingevent.agentas--agent <name>when it's a non-empty string fixes both: a patched copy of this head keeps the prompt, runsls -lain the second case, and still rewrites when no agent rule applies. 1.x has no agent field (tool.execute.beforegets{tool, sessionID, callID}), so the V1 path stays root-only. On 1.18.34 the agent-scoped ask is still silenced, with or without the change. Please say so inhooks/opencode/README.md. -
f. rtk reads only V1-shaped rules.
rtk hook opencode(src/hooks/permissions_opencode.rs:87andload_opencode_rules) readspermission.bashandagent.<name>.permission. OpenCode 2.0.22 also applies:- V2's native
"permissions": [{action, resource, effect}]list, where the action isshellor a wildcard such as*; permission.shell;"agents": {"<name>": {"permissions": [...]}}.
None of these reach rtk, and develop never meets them, because its plugin doesn't load on V2. This PR is what exposes those users, so reading them belongs here. Measured, OpenCode alone vs this PR:
permissionswith*denied andls -laallowed:ls -laruns vsrtk ls -lais denied. Same withpermission.shell.{action: "*", resource: "ls *", effect: "ask"}: prompts vs silenced.agents.build.permissionswith an ask onls *: prompts vs silenced. Still silenced with 1e alone, because rtk doesn't read that list.
Order matters for "last match wins". OpenCode applies the legacy
permissionmap first and thepermissionslist after it, whatever their order in the file: legacy allow plus list deny is denied, legacy deny plus list allow is allowed. Per the V2 docs, global rules come before project rules and agent rules come last. The docs also list agents defined in.opencode/agents/<name>.md/~/.config/opencode/agents/<name>.mdwithpermissions:frontmatter; I haven't run those. This is Rust in the same file #4458 refactors, so whichever lands second rebases. - V2's native
A correct fix passes these rows:
- rtk only in
~/.cargo/binor only atRTK_BIN→ls -laruns raw or through rtk, nevercommand not found. RTK_BIN= a directory with rtk onPATH→ rewritten.- A develop build (
rtk 0.49.0withhook opencode) → rewritten. A pre-#4349 binary → disabled, with a message that names the missing subcommand. rtk ls && ls -la→rtk ls && rtk ls -la.- 2.0.22 with
agent.build.permissionrules: the ask onls *still prompts, and*denied /ls -laallowed still runsls -la. - 2.0.22 with each V2 form in 1f: same outcome as OpenCode alone, including the legacy-map-then-list order.
For (a), two shapes meet the rows: resolve from
PATHonly (keeping PATHEXT, which is the #1993 fix), or keep the discovery and make the spawned shell resolve the same binary. The first matches the narrowed claim. -
-
hooks/opencode/rtk.ts:236— OpenCode ≤ 1.3.3 regresses. Loaders before 1.3.4 ("single target plugin entrypoints", 2026-03-29) call every export,defaultincluded, asfn(input):- 1.1.4: OpenCode exits 1 at startup (
TypeError: fn3 is not a function). - 1.3.3:
failed to load plugin, no rewrite. - Develop's current plugin rewrites on both.
V2 rejects a function default and these loaders reject an object, so no single file serves both. We'd like to keep develop's current
rtk.tsas a legacy plugin, the way--claude-mdkeeps the legacy Claude Code mode. Ship it alongside the new one (embedded like the current file), and havertk init -g --opencodechoose between them:- OpenCode ≥ 1.3.4 or 2.x (from
opencode --versionat install time) → the new plugin. - OpenCode ≤ 1.3.3 → the legacy plugin, with a line saying so.
- An explicit legacy option → the legacy plugin whatever the version, for installs where
opencodeisn't onPATHat init time. opencodenot found and no option → the new plugin.
Both land at
~/.config/opencode/plugins/rtk.ts, so uninstall andrtk init --showkeep working unchanged. The floor and the option go inhooks/opencode/README.mdandsupported-agents.md. Rows: the legacy file rewrites on 1.1.4 and 1.3.3, the new one on 1.3.4 and 2.0.22, and a Rust test pins the version split at 1.3.3 / 1.3.4, including2.0.22and an unparsable string. - 1.1.4: OpenCode exits 1 at startup (
-
hooks/opencode/rtk.test.mjs:204— unpinned behaviour. Each edit below leaves the suite green:- Hook wiring: no test ever invokes a registered hook.
handleToolHook(e?.tool, e?.args)insetup,input?.argsinserver,"tool.execute.after"as the V1 key, andcontainer.command = rewrittenwithout theif(which writesnullover every command rtk leaves alone) all survive. - Usability gates: dropping
ensureRtkUsable()fromsetuporserver,if (false)for the probe-null gate, andif (false)for the floor gate all survive. - Guards whose tests claim to pin them:
- Deleting
if (isAlreadyRtk(command)) return nullsurvives. The test sendsrtk rewrite, which the mock doesn't answer, so null comes back anyway. - Deleting the
RTK_DISABLEDcheck survives. That case runs before thehookmock is written, so node fails and returns null anyway. - Deleting the probe cache survives. The test resets it before the second call.
- Deleting
The tests must, with
rtkgiven throughRTK_BINas the suite now does:- V2: call the callback
setupregistered with the shape 2.0.22 sends,{tool: "shell", sessionID, agent, messageID, id, input: {command: "ls -la"}}. Assertinput.command === "rtk ls -la", and that a command answered{}stays as typed. - V1: call
(await server())["tool.execute.before"]with 1.18.34's shape,({tool: "bash", sessionID, callID}, {args: {command: "ls -la", description}}), with the same two assertions. - An unusable binary (probe fails, or the capability check from 1c fails):
setupregisters nothing andserver()returns no hook. - The agent (1e), with a
hookmock that echoes its argv: the V2 callback, givenagent: "build", spawnsopencode --agent build ls -la; the V1 hook spawnsopencode ls -la. I checked this shape against a patched copy: it passes there, fails on this head, and fails again oncee?.agentis dropped. - The
RTK_DISABLEDand probe-cache tests must fail when their line is deleted. - Rust (1f): a
permissions_opencodetest per V2 form, and one pinning that the legacy map comes before the list.
Each mutation above must turn one of them red.
- Hook wiring: no test ever invokes a registered hook.
Optional — will not hold merge
hooks/README.md: the snippet assignsargs.commandinside a non-awaitedexecFilecallback, so copying it rewrites nothing.rtk.test.mjswrites itshookandrewritemocks intoprocess.cwd(), which clobbers files of that name. It also restores unsetRTK_BIN/RTK_DISABLEDas the string"undefined".mkdtempSyncanddeleteavoid both.
Filed as follow-ups
- #4462 OpenCode plugin: no rewrite when rtk is not on OpenCode's
PATH(Desktop launched from the macOS GUI). It's cut by the narrowed claim, and it carries #2039's case, which was closed into #3326. Filed.
Checked and correct — no need to re-verify
- OpenCode 2.0.22: develop fails with
PluginModule.LoadError … Missing key at ["default"]. This PR loads and rewritesls -la(with a banner the floor accepts). - Delegation keeps #4349's intent on 1.x. With
permission.bashset to*denied andls -laallowed,ls -laruns as typed. With*allowed andls *on ask, the prompt is kept. Same as develop on 1.18.34, and on 2.0.22 with thebashkey. RTK_DISABLED=1in OpenCode's environment passes commands through. Develop's plugin keeps rewriting there, so this is new and works.- The command reaches
rtk hook opencodeas one argv element, and a non-JSON or failed answer passes the command through. - The hook mapping matches the real event shapes: V2
event.tool === "shell",event.input.command; V1input.tool === "bash",output.args.command.
Your questions
- None open.
Next
1c and 1d are corrections of my own #2426 suggestion. Then fix 1a, 1b, 1e and 1f, add the legacy plugin for 2, and add the tests in 3. After your push I'll rerun the rows above on the new head.
Rounds: 1/3. Threads: 3 open (waiting on the author), 0 resolved this round.
| if (existsSync(expanded)) return (cachedRtkPath = expanded) | ||
| } | ||
|
|
||
| const dirs = [ |
There was a problem hiding this comment.
Blocker 1 (a–f): the plugin's own checks disagree with what actually runs. Discovery outside PATH here, the 0.51.1 floor, the rtk skip, the missing --agent, and the V2 rule forms rtk doesn't read. The review body has the evidence and the rows a fix has to pass.
| * Exports a plain object with `id` and `setup(ctx)` matching OpenCode 2.x Schema validation, | ||
| * with a `server()` method for OpenCode 1.x (1.18.29+) dual-shape compatibility. | ||
| */ | ||
| const RtkOpenCodePlugin = { |
There was a problem hiding this comment.
Blocker 2: with an object default export, OpenCode 1.1.4 exits 1 at startup (fn3 is not a function) and 1.3.3 fails to load the plugin, while develop's plugin rewrites on both. The review body has the legacy-plugin ask.
| _resetCachedRtkPath() | ||
| process.env.RTK_BIN = process.execPath | ||
| assert.equal(await tryRewriteCommand("bash", "rewrite"), "rtk git status") | ||
| assert.equal(await tryRewriteCommand("bash", "rtk rewrite"), null) |
There was a problem hiding this comment.
Blocker 3: this assertion passes without the isAlreadyRtk guard, because the mock doesn't answer rtk rewrite, so null comes back anyway. No test invokes a registered hook either. The review body lists the surviving mutations and the test rows.
The skip drops rewrites develop makes and saves one process. `rtk hook
opencode 'rtk ls && git status'` answers `rtk ls && rtk git status`, and a
bare `rtk ls` already answers `{}`, so the guard only ever suppressed the
rewrite of the bare halves in a chain.
Reviewed as part of rtk-ai#4187 round 1.
…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.
KuSh's review round 1, item 2. Installing the 2.x plugin on an old OpenCode breaks it: loaders before 1.3.4 call every export as `fn(input)`, and the plugin default-exports a plain object — 1.1.4 exits 1 at startup with `fn3 is not a function`, 1.3.3 logs `failed to load plugin`. The two shapes cannot be one file, because the 2.x loader is the mirror image and rejects a callable default. So develop's plugin comes back as `hooks/opencode/rtk-legacy.ts`, embedded alongside the current one, and `rtk init -g --opencode` picks between them from `opencode --version`: <= 1.3.3 gets the legacy file and a line saying so, >= 1.3.4 and 2.x get the current one, and a missing or unparsable OpenCode gets the current one — it works on every released version, so that is the safe default. `--opencode-legacy` forces the legacy file for anyone whose setup hides the binary from init. The version probe lives in `ensure_opencode_plugin_installed` behind an AtomicBool rather than a parameter, because that function has four call sites across three init modes and none of them otherwise know anything about OpenCode versions. The tests pin the split at both sides of 1.3.4 (plus 2.0.22, a 0.x version, and three unparsable banners), and pin each file's export shape, so a future edit that turns the legacy file into an object — or the current one into a function — fails here rather than at someone's startup.
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.
KuSh's review round 1, item 2, reconsidered after reading OpenCode's own docs. The legacy file existed because the object-shaped plugin cannot load on old V1: those loaders call every export as `fn(input)`, and an object is not callable. I answered that with a second plugin file plus a version probe, which means shipping a Bun-`$`-only, Windows-broken plugin forever, wired to a boundary I had measured rather than one OpenCode publishes. https://opencode.ai/v2/docs/build/plugins#support-v1 answers this directly. The dual `setup()`/`server()` default export is the shape OpenCode recommends, and the page puts the object form at 1.18.29 — older V1 releases "may expect function exports instead". The same page says to remove the V1 implementation once the support window closes, and the migration guide says the same thing again. So the floor is 1.18.29, which is also the last 1.x line. `needs_legacy_plugin` had it at 1.3.4, from the review's own matrix: 1.3.3 fails, 1.3.4 works. That is the version where the loader changed, not the version OpenCode supports, and between 1.3.4 and 1.18.29 we were claiming releases the vendor does not. So `rtk-legacy.ts`, `OPENCODE_PLUGIN_LEGACY`, the override flag and the selector are gone. `rtk init -g --opencode` writes the one plugin and, when it can read `opencode --version`, names the version it found and the floor if the install will not load. An OpenCode we cannot ask stays silent rather than being told off for a banner we could not parse. Also repairs the encoding of `hooks/opencode/rtk.ts`, which had picked up mangled bytes for every em dash and ellipsis in its comments when it was rewritten in an earlier commit. Same characters, correct UTF-8.
|
Item 2 is a scope disagreement: you asked for the legacy plugin, 9513636 drops it. OpenCode's docs put the floor higher than we did. The "Support V1" page at https://opencode.ai/v2/docs/build/plugins#support-v1 says the object default export works from 1.18.29, that older 1.x releases expect function exports, and that the V1 implementation should be removed once the support window closes. 1.18.29 is the last 1.x release, so that is current 1.x plus 2.x. Our 1.3.4 came from your matrix. That is where the loader changed, not what OpenCode claims to support. The cost: on 1.3.3 and older the plugin stops working where develop's works. That is a real regression on a version you tested, so the scope call is yours to make. What I am asking for is current 1.x and 2.x, not the 1.x that predates the object export. Also confirmed from the v1 docs: there is no |
…eady has A review pass over every md in the repo turned up the plugin's own doc comment still describing the file this PR deleted. `hooks/opencode/rtk.ts` said 1.3.4 and named `rtk-legacy.ts` as what `rtk init` installs for older loaders; neither is true since 9513636. It now says 1.18.29, cites the V2 plugin docs for that number, and describes what init actually prints. The floor was documented in two files out of eight that describe OpenCode, so anyone on 1.3.4 reading `README.md`, `TECHNICAL.md`, `hooks/README.md` or the Copilot awareness table would conclude the plugin works there. All four now name the floor. `hooks/opencode/README.md` described the permission rules as "last match wins" over `opencode.json`/`.jsonc` and did not name the V2 forms, though the Rust side has read them since 0c1cb3a. The order is load-bearing rather than last-match, so the text now gives it: legacy map and `agent.<name>.permission` first, then the V2 list, `permission.shell` and `agents.<name>.permissions`, whatever their order in the file, global before project and agent last. `troubleshooting.md` told people to re-run `rtk init` when OpenCode was not rewriting. On anything older than 1.18.29 that cannot help, and the symptom is different: OpenCode exits 1 at startup, or logs `failed to load plugin`. It now says so, and adds the other case worth checking, an rtk that OpenCode's shell tool cannot spawn because it is not on the PATH OpenCode sees. The OpenCode snippet in `hooks/README.md` builds argv by hand, so it shows the `--agent` element the paragraph above it describes.
The review pass found the plugin claiming to "honour RTK_DISABLED=1" without saying which of the two forms that is, and they are not the same thing. The per-command form, `RTK_DISABLED=1 git status`, is an env prefix inside the command string. rtk never reads it from an environment: `cmd_has_rtk_disabled_prefix` in the rewrite engine looks for it as a token prefix, and because this plugin hands the command string through untouched, that form already works through the delegation path and needs no code here. What the plugin checks is `process.env.RTK_DISABLED`, OpenCode's own process environment. That one is session-scoped rather than command-scoped, and it is the same check hooks/pi/rtk.ts:124 makes. The comment in rtk.ts claimed the env var was "the documented escape hatch (hooks/README.md)", which reads as if it were the documented form. It is a second one, and now says so.
|
when will this PR be merged? |
# Conflicts: # docs/contributing/TECHNICAL.md
It appears this won’t happen anytime soon. The developers intend to include support for both v1 and v2 in a single release, even though OpenCode v1 is at EOL and is no longer supported or actively developed. Why the RTK developers are doing this remains a mystery to everyone. But for the sake of supporting the outdated OpenCode v1, which has reached its end-of-life — they are not releasing RTK for the current v2 version and are blocking the project’s compatibility with the most popular coding agent. |
|
This compatibility should have been addressed before the stable release of opencode2. It has remained unresolved for months, despite being a critical issue. Could the team prioritize reviewing, merging, and resolving these issues? opencode2 is a core development tool, not a secondary harness, and its compatibility deserves timely attention. |
Summary
Refactors
hooks/opencode/rtk.tsto be universally compatible with both OpenCode 2.0 and OpenCode 1.x, fixes compatibility with OpenCode Desktop, and resolves cross-platform path issues on Windows.Closes #3898, Closes #3463, Closes #3326, Closes #2516, Closes #1993.
Problem
{ id, setup }andctx.tool.hook("execute.before", event => ...), replacing the legacy V1 signature.rtk init --opencodeplugin silently does nothing on Desktop ($ is not a function) and on Windows (whichnot found) #3326): OpenCode Desktop runs in an Electron/Node environment where the Bun Shell$helper is undefined, throwingTypeError: $ is not a function.which rtk, which fails on Windows.rtk rewriteexits with status code3on success for advisory rewrites; standard Node child_process callbacks treat non-zero exits as errors, causing rewrites to be discarded.Solution
.idand.setupproperties (for OpenCode 2.0).$withnode:child_process.execFilewith zero external dependencies.stdoutregardless of whetherchild_processflags a non-zero exit code.rtkacrossRTK_BIN,PATH,~/.cargo/bin,~/.local/bin, Homebrew, and WindowsPATHEXT.rtkis missing or times out.Verification
execute.beforefires and mutatesevent.input.command.tool.execute.beforehook functions identically.rtkexits with code3on rewritten rules.