Repository navigation
Next Release - #4399
Open
rtk-release-bot[bot] wants to merge 143 commits into
Open
Next Release#4399rtk-release-bot[bot] wants to merge 143 commits into
rtk-release-bot[bot] wants to merge 143 commits into
Conversation
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 #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>
`rtk rewrite --help` still described a two-code protocol (0 rewritten, 1 no equivalent) and suggested `$(rtk rewrite "$CMD") || exit 0`. The command returns four codes, and with no permission rules it exits 3 by design (#1155), so a hook following the help text drops every default rewrite. List all four codes in the help, note that the no-rule default is 3, and align the table on `rewrite_cmd::run` with the protocol tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WcTsSr2cKxstnvgXk2bP88
Address #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.
…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
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 #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 #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 #4052 to #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>
`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
Matching the last dot-segment also accepted any plugin tool ending in .bash (e.g. myplugin.bash). Accept only bash and file_system.bash.
# Conflicts: # docs/contributing/TECHNICAL.md
fix(core): emit one trailing newline, not one on top of the output's own
`;;`, `;&` and `;;&` each terminate a `case` arm, but the lexer emitted a
separate `Operator(";")` per character. `rewrite_compound` splits on every
operator and rebuilds the text around it, so the second half of a terminator
came back as its own empty segment and the arm was re-emitted with the two
halves apart:
case x in a) echo 1;; esac -> case x in a) echo 1; ; esac
case x in a) echo 1;& esac -> case x in a) echo 1; & esac
The first is a bash syntax error, so any command mixing a `case` statement
with a rewritable command never ran at all. The second is worse than an
error: `; &` is valid, and turns a fall-through into a background job.
The `;` arm now folds `;;`, `;&` and `;;&` into one operator token, matching
bash's own maximal munch — `;;;` is `;;` then `;`, and `;&&` is `;&` then
`&`, which bash's syntax errors name explicitly. The emitter's existing
no-padding branch for `;` covers every terminator starting with one, so the
terminator itself is reproduced verbatim instead of being padded to ` ;; `.
Whitespace after a terminator is not preserved: that branch appends a space
when the operator is not the last token, so `echo 1;;esac` comes back as
`echo 1;; esac`. Both parse identically, and the behaviour predates this
change — it applied to bare `;` already.
No existing test changes: the old two-token form produced an empty segment
between the halves, which the segmenters already dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-up: the segmenter doc comments and the discover README still named only `&&`, `||` and `;` as split points; `;;`, `;&` and `;;&` are now one operator token each and split the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(hook): keep `case` terminators intact when rewriting (#3197)
Exit 0 needs allow rules covering every command of a chain, only RTK_REWRITE_HOST=openclaw relaxes the default 3, and exit 1 also covers excluded commands, `$(...)` and file redirects. Say so in --help and on the rewrite_cmd::run table, update the outdated FEATURES.md section, and test that --help gives each exit code its own paragraph. Co-authored-by: tandede <1090179959@qq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…codes docs(hook): document all four rtk rewrite exit codes in --help
fix(hook): match namespaced bash tool name in the Vibe hook
…them
Round 2 blocker 1. Four arms of the same class: a config shape where rtk saw
fewer rules than OpenCode does, and a rewrite moved the command out from under
an ask or a deny.
`execute.before` runs before permission evaluation, so the engine judges the
string the plugin hands back. If that string no longer matches a rule the
original did, the rule is gone. djbclark reproduced exactly that on 2.0.26: a
`deny` on `*PERMTEST-REWRITTEN*` rejected `echo PERMTEST-ORIGINAL` after a
rewrite, which proves the engine evaluated the rewritten form. Our defence is
comparing the verdict before and after and answering `{}` when they differ, and
that defence is only as good as our model of the rules.
The ordering. OpenCode reads the root rules of every file first, then the agent
rules of every file. rtk looped over the files and read root-then-agent inside
each, so a project `allow` outranked a global agent `ask`. Now two passes.
The agent block. Both spellings are legacy maps and both precede the 2.x list,
but rtk appended `agent.<name>.permission` after `agents.<name>.permissions`.
One file declaring both is now read map-first, so its ask wins.
Wildcard actions. OpenCode matches `action` as a glob. `sh*` was dropped by a
comparison against the two exact spellings, which is the same as not reading the
rule at all.
Markdown agents. An agent can be a file under `.opencode/agents/` with
permission rules in its frontmatter, and rtk read none of them. Their contents
are still not parsed; `agent_rules_unreadable` reports that a key is present and
`rtk hook opencode` answers `{}`, which is the safe direction. An agent file
with no such key keeps its rewrites, since most carry only a prompt and a model.
Five tests, one per arm. I reverted each fix to confirm the suite goes red
without it: the ordering and the agent-block swaps and the wildcard test all
fail on the old code, and the markdown one failed until a test exercised the
call site rather than the helper — the helper test passed with the call site
disabled, so `a_markdown_agents_rules_stop_the_rewrite_entirely` now goes
through `opencode_answer_for`.
The README described the order as "Global before project, agent last", which
was never what the code did.
… load it Round 2 blocker 2. `rtk init -g --opencode` printed a warning that the plugin would not load, then wrote it anyway — so on 1.1.4 the user got an OpenCode that exits 1 at startup, having been told exactly that would happen. The warning and the write contradicted each other. Below the floor nothing is written now, and an existing file is left as it is rather than replaced, so somebody who already has a working plugin keeps it. The message says which of the two happened, since "installed nothing" and "left your plugin in place" call for different follow-ups. The banner is now reported as `x.y.z` rather than the whole `opencode --version` line, which on some builds carries more than the number and wrapped the message mid-sentence. The version arrives as a parameter instead of being spawned here. That is what makes the no-write branch testable, and it also fixes the optional note from the review: `test_opencode_plugin_install_and_update` ran with `dry_run=false`, so its result depended on whichever OpenCode the developer had installed. Three tests now cover the branch directly — an old version leaves an existing file alone and does not create a missing one, a new or unreadable version still gets the plugin, and the install/update test passes `None` explicitly. Removing the early return turns the suite red.
feat(rewrite): rewrite inner commands of a quoted fish -c wrapper
clippy runs with -D warnings over --all-targets, so a function left alive only for a test fails the build. `too_old_opencode` absorbed `is_too_old` when it started taking the banner as a parameter and needed the parsed triple, and the test kept calling the old name. The test now calls `too_old_opencode` itself, which is what it should have been doing anyway: the old pair of assertions went through a helper that shared no code with the branch that actually runs. It also picks up the trimmed-output check while it is there, since that was the reason the function was split at all.
aeppling
approved these changes
Oct 11, 2026
aeppling
approved these changes
Oct 11, 2026
aeppling
approved these changes
Oct 11, 2026
…init honest - A 2.x list rule whose action only names `bash` is no longer counted: OpenCode ignores it, and a counted allow outranked an ask it applies. - A legacy map key the two generations read differently (`shell` and `sh*` apply only on 2.x, `b*` only on 1.x) now stops the rewrite: rtk cannot tell which generation it is serving. Configs are read once per call. - Agent frontmatter keys are found quoted, after a byte-order mark, or in an unclosed block, and agent files in the singular `agent/` directory too. - Below the floor, init no longer prints the plugin path or an OpenCode restart hint for a plugin it did not write, and its message and the docs no longer claim the plugin cannot load from 1.3.4 on. - Tests and dry runs no longer spawn the developer's own `opencode --version`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Blocker: regression vs master from #4126
{"permissions":{"allow":["Bash(env:*)"],"deny":["Bash(git push:*)"]}}$ rtk rewrite "env bash -c 'git push'"; echo "exit=$?"
env bash -c 'rtk git push'
exit=0
|
4 tasks done
aeppling
pushed a commit
that referenced
this pull request
Oct 12, 2026
#4126 made the rewrite descend through prefixes (env, assignments, timeout, nice, nohup, time, exec, command, uv run, configured transparent_prefixes) to a quoted shell wrapper, but the permission gate still looked for the wrapper at the segment start only. A denied inner command rode the outer allow: `env bash -c 'git push'` under `allow env:*` + `deny git push:*` was rewritten and auto-allowed. The gate now checks deny rules against the wrapper's script at every position the rewrite reaches (`registry::shell_wrapper_positions`, the rewrite's own prefix walk), on the text the rewrite reads: continuations joined, segments not cut at a redirect (`lexer::split_for_wrappers`). A match returns the new `PermissionVerdict::Defer`: no rewrite, no decision, the host sees the command as typed — master's behaviour, instead of #4126's Deny. Nothing else changes: a prefixed wrapper otherwise keeps the outer command's verdict, and a wrapper at the segment start keeps develop's rule. Fixes the release blocker reported on #4399. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Feats
rtk init --opencodeplugin silently does nothing on Desktop ($ is not a function) and on Windows (whichnot found) #3326) #4187 — Closes #1993 (to verify), #2516 (to verify), #3326 (to verify), #3463 (to verify), #3898 (to verify)Fix
caseterminators intact when rewriting (bug(hook): rewrite path turns;;into; ;, breaking everycasestatement (bash syntax error) #3197) #4130rtk uvrows as python in ecosystem mix #4328 — Closes #4316 (to verify)rtk init --opencodeappears to skip Claude hook installation #4046) #4323 — Closes #2519 (to verify), #4046 (to verify)Other