Skip to content

Next Release - #3367

Merged
aeppling merged 26 commits into
masterfrom
develop
Aug 7, 2026
Merged

Next Release#3367
aeppling merged 26 commits into
masterfrom
develop

Conversation

@rtk-release-bot

@rtk-release-bot rtk-release-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Feats

Fix

  • fix(hooks): stop Copilot from silently deciding permission on unconfigured commands #3212Closes #1425 (to verify)

Other

KuSh and others added 12 commits July 27, 2026 13:59
…AskRewrite

handle_vscode (rtk hook copilot's PascalCase path, shared by VS Code Copilot
Chat and Copilot CLI's Claude-compat entry) always set permissionDecision to
"allow" or "ask", diverging from process_claude_payload which only asserts
"allow" for an explicit user-configured Allow rule and stays silent otherwise.
Asserting "ask" is what caused #3037: Copilot CLI 1.0.66+ treats it as
authoritative and forces a blocking dialog with no "remember" option on every
rewritten command.

84aa4d6/0df6929 patched the wrong path (the camelCase native handler, which
never had this problem) with an auto-allow heuristic gated on an `explicit`
flag, instead of fixing handle_vscode itself. Replace both with the same rule
Claude's own hook and Droid's hook already use: never assert a decision for
AskRewrite, regardless of whether the verdict was Default or an explicit Ask
rule. This removes the Copilot-specific heuristic (and the now-unused
`explicit` field) in favor of one behavior shared across all hosts.

Also extracts vscode_response_from_decision as a pure, testable function
(mirroring copilot_cli_response_from_decision), closing a prior gap where
handle_vscode's decision output had no direct unit test coverage.
…ok config

rtk init --copilot registered both a PascalCase PreToolUse entry and a
camelCase preToolUse entry in the same rtk-rewrite.json, on the assumption
that VS Code Copilot Chat needs the former and Copilot CLI needs the latter.

Live testing showed Copilot CLI treats PreToolUse/preToolUse as two
independent, sequentially-run hooks — a redundant second `rtk hook copilot`
process spawn per tool call, chaining the first hook's rewrite into the
second's input (confirmed via raw stdin capture, and confirmed independent
of declaration order in the file). Also confirmed Copilot CLI honors the
PascalCase-only schema perfectly well on its own, receiving the same
tool_name/tool_input.command shape either way — so the camelCase entry buys
nothing for Copilot CLI, while adding process overhead and an extra,
harder-to-reason-about execution path.

Drop the camelCase preToolUse entry, keeping the single PascalCase
PreToolUse entry shared by both hosts. Existing installs are not upgraded
automatically — re-running `rtk init --copilot` / `rtk init --global
--copilot` overwrites the old dual-schema file with the new one
(write_if_changed overwrites unconditionally on content diff), verified
by test_copilot_init_upgrades_old_dual_schema_install and
test_copilot_global_install_upgrades_old_dual_schema_install, which seed
the old dual-schema content and assert it gets replaced.
detect_format only matched tool_name values "runTerminalCommand", "Bash",
and "bash" for the VsCode hook format. Live payload capture from a real VS
Code Copilot Chat session (agent mode, GitHub.copilot-chat) showed it
actually sends "run_in_terminal" — none of the previously recognized
values — so detect_format fell through to PassThrough and the hook never
fired at all for VS Code Copilot Chat: no rewrite, no permissionDecision,
nothing.

Add "run_in_terminal" to the recognized tool_name set. Verified against the
real captured payload end-to-end: a compound "cd <dir> && git status
--short --branch" command now correctly rewrites only the git segment to
"rtk git status --short --branch", leaving "cd" untouched, with no
permissionDecision asserted (consistent with the Default-verdict behavior
fixed in 042aeaf).
…ot generated

The doc comments described the camelCase toolName/toolArgs schema as "GitHub
Copilot CLI"'s format, which was accurate when rtk init --copilot registered
it. Now that only the PascalCase schema is generated, this path is reachable
only via not-yet-upgraded installs' leftover registration, or hosts that use
this schema under a different toolName value on their own (JetBrains/
IntelliJ's Copilot plugin sends "run_in_terminal", tracked in #2443/#3093).
Note both call sites accordingly so the code isn't mistaken for dead weight.
…owershell

Contributor testing on Windows 11 with Copilot CLI 1.0.73 (#3179) confirmed
Copilot CLI remaps its native bash/powershell shell tool to tool_name: "Bash"
under the PascalCase PreToolUse schema, and that its updatedInput is honored
end-to-end there. Since rtk init --copilot now only registers that schema,
Windows already works standalone through it — the camelCase toolName
"powershell" case (#3178/#3179) becomes legacy-only, relevant solely to
un-upgraded installs. Document that on both HookFormat variants.

Also fixes a HookDecision::AskRewrite struct-pattern leftover in
copilot_ide_response_from_decision (added by #3093, merged into develop
after this branch's own struct-to-tuple AskRewrite refactor), surfaced as a
compile error by rebasing onto develop to pick up #3179.
Coding agents routinely emit multi-line Bash blocks of sequential
commands. The rewriter tokenized newlines as plain whitespace, so a
block was treated as one command: the rtk prefix landed on the first
line and every following line ran raw and unfiltered.

Split multi-line input at the newline tokens the quote-aware lexer
emits (newlines inside quoted strings are never split points) and
rewrite each line through the existing single-line path. Blank lines,
comment lines, indentation, and CRLF separators are preserved verbatim.

Per-line rewriting only applies when every line is an independent
command. The whole block passes through untouched when:
- a shell keyword opens control flow (for/if/while/case/...)
- a list or pipeline continues across the line break (&&, ||, |, |&
  at a line edge)
- a subshell or group spans lines
- the block contains a heredoc (existing gate)

Ref #1243
Hardening from adversarial review of the multi-line rewrite:

- If any newline byte was swallowed by quote state (raw \n/\r count vs
  emitted newline tokens), pass the whole block through. The lexer has
  no comment awareness, so an apostrophe in a # comment opens quote
  state and hides subsequent lines; rewriting such a block would act on
  lines no permission verdict accounted for. Passthrough hands the
  original command to native permission handling. Quoted multi-line
  strings (commit messages) forgo their rewrite as the safe trade.
- Bail when (( or )) sits at a line edge: arithmetic spanning lines
  must not get a command prefix spliced into arithmetic context.
- Strip trailing unquoted comments before the independence checks:
  'git log | # keep pipeline' continues the pipeline across the
  newline even though the line ends in comment text, and rewriting
  the next line would rebuild a pipeline whose final stage the
  pipeline-safety guard may specifically reject (e.g. grep -f).
- Bail when a line's unquoted ()/{} don't balance: array literals
  (arr=(one), function bodies (foo() {), and groups span lines, so
  surrounding lines are not independent commands. Subsumes the
  previous single-char edge checks.
- Bail on any block containing $'...': inside ANSI-C quoting bash
  treats \' as a literal quote that does not close the string, but
  the lexer thinks it does — emitting a split point bash would never
  honor, the inverse of the swallowed-newline case the count check
  catches.
…on-decision

fix(hooks): stop Copilot from silently deciding permission on unconfigured commands
aeppling and others added 2 commits August 3, 2026 10:41
feat(rewrite): rewrite multiline blocks
Add `rtk init -g --agent vibe` and `rtk hook vibe` to route bash tool
calls through the RTK proxy via Vibe's newly-shipped pre_tool hook.

Implementation follows the Gemini / Droid pattern:
- Native binary hook (`rtk hook vibe`), no shell script dependency.
- Global-only install (`~/.vibe/hooks.toml`); user-scope only.
- Idempotent install: detects existing `name = "rtk-rewrite"` entry.
- Uninstall is surgical: strips only the RTK `[[hooks]]` block and the
  `~/.vibe/prompts/rtk.md` prompt file, preserving any other user hooks
  byte-for-byte. Removes hooks.toml only when it becomes empty.
- Hook response uses Vibe's documented `hook_specific_output.tool_input`
  rewrite contract with a `system_message` for UI visibility.

Vibe hook API reference:
https://docs.mistral.ai/vibe/code/cli/hooks

Closes #800.

@pszymkowiak pszymkowiak 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.

Reviewed the two constituent changes (#3212 already reviewed/approved separately; #3319 read independently despite prior approvals).

For #3319, went beyond a code read — built develop HEAD (3044911) in an isolated worktree and verified:

  • fmt/clippy/cargo test --all: 2562+ tests, 0 failed
  • Manually exercised the multiline-rewrite path via rtk hook check:
    • independent lines → each rewritten independently
    • &&-continuation → joined and rewritten as one unit
    • multi-line quoted commit message → newline inside quotes preserved, not split
    • shell function definition → correctly classified unsafe, full passthrough
    • blank line inside a continuation → collapses correctly
    • plain single-line commands (git status, cargo test) → unaffected, confirming no regression on existing behavior

All CI green (including full cross-platform pre-release builds). Bundle diff matches its description exactly, nothing unreviewed slipped in. Approving.

xavierpestel-ai and others added 11 commits August 5, 2026 13:55
…docs

Addresses @aeppling's review on #3391:

Blocking fixes:
- run_vibe now returns Ok(()) on malformed JSON (matches run_droid /
  run_copilot / run_cursor pattern). Prior code violated the exit-code
  contract documented at src/hooks/README.md:100 — a bad payload exited
  non-zero and blocked the agent's command. Fixed via a match on
  serde_json::from_str with a stderr warning fallback.
- Extract run_vibe_inner(input: &str) -> Option<String> from run_vibe so
  the hook contract is unit-testable (mirrors run_droid_inner). Public
  run_vibe becomes a thin stdin/stdout wrapper.
- Add 6 runtime tests exercising the hook contract: bash rewrite happy
  path, non-bash tool passthrough, empty command passthrough, malformed
  JSON returns None, unknown binary passthrough, substitution defers.

Should-fix:
- Telemetry agent detection: add ~/.vibe/hooks.toml to detect_hook_type()
  checks in src/core/telemetry.rs, plus the two test enum arrays so Vibe
  sessions no longer report as 'unknown' in rtk gain history.
- Dead deny arm: add a comment on Host::Vibe in permissions.rs
  documenting that the empty-rules branch is defensive scaffolding for
  when Vibe ships native denylist/allowlist config we can honor.
- Broken link: patch_vibe_hooks_toml skip-message now points at
  https://www.rtk-ai.app/guide/getting-started/supported-agents#mistral-vibe
  instead of a fragment that doesn't resolve.

Nits addressed:
- Install summary no longer prints 'hook installed' when the user chose
  PatchMode::Skip or declined the interactive prompt. patch_vibe_hooks_toml
  now returns a VibeHookPatchOutcome enum (Installed / AlreadyPresent /
  Skipped) and the caller gates the summary on it.
- Document the string-spacing tradeoff on vibe_hooks_toml_has_rtk: a
  reformatted 'name="rtk-rewrite"' would defeat idempotency, acceptable
  because we control the writer and toml_edit round-trip would clobber
  user comments.
- Fix stale line in src/hooks/README.md 'Adding New Functionality':
  hook_check.rs::maybe_warn() only checks the Claude Code hook now,
  not every agent.

Documentation:
- docs/guide/getting-started/supported-agents.md: frontmatter now lists
  Mistral Vibe, drop 'planned' from the intro, tier table row flipped
  from 'Planned (#800)' to 'Rust binary (pre_tool) / Yes', replace the
  ### Mistral Vibe (planned) placeholder with a full user-facing section
  modeled on Factory Droid (install/uninstall commands, hook mechanism,
  permission semantics, idempotency contract).
- hooks/README.md: agent count 9 -> 10, add Vibe entry to Directory
  Structure list, add Vibe row to Supported Agents table, add
  '### Mistral Vibe (Rust Binary)' entry to the JSON Formats section
  showing the pre_tool input shape and rewrite response shape.
- src/hooks/README.md: agent count 5 -> 6, add Vibe row to per-host
  ask-support table.
- README.md: '15 AI coding tools' -> '16'.

No behavior change for existing agents.
Every other agent with a dedicated hook implementation carries a
hooks/<agent>/README.md (see antigravity/cline/opencode/copilot/hermes
for the shape). The initial Vibe commit skipped this, leaving Vibe as
the odd one out in the hooks/ layout.

- Add hooks/vibe/README.md following the Copilot template (Rust binary
  hook, no shell dependency). Documents the pre_tool hook location,
  input JSON shape, rewrite response, passthrough / deny behavior,
  and the belt-and-suspenders prompt fallback.
- Fix hooks/README.md Directory Structure entry to point at
  vibe/README.md (previously claimed 'no dedicated subdirectory').
The comment was previously added in response to review point #5. Removed
per follow-up feedback — the arm itself is self-explanatory in context
alongside the other Host variants.
Legacy camelCase invocation rewrites the old stock config to the single-schema form, so upgrades need no manual rtk init --copilot.
Match the exact stock camelCase entry instead of the whole file, so extended configs (extra hooks/keys) heal too; anything non-stock stays untouched.
Real-binary tests: heal correctness, refusal matrix, response integrity, concurrency, unwritable dir.
…paths

The Semgrep security scan on PR #3391 flagged 2 new fs::remove_file calls
in uninstall_vibe_at as blocking findings under the filesystem-deletion
rule (WARNING severity, but the CI runs semgrep --error which promotes
all findings). Both calls are legitimate uninstall behavior:

- prompt file removal at src/hooks/init.rs:4697 — removes only
  ~/.vibe/prompts/rtk.md, which RTK installed itself.
- hooks.toml removal at src/hooks/init.rs:4714 — removes the file only
  when it becomes empty after stripping the RTK entry, so no orphan
  empty file is left behind.

Both suppressions follow the existing repo convention (`// nosemgrep:
<rule-id> -- <justification>` on the line above the code), matching
precedents in src/discover/lexer.rs and src/core/stream.rs.
Two follow-ups to #3391 (review):

1. Move the summary-verb mapping onto VibeHookPatchOutcome as
   summary_verb() -> Option<&'static str>, returning None for Skipped.
   The call site becomes 'else if let Some(v) = outcome.summary_verb()'
   which collapses the guard and the match into a single decision point
   and removes the unreachable!() branch. If a future variant is added,
   the compiler forces a decision in summary_verb() and the caller
   handles it naturally through the Option.

2. uninstall_vibe now prints a stderr warning when resolve_vibe_dir()
   fails instead of silently returning Ok(()). Users asking to uninstall
   no longer see an empty response when the home dir can't be resolved.
   uninstall_gemini has the same swallow-and-return-Ok pattern; leaving
   that untouched here to keep the diff scoped to Vibe, but the same
   improvement would apply as a follow-up.

Both are quality improvements with no behavior change on the happy path.
feat(hooks): transparent pre_tool rewrite for Mistral Vibe CLI (closes #800)
@aeppling

aeppling commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

needs : #3449

fix(hooks): copilot self heal dual hooks (drop camelCase entry)
@aeppling
aeppling merged commit bbd9a70 into master Aug 7, 2026
1 of 11 checks passed
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.

5 participants