Skip to content

fix(opencode): resolve rtk binary path for GUI-launched OpenCode (#4462) - #4507

Open
TechWizard9999 wants to merge 103 commits into
rtk-ai:masterfrom
TechWizard9999:fix/4462-opencode-path-resolution
Open

TechWizard9999 wants to merge 103 commits into
rtk-ai:masterfrom
TechWizard9999:fix/4462-opencode-path-resolution

Conversation

@TechWizard9999

Copy link
Copy Markdown
Contributor

Summary

Fixes #4462: OpenCode plugin now works when rtk is installed outside OpenCode's PATH (e.g., ~/.cargo/bin on macOS GUI launch).

Problem

When OpenCode is launched from the macOS Dock (GUI launcher), it doesn't inherit the user's shell PATH (which includes ~/.cargo/bin, /opt/homebrew/bin, etc.). The plugin would either:

  1. Disable itself entirely (which rtk fails at init)
  2. Emit a bare rtk ... rewrite that OpenCode's shell can't execute (rtk: command not found)

Solution

Shape A — Rewrite carries resolved absolute path:

  1. hooks/opencode/rtk.ts: Added resolveRtkPath() with multi-strategy discovery:

    • PATH (current behavior)
    • RTK_BIN environment variable
    • ~/.cargo/bin/rtk (cargo install)
    • /opt/homebrew/bin/rtk (Homebrew ARM)
    • /usr/local/bin/rtk (Homebrew Intel)
    • Windows: LOCALAPPDATA/cargo/bin, ProgramFiles/rtk
    • Bundled target/release/rtk (fallback)
  2. src/hooks/hook_cmd.rs: Modified opencode_answer() to accept optional bin_path and emit absolute-path rewrite when provided (e.g., /Users/x/.cargo/bin/rtk git status instead of rtk git status).

  3. src/main.rs: Pass std::env::current_exe() as bin_path to run_opencode().

Acceptance Criteria (from issue)

  • ✅ rtk only in ~/.cargo/bin, not on OpenCode's PATH → ls -la rewritten and runs
  • ✅ rtk only at RTK_BIN → same
  • ✅ rtk on PATH → unchanged behavior

Testing

  • All existing tests updated to pass None for bin_path (preserving bare rtk ... output for rtk hook check protocol)
  • New test file for plugin behavior under constrained PATH can be added in follow-up

cc @KuSh for review

aeppling and others added 30 commits June 12, 2026 18:27
run_test(&Commands) → run_vitest/run_jest over a shared helper, dropping the
module dependency on the CLI Commands enum and the unreachable!() arm. Also fix
two doctests that surface once the crate is bundled as a lib: correct
join_with_overflow (Unicode ellipsis) and mark the TimedExecution example ignore.
Command::output() gives the child an immediate-EOF stdin, so stdin-based
bodies (-d @-, --data-binary @-, -T -, -K -) arrived empty: the server
received Content-Length: 0. Inherit rtk's own stdin before capturing
stdout/stderr, mirroring RunOptions::inherit_stdin (npm, wc).

Fixes rtk-ai#4084
`show_positionals` hand-rolled git's short-flag cluster grammar to find which
`git show` arguments are objects: a per-char walk that re-queried the flag table
through a `format!("-{c}")` allocation, plus its own `--`-boundary slice. The
tokenizer's `ValueSpec` table models exactly that — attached value, separate
value, and the solo-only cluster rule — so the walk is now `tokenize_grammar` +
`before_dashdash` + `Token::is_free_positional`.

It reads `git show` under diff's grammar rather than log's, matching the route
classifier beside it: `git show -wl 100` consumes the 100 as the rename limit,
where log's `-l` is solo-only and would leave it looking like a second object and
silently drop the blob window.

Verified byte-identical (stdout, stderr and exit code) against a binary built
from the merge-base over 94 `git show` invocations: blob and commit shows, the
`-wG a:b HEAD:blob` cluster, `-pS a:b`, `-S 'url:1' HEAD`, `-L 1,2:file`,
`-- <pathspec>`, `--textconv`/`--filters`/`--ext-diff`, the `:(exclude)`/`:!`/
`:^`/`:/` magic pathspecs, and every short flag whose log and diff spellings
disagree.

The `cat-file` probe keeps its subprocess: it only runs once a free positional
already looks like `rev:path`, which is the one case where nothing but git can
answer, so no flag pre-filter can skip it without risking the blob path.

Also drops a doc link on `log_wants_raw_shape` pointing at a `#[cfg(test)]`
helper, which rustdoc cannot resolve outside a test build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`extract_identifier_and_extra_args` picked the MR/issue identifier with a
hand-rolled walker: a hardcoded eight-entry list of value-taking flags, a
`skip_next` bool, and `arg.starts_with('-')`. Anything the list missed had its
value read as the identifier, and the list itself was a mix of flags from
different subcommands.

Verified against glab 1.36.0 and 1.117.0:

    rtk glab mr view --page 2      ->  glab mr view 2 -F json --page
    rtk glab issue view -P 50      ->  glab issue view 50 -F json -P
    rtk glab mr view --jq .iid     ->  glab mr view .iid -F json --jq

Replace it with `arg_tokenizer::tokenize_grammar` plus one `takes_value`
predicate per glab subcommand, transcribed from each subcommand's own `--help`,
and take the first free positional. `mr merge`, `mr approve`, `mr note` and
`mr update` each get their own table: `-s` is the boolean `--squash` on merge
but the value-taking `--sha` on approve, and `-d` is `--remove-source-branch`
on merge but `--description` on update.

`run()` also restores the `--` that clap's `trailing_var_arg` consumes, over
the reassembled `[subcommand] + args` region rather than `args` alone, and
moves rtk's own `-R`/`-g` from the end of the args to the tokenizer's
injection point — glab reads everything past `--` as a positional, so an
appended `-R` arrived as two extra arguments instead of the repo flag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two fallouts of classifying `--jq` and restoring `--`.

`--jq` now reaches glab with its value intact, so glab returns the user's
projection — and `run_glab_json` reformatted it as an MR summary of fields the
projection does not have (`? MR !0: ???`). Previously the value was hoisted out
as the MR number and glab failed loudly, so this turned a visible error into a
silent wrong answer. `has_output_flag` now counts `--jq`.

A `--` at the head of the region ended rtk's own `-R`/`-g` parsing, not glab's.
Forwarding it makes glab stop looking for a subcommand: `glab -- mr view 42`
prints root help on 1.117.0. Drop it instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`trailing_var_arg` only ever lets clap strip one `--`, the one at the head of the
region it hands to glab — before the subcommand, or right after it, since `-R`/`-g`
are parsed before the subcommand positional either way. Only checking index 0 let
the index-1 case through: `glab mr -- view 42` prints the `mr` help instead of
viewing the MR, and `glab api -- projects/1 --paginate` is `Accepts 1 arg(s),
received 2` on glab 1.117. Every later `--` clap passes through untouched and still
reaches glab verbatim.

`split_identifier` reads positionals past `--` because glab does too
(`glab mr view -- 42` views MR 42), but the callers re-emit the identifier ahead of
the boundary. When the escaped region held more than the identifier, that promoted a
flag the user had escaped back into flag position: `rtk glab mr view -- --web 42`
ran glab with `--web` live, opening a browser the user had asked glab to treat as a
positional. glab takes at most one positional for these subcommands, so an escaped
region holding more than one token is something glab rejects whatever rtk sends.

Also isolate HOME and RTK_DB_PATH in the argv test, which otherwise read the
developer's own config and wrote to their real tracking database.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refusing to look past `--` only when two or more tokens trailed it still unescaped
the single-token case, which is the one the user is most likely to type:
`rtk glab mr view -- --web` hoisted `--web` into identifier position and ran glab
with the browser flag live, where develop passed the command through untouched.

What makes hoisting safe is not how much the boundary escapes but whether
unescaping it is a no-op: reach past `--` only for one token that is still read as a
positional without it, so `glab mr view -- 42` keeps gaining `-F json`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`escapes_a_bare_positional` landed between the header and the function it describes,
so rustdoc rendered the helper under `split_identifier`'s summary and left
`split_identifier` undocumented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Address rtk-ai#3808 review: only accept sudo/env/KEY=VALUE/-flag wrappers,
never search past non-Arg tokens for RTK_DISABLED=, and stop at the
first command word instead of scanning into wrapper arguments.

Fixes false positives from docker -e, && chains, ssh/xargs/watch, and
value-taking sudo flags like -u root.
- Add fs::create_dir_all for .claude dir in patch_settings_json_command and run_default_mode (fixes rtk-ai#2519, rtk-ai#4046 Windows regression)
- Hoist OpenCode plugin installation before Unchanged early return in run_claude_md_mode_with
- Fixes rtk-ai#4046 regressions.
The previous fix created the .claude directory unconditionally, but dry-run
mode must not create directories. Only create the directory when not in
dry-run mode.

Fixes test: test_run_default_mode_dry_run_does_not_create_missing_claude_dir
categorize_command mapped pip/pytest/ruff/mypy/sqlfluff to python but
missed uv, so every rtk uv … telemetry row fell through to other.
After the pip uv-fallback labelled those rows rtk pip …, the same
invocation was bucketed differently depending on entry path.

Adds uv to the python arm and pins the mapping in a unit test.

Fixes rtk-ai#4316
Keep wrappers (sudo/env/transparent_prefixes) when asking whether the
hook would have rewritten the line without RTK_DISABLED=. Peel only the
RTK_DISABLED= assignments for that gate; leave gain on the syntactic
strip_disabled_prefix so it does not inflate without a coverage check.

Addresses KuSh round-3 CHANGES_REQUESTED on rtk-ai#3808.
… filter

Successful fuzz runs collapsed to a bare "N passed", dropping the final "fuzz: elapsed:" line. Non-JSON stdout (e.g. setup errors) was discarded, so users saw "Go test: No tests found" instead of the real error.
- README.md: change --opencode description to 'Claude setup + OpenCode plugin'
- supported-agents.md: clarify OpenCode installs Claude setup + plugin
- troubleshooting.md: change --opencode label to 'Claude Code + OpenCode plugin'
- main.rs: install Claude even when --opencode is passed (additive)
- mod.rs: update help text to 'Claude setup + OpenCode plugin'

Ports docs edits from rtk-ai#4052 to rtk-ai#4323.
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.182 to 0.2.189.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.189/CHANGELOG.md)
- [Commits](rust-lang/libc@0.2.182...0.2.189)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.189
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps dirs from 5.0.1 to 7.0.0.

---
updated-dependencies:
- dependency-name: dirs
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.102 to 1.0.104.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](dtolnay/anyhow@1.0.102...1.0.104)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [ignore](https://github.com/BurntSushi/ripgrep) from 0.4.25 to 0.4.32.
- [Release notes](https://github.com/BurntSushi/ripgrep/releases)
- [Changelog](https://github.com/BurntSushi/ripgrep/blob/master/CHANGELOG.md)
- [Commits](BurntSushi/ripgrep@ignore-0.4.25...ignore-0.4.32)

---
updated-dependencies:
- dependency-name: ignore
  dependency-version: 0.4.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.149 to 1.0.151.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](serde-rs/json@v1.0.149...v1.0.151)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.151
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
The golangci-lint rewrite rebuilt the command from its parsed global and
`run` segments and returned early, before the trailing redirect was put
back, so `golangci-lint run 2>&1` became `rtk golangci-lint run` and
output meant for a file or /dev/null reached the terminal instead.

Every rewrite of a supported command now builds only the command part
(`rewrite_command_part`), and the redirect split off before matching is
re-attached once, exactly as typed. A branch can no longer forget it, and
the original boundary is kept: `run ${PKG}2>/dev/null` stays one operand
followed by a redirect, with no separating space inserted.

Across every rule prefix combined with six trailing-redirect forms (1752
inputs), golangci-lint was the only rewrite that dropped the redirect;
with this change none does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since v0.37.2, rtk init -g installs the native rtk hook claude entry on Windows, so the troubleshooting page no longer needs to describe the CLAUDE.md fallback. Point legacy setups at re-running rtk init -g to migrate off rtk-rewrite.sh.
Real `diff <file>` with one operand reports `diff: missing operand
after '<file>'` and exits 2 before opening the file. rtk treated the
single operand as the stdin form, ignored the file, echoed stdin back
as a diff to condense, and exited 0 — so a `diff <file> && next` step
ran where `diff` would stop, and a missing operand was never reported.

The `-` operand still reads a piped diff from stdin; only a real file
operand alone takes the usage-error path. The hook rewrites a bare
`diff <file>` to `rtk diff <file>`, so the command an agent wrote now
fails where the tool it stands in for fails.

Fixes rtk-ai#4320.
- one_file_operand_is_a_usage_error_not_a_stdin_condense: `rtk diff
  <file>` exits 2 with diff's `missing operand after` message, and
  stdin is not echoed back as a diff.
- explicit_stdin_dash_still_condenses: `rtk diff -` keeps reading a
  piped diff from stdin and exits 0.
The gate decides deny/ask/allow from `split_for_permissions`, so anything
that segmentation cannot see is a rule that silently does not apply. Three
constructs do that today.

**ANSI-C quoting.** `\'` inside `$'…'` is the one case where the lexer's
quote state provably diverges from bash: bash reads a literal `'` and keeps
the word open, the lexer closes the span and reopens it. Every operator
after that point then produces no token, so

    ls -la $'\'' ; rm -rf /

reaches the gate as a single segment beginning `ls `, which an
`allow: ["Bash(ls:*)"]` rule matches while the `rm -rf /` rides along. The
verdict was Allow, and the hook returned it as an auto-approve.
`ansi_c_quote_defeats_lexer` already detected this but only
`rewrite_multiline_block` consulted it; `contains_unattestable_construct`
now does too, so the gate prompts instead. It moves to `lexer.rs` with
`QuoteScan`, next to the `advance_quote_state` both are built on.

**Leading redirects.** A redirect may precede the command it applies to.
`split_for_permissions` truncated each segment at its first redirect, so
`2>&1 git push --force` truncated at offset 0 and returned no segments at
all: the deny loop iterated an empty list and the rule did not fire. A
redirect seen before any command text is now stepped over, along with its
file operand, so the command behind it is still checked. Trailing redirects
keep truncating as before.

**Grammar residue.** `{` and `}` are not segment boundaries, so
`... && { rm -rf / ; }` reaches the gate as `{ rm -rf /` and an exact deny
pattern cannot match it. Deny and ask now also test the segment with leading
`{`, `}`, `!`, `(`, `)` words stripped. Allow deliberately does not:
stripping can only make a rule fire on more segments, so applying it to deny
and ask can only make a verdict stricter, while applying it to allow would
let `{ git status` inherit `Bash(git:*)` and turn a prompt into an
auto-approve.

Stepping over a leading redirect also widens the allow side: `2>&1 <cmd>`
used to produce no segment at all and fall to Default, and now matches the
rule that covers the command behind it. That is rule-faithful and applies
only to fd-dup and /dev/null targets — a real file target still reaches
`contains_unattestable_construct` and prompts — but it is a genuine
widening, so it is pinned by its own test.

Rewriting is unchanged except where the ANSI-C guard now defers, verified by
differential against develop over 13 compound forms (pipelines, subshells,
background, brace groups, heredocs, multiline, quoted separators, env
prefixes): one diff, the intended one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(hooks): close three permission-gate holes in command segmentation
KuSh and others added 22 commits October 7, 2026 01:16
fix(ls): parse arguments through the tokenizer
`every_variable_rtk_reads_is_redirected_for_a_child` skipped the three
resolver files outright. The exemption is there because a resolver defines the
accessors, so its own `user_env::var_os(name)` names no variable the scan can
resolve — but skipping the whole file also hides any read in it that *does*
name one, and `user_dirs` is where a new path override would naturally be
written.

Forgive the forwarding instead of the file: a read whose name does not resolve
is allowed in a resolver and still refused everywhere else, and a literal read
is collected wherever it appears. A variable added to `user_dirs` now has to be
redirected for a spawned child like any other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…olvers

test(isolation): scan the resolvers for the variables they name
`OTHER_SUBCOMMANDS` and the pre-subcommand option table were written from
recall rather than read off the binary. `docs` is not an ast-grep subcommand,
so `rtk ast-grep -p X docs` -- a search over a directory named `docs` -- was
sent to the passthrough and printed uncompressed. `outline` and `help` are
subcommands and were missing, so both were captured and only rescued by the
unparsed-output guard. `--color` is a subcommand option, not a global one.

Both tables now come from `ast-grep --help` and `ast-grep run --help` (0.45.3):
seven non-`run` subcommands, `-c/--config` as the only value-taking global
option, and `run`'s full arity including `--kind`, `--no-ignore`, `--inspect`,
`--heading` and `--color`. `--json` and `--debug-query` stay out of the table
because ast-grep only accepts their value attached with `=`, and an entry would
make them swallow the next argument -- including a `--stdin`, which has to force
a passthrough.

That same help text lists `-i/--interactive`, which has to reach the terminal
as it happens for the reason `--stdin` does: it drives a full-screen session,
one prompt per match, written to stdout while the answers are read from
/dev/tty. Captured, the session never reached the terminal and ast-grep sat
waiting for an answer to a question nothing had displayed. It joins `--stdin`
in forcing a passthrough.

The hint comment claimed the store decision was taken on text the hint was
already part of; the code reserves a fixed 256 B before the hint exists, since
writing the archive is what produces the path the hint names. The comment now
says so, and its duplicate above the block is gone.

FEATURES.md named the phantom subcommand too, and tied the fallback route to
recovery being off, where the filter also falls back with recovery on when the
compaction does not save enough to pay for a hint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-grammar

fix(ast-grep): route subcommands by the grammar ast-grep actually has
Move the strip out of callers (tool-mode closure and the pipe wrapper)
into the parser itself, as filter_phpunit_output already does, so no
caller can bypass the precondition that caused the pipe-mode bug.
The broken-run passthrough keeps its own strip. Pipe route reverts to
the plain function pointer. Regression test unchanged.
fix(pipe): strip ANSI before the pytest filter in pipe mode
Fixes 42 broken relative links and anchors in the markdown docs and the 20
doc comments that make `cargo doc` fail under `warnings = "deny"`, and adds a
`rustdoc` CI job so the second class cannot come back unnoticed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: fix doc references that never resolved, and gate cargo doc in CI
26b96ec pinned pr-target-check.yml, and next-release.yml matches it. Both are
pull_request_target workflows that read secrets.APP_PRIVATE_KEY and mint a
write-scoped token, which is where a repointed tag does the most damage. Two
workflows in the same position were never covered:

  cd.yml       contents: write, pull-requests: write, reads APP_PRIVATE_KEY,
               and calls googleapis/release-please-action on a floating tag
  release.yml  contents: write, builds and publishes the binaries users install,
               through softprops/action-gh-release and dtolnay/rust-toolchain

A tag is mutable, so whoever owns the action repository can repoint it and have
the new code run against that key, or against the build that produces the
release artifacts. A commit SHA cannot be repointed.

  googleapis/release-please-action  5c625bf  # v4.4.1
  softprops/action-gh-release       3bb1273  # v2.6.2
  dtolnay/rust-toolchain            4360b52  # stable branch, untagged
  actions/create-github-app-token   bcd2ba4  # v3.2.0

Each SHA was resolved from the ref it replaces and annotated with the version
that actually points at it. rust-toolchain is the exception: `stable` is a
branch selecting the Rust toolchain, and the repository's only tag, v1, points
at a different commit, so there is no version to name.

ci.yml is left on tags on purpose. It runs on pull_request with contents: read
and no secrets, so pinning it would buy little and stop the toolchain and cache
actions receiving fixes.

actions/* is not exempt either; the line is privilege, not who owns the
repository. 26b96ec already pins create-github-app-token and github-script,
the two that mint a write-scoped token or run script with secrets in reach,
while checkout, upload-artifact, download-artifact, setup-go and stale stay on
tags because none of them touch a credential. cd.yml and release.yml hold the
three create-github-app-token references 26b96ec did not reach, so one action,
one key and one set of scopes were split across two treatments. v3 and v3.2.0
both point at bcd2ba4 today, so this pins the code already running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`extract_identifier_and_extra_args` picked the PR/issue/run identifier with a
hand-rolled walker: a hardcoded eight-entry list of value-taking flags, a
`skip_next` bool, and `arg.starts_with('-')`. Anything the list missed had its
value read as the identifier, and `pr_action` did not consult the list at all.

Verified against gh 2.46.0 and 2.100.0:

    rtk gh pr checks --interval 30   ->  gh pr checks 30 --interval
    rtk gh run view -j 12345         ->  gh run view 12345 -j
    rtk gh pr edit --add-label bug   ->  "ok edited #bug"

Replace it with `arg_tokenizer::tokenize_grammar` plus one `takes_value`
predicate per gh subcommand, transcribed from each subcommand's own `--help`,
and take the first free positional. `pr_action` now uses the same tokenization,
and `run()` restores the `--` that clap's `trailing_var_arg` consumes so gh
receives the boundary the user typed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`trailing_var_arg` only ever lets clap strip one `--`, the one at the head of
the region it hands to `gh` — before the subcommand, or right after it. That
one is rtk's option terminator and was never part of the gh command line, so
restoring it fabricated an argument: `gh -- pr view 42` is `unknown command
"pr" for "gh"` and `gh api -- repos/o/r --jq .name` is `accepts 1 arg(s),
received 3` on gh 2.46.0 and 2.100.0 alike. Every later `--` clap passes
through untouched and still reaches gh verbatim.

Rebuilding the region before the drop keeps the subcommand from being
reinjected, so `gh -- pr view 42` runs `gh pr view 42`, not `gh pr pr view 42`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`split_identifier` reads positionals past `--` because gh does too
(`gh pr view -- 42` views PR 42), but the callers re-emit the identifier ahead
of the boundary. When the escaped region held more than the identifier, that
promoted a flag the user had escaped back into flag position:
`rtk gh pr checks -- --watch 42` ran gh with `--watch` live, blocking on a
watch the user had asked gh to treat as a positional.

gh takes at most one positional for these subcommands, so an escaped region
holding more than one token is something gh rejects whatever rtk sends. Look
past the boundary only while it escapes a single token, and hand the rest to
gh untouched.

Also isolate HOME and RTK_DB_PATH in the argv test, which otherwise read the
developer's own config and wrote to their real tracking database.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refusing to look past `--` only when two or more tokens trailed it still unescaped
the single-token case, which is the one a user is most likely to type: `rtk gh pr
checks -- --watch` hoisted `--watch` into flag position and ran the blocking watch
the user had escaped, where develop passed the command through untouched. `gh issue
view -- --web` and `gh run view -- --log` went the same way.

What makes hoisting safe is not how much the boundary escapes but whether unescaping
it is a no-op: reach past `--` only for one token that is still read as a positional
without it, so `gh pr view -- 42` keeps gaining `--json`. gh reads everything past
the boundary as a positional -- `gh pr view -R o/r -- --web` resolves `--web` as the
PR selector -- so a flag left there is a flag gh never acts on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`dirs::config_dir()` prefers `XDG_CONFIG_HOME` over `HOME`, so on a machine that sets
it the argv assertions still ran against the developer's real `~/.config/rtk`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `--` tests started rtk from CARGO_BIN_EXE_rtk and pointed HOME,
XDG_CONFIG_HOME and RTK_DB_PATH at a tempdir by hand. A child spawned
that way is not covered by the isolation guard, which fails the suite for
any test file that names the binary directly, because it also writes the
tee spool and recall database under the developer's real data directory.

`common::rtk_command()` pins all of those, plus telemetry, so the manual
settings and the comment justifying them go. The PATH that puts the
argv-recording `gh` stub first is not covered by it and stays.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ci: pin the third-party actions that run with secrets and write access
fix(gh): tokenize gh args instead of walking them by hand
- hooks/opencode/rtk.ts: add resolveRtkPath() with multi-strategy discovery
  (PATH, RTK_BIN, ~/.cargo/bin, Homebrew, /usr/local/bin, Windows paths)
  and use absolute path when invoking rtk hook opencode
- src/hooks/hook_cmd.rs: accept optional bin_path in opencode_answer()
  and emit absolute-path rewrite when provided
- src/main.rs: pass current_exe() as bin_path to run_opencode()

Fixes rtk-ai#4462: OpenCode plugin now works when rtk is installed outside
OpenCode's PATH (e.g., ~/.cargo/bin on macOS GUI launch).
@rtk-release-bot rtk-release-bot Bot added the wrong-base PR targets master instead of develop label Oct 10, 2026
@rtk-release-bot

Copy link
Copy Markdown
Contributor

Automatic message from CI checks : It seems like this branch is targeting the wrong branch, any contribution should target develop branch.

See CONTRIBUTING.md for details.

@rtk-wshm-sync-bot

Copy link
Copy Markdown

wshm · Automated triage by AI

📊 Automated PR Analysis

🐛 Type bug-fix
🟡 Risk medium

Summary

Fixes OpenCode plugin failing to find the rtk binary when OpenCode is launched from a GUI (e.g. macOS Dock) and doesn't inherit the shell PATH. Adds multi-strategy path resolution (RTK_BIN, cargo/homebrew paths, bundled binary) and plumbs a resolved absolute bin_path through to the generated rewrite command. Also bundles unrelated CI/dependency maintenance (pinned GitHub Actions SHAs, Cargo.lock bumps, a new rustdoc CI job, and doc link fixes).

Review Checklist

  • Tests present
  • Breaking change
  • Docs updated

Linked issues: #4462


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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working ci dependencies opencode path-resolution wrong-base PR targets master instead of develop

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants