Skip to content

fix(hooks): recognize configured agent hooks in warning check (#913) - #4343

Open
Aayushyaash wants to merge 4 commits into
rtk-ai:developfrom
Aayushyaash:fix/multi-agent-hook-detection
Open

Aayushyaash wants to merge 4 commits into
rtk-ai:developfrom
Aayushyaash:fix/multi-agent-hook-detection

Conversation

@Aayushyaash

Copy link
Copy Markdown
Contributor

Summary

Fixes #913

  • Stop showing the false [warn] No hook installed message in rtk gain and command interception when any supported agent is configured, including OpenCode, Cursor, Antigravity, Gemini, and Codex.
  • Move agent detection into each agent's initialization module under src/hooks/init/<agent>.rs using a consistent is_configured() -> bool function instead of doing path checks directly in hook_check.rs.
  • Keep outdated hook warnings ahead of missing hook checks so an upgrade warning is not hidden when another agent is configured.
  • Keep hook_check::status() tied to Claude Code so rtk discover continues to use Claude's hook status.
  • Remove obsolete test helpers and unused code from hook_check.rs.

Why This Change Is Needed

hook_check.rs and rtk gain originally assumed Claude Code was the only configured agent. If Claude was not configured, status() returned HookStatus::Missing, which caused [warn] No hook installed to be shown on every run.

This became especially confusing on Windows after running rtk init -g --opencode. The OpenCode plugin was active and saving tokens, but rtk gain still reported that no hook was installed.

How It Works

  1. Per-Agent Detection: Each agent module under src/hooks/init/<agent>.rs now exposes pub fn is_configured() -> bool. The agent module already knows where its configuration lives, such as settings.json, hooks.json, or its plugin directory, so the detection stays with the agent instead of being duplicated in hook_check.rs.

  2. Check All Supported Agents: hook_check.rs keeps a list of the supported agent probes and checks whether any of them is configured.

  3. Outdated Hooks Take Priority: is_any_hook_outdated() is checked before is_any_agent_configured() in both check_and_warn() and gain.rs. For example, if OpenCode is configured but the Claude hook is outdated, the user still gets the upgrade warning.

  4. Keep rtk discover Behavior: hook_check::status() still checks Claude Code specifically. rtk discover uses Claude's hook status when working with Claude session logs, so this keeps that behavior unchanged.

Test Plan

  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test --bin rtk hooks:: (582 passed)
  • Added integration tests in tests/hook_detection_multi_agent_test.rs:
    • Verified the warning is shown when no agent is configured.
    • Verified the warning is silenced when OpenCode (rtk.ts), Cursor (hooks.json), Gemini (settings.json), or Antigravity is configured.
    • Verified the outdated hook warning is still shown when another agent is configured.
  • Manual testing on Windows with OpenCode: verified rtk gain reports token savings without the false warning.

@Aayushyaash

Copy link
Copy Markdown
Contributor Author

A quick fix for #913 would have been to add an OpenCode path check directly to hook_check.rs. I went with the per-agent probes instead because each agent already has its own init module and knows how its configuration works.

1. Keep configuration knowledge with the agent

Each agent under src/hooks/init/<agent>.rs already knows its own config files, paths, and formats. Adding is_configured() -> bool there keeps that knowledge in one place instead of duplicating paths and config details in hook_check.rs.

If an agent changes its config format or install path later, its detection logic can be updated in the same module.

2. Avoid the hook masking issue

There was a previous attempt to solve this in hook_check::status() with an other_integration_installed check. Looking at the history around commits 9cf9ccc1 and d85fe338, that approach was reverted because making status() report success when another agent was configured could hide the actual Claude hook status and affect rtk discover.

This refactor keeps hook_check::status() focused on Claude, while is_any_agent_configured() is used specifically for warning suppression. That fixes #913 without changing what status() means for rtk discover.

3. Clean up old code while making the change

There were also a couple of leftovers in hook_check.rs from the previous approach:

  • binary_hook_registered() duplicated Claude's hook_already_present() and was only used by one test.
  • other_integration_installed() and its seven tests were still under #[cfg(test)] even though the implementation had been reverted.

I removed these along with the related test code. This also removes the need for the #[allow(dead_code)] workaround in that file.

4. Make adding agents simpler

With this structure, adding support for another agent means implementing is_configured() -> bool in its init module and adding it to AGENT_PROBES, rather than adding more agent-specific path checks to hook_check.rs.

@rtk-wshm-sync-bot

Copy link
Copy Markdown

wshm · Automated triage by AI

📊 Automated PR Analysis

🐛 Type bug-fix
🟡 Risk medium

Summary

Fixes a false '[warn] No hook installed' message in rtk gain and command interception when any supported agent (OpenCode, Cursor, Antigravity, Gemini, Codex, etc.) is configured but Claude Code is not. Moves per-agent hook detection into each agent's src/hooks/init/<agent>.rs module via a consistent is_configured() function, aggregates them in hook_check.rs, and ensures outdated-hook warnings still take priority over missing-hook checks while rtk discover keeps using Claude-specific s…

Review Checklist

  • Tests present
  • Breaking change
  • Docs updated

Linked issues: #913


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

…ules

- Move agent configuration detection into corresponding init submodules with a common `is_configured() -> bool` interface.
- Replace ad-hoc path checks with self-contained probes owned by each agent's initialization module for consistent hook detection.
- Expose init submodules with `pub(crate)` visibility for use by hook-check logic.
@Aayushyaash
Aayushyaash force-pushed the fix/multi-agent-hook-detection branch from 828922d to 7e2279c Compare October 1, 2026 13:09
…#913)

- Fix false `[warn] No hook installed` in `rtk gain` and command interception when a supported agent is configured (especially on Windows with binary hooks).
- Use per-agent configuration probes to detect whether any supported agent has an active RTK hook or plugin.
- Evaluate outdated-hook detection ahead of missing-hook suppression so upgrade warnings are not hidden.
- Keep `hook_check::status()` tied to Claude's probe to preserve `rtk discover` transcript analysis fidelity.
- Add integration test coverage for Antigravity, Cursor, OpenCode, and Gemini.
@Aayushyaash
Aayushyaash force-pushed the fix/multi-agent-hook-detection branch from 7e2279c to 9bc2f39 Compare October 1, 2026 13:25
- Remove dead configured_agents() utility and its consistency test.
- Remove duplicate binary_hook_registered() helper and test (superseded by claude::hook_status).
- Remove obsolete other_integration_installed() test helper and its 7 unit tests.
- Clean up unused constants and imports across hook_check.
- Add explanatory doc comment on hook_check::status() documenting Claude delegation for rtk discover.
@Aayushyaash

Copy link
Copy Markdown
Contributor Author

@KuSh @aeppling
This PR is ready for review! Whenever you have a moment, I’d appreciate you taking a look. Thanks!

Resolve merge conflict in src/hooks/init/gemini.rs by adopting #[cfg(all(test, feature = engine))], and adapt claude hook_already_present call to new signature.
@Aayushyaash
Aayushyaash force-pushed the fix/multi-agent-hook-detection branch from 3cd876f to 079bb3c Compare October 14, 2026 07:02

@KuSh KuSh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Round 1 — CHANGES REQUESTED

Claim: silence the false No hook installed warning in rtk gain and in the daily check when another agent's RTK hook is configured, keep Claude's outdated warning first, and leave hook_check::status() (used by rtk discover and the init footer) unchanged.
Scope: accept if narrowed to "silence the missing-hook warning when another agent's RTK hook is registered, with Claude's outdated warning still first". The new Cursor "outdated" check is cut (blocker 2).
Ran: 66 home/workspace states, each run under the merge-base binary and under this head merged onto develop: rtk ls . (the daily check, which is what the hook's rewritten commands reach) and rtk gain, with stderr captured separately, plus rtk init --show. The states are 45 real rtk init installs and uninstalls from this head (--agent × 13, --opencode, --gemini, --codex, --copilot, global and project scope), 9 hand-built Claude settings shapes (legacy script v2/v4, registered/dangling/unreadable, absolute path, BOM, PostToolUse only), 6 Cursor legacy shapes (including the hooks.json that pre-binary installs wrote, and following the advice from each) and 6 controls. Invariants: every decision, split streams. Savings: n/a (no filter).
Pins: 34 decisions, 34 mutated, 21 survived the suite (listed in blockers 1–3); 4 equivalent mutants
Matrix: executed this round; pin check: executed this round
CI @079bb3c6e: all green (14 checks). Local gate on the merge with develop is green too, apart from one test that fails identically on the merge-base on this machine.

What works: on develop, an OpenCode plugin with a ~/.claude that has no hook warns on every path; on this head it is silent. Global installs of Gemini, Cursor, Trae, Antigravity, Pi, OMP, Hermes, Droid and Vibe, plus project-scope Trae, Antigravity, Pi, OMP and Hermes, go from No hook installed to silent. Every uninstall brings the warning back, and suppress_hook_warning still wins.

Blocks merge (3, frozen at round 1)

  1. src/hooks/init/claude.rs:990 — status() is no longer develop's function (one cause, four shapes). The removed binary_hook_registered accepted only rtk hook claude (any rtk path) under PreToolUse. hook_already_present also accepts any command containing rtk-rewrite.sh, and the new code checks PostToolUse too:

    • settings.json points at ~/.claude/hooks/rtk-rewrite.sh, script deleted: develop No hook installed, head silent (a broken hook stops being reported)
    • settings.json points at the v4 script (the version hooks/claude/rtk-rewrite.sh ships): develop silent, head Hook outdated every day
    • rtk hook claude only under PostToolUse: develop No hook installed, head silent
    • script exists but is unreadable: develop Outdated, head Missing

    The same value feeds rtk discover and the rtk init footer, which the PR says stay unchanged. A fix passes when these rows match develop, and the absolute-path case (/opt/homebrew/bin/rtk hook claude → silent) is pinned again, since this PR deletes its test. Keeping develop's matcher inside claude.rs satisfies all of them.

  2. src/hooks/init/cursor.rs:268 — the Cursor "outdated" arm only fires on files nothing runs (cut by the narrowed claim). binary_registered uses cursor_hook_already_present, which also matches rtk-rewrite.sh commands. On a real legacy install (hooks.json → ./hooks/rtk-rewrite.sh plus the script), the arm stays quiet. It fires only for a stray script that hooks.json does not reference. A Cursor-only user with no ~/.claude then gets Hook outdated — run rtk init -g to update on every command, once a day. Following that advice creates ~/.claude and installs the Claude hook. Making the arm fire on real legacy installs would not help either: rtk init -g deletes the script and leaves hooks.json pointing at it (#3465, #3693). Please remove cursor::is_outdated. Once it is gone, is_any_hook_outdated() equals status() == Outdated, so it and its three call sites can go, and warning_text becomes a pure function again.

  3. src/hooks/hook_check.rs:91 and the probes — unpinned behaviour (mutations that pass the whole suite). The mutations added an env-var guard. only_user_dirs_resolves_user_locations went red on each one, but that test is a lint about env access, not these decisions.

    • Daily check, the [rtk] /!\ line that #913's "command interception" refers to: || is_any_agent_configured() dropped from suppress_missing passes. The only tests run rtk gain. Test must: rtk ls . with ~/.claude and an OpenCode plugin → stderr has no No hook installed. The same command without the plugin must still show it.
    • Probes with no test at all, each replaced by return false: codex, droid, trae, hermes, pi, vibe. Arms that also survive: Trae and Pi project scope (loop reduced to [true]), OMP extension, Hermes plugin file, Hermes config.yaml, the Gemini matcher (any matcher accepted), Gemini's script-file fallback, and Cursor's legacy-file arm. Test must: for each agent, the files rtk init [-g] --agent <a> writes → silent, and the same files after --uninstall → warning back. Each mutation above must turn one of them red.
    • Gemini's script-file fallback and Cursor's legacy-file arm count an unregistered file as configured, so a script whose settings entry is gone silences the warning. Dropping those two arms is simpler than pinning them.

Optional — will not hold merge

  • Copilot (rtk init -g --copilot and project scope), project-scope Codex (.codex/hooks.json) and project-scope Droid (.factory/hooks.json) still warn: their probes read only the global directory, or there is no probe. On develop they warn as well, so this is not a regression. If you would rather keep this PR to what it has, I will file it as an issue on approval.
  • The claude entry in AGENT_PROBES never decides anything: it is true only when status() is Ok, and both callers run it only when status() is Missing. claude::is_configured() is also true when ~/.claude does not exist. Removing the entry is simpler than explaining it.
  • test_agent_probes_covers_all_supported_agents compares the list with a copy of itself. strip_leading_bom before from_json_str is redundant: develop has no strip, and a BOM settings file reads the same on both sides. copilot became pub(crate) without a caller, and the comment under the mod block in init/mod.rs ("every other submodule is used only from here") is now stale.

Checked and correct — no need to re-verify

  • The warning returns after --uninstall for every agent with an uninstall path (Cursor, Antigravity, Pi, Hermes, Droid, Vibe, Trae, Gemini, Codex).
  • Claude outdated still beats another configured agent (Antigravity + v2 script → Hook outdated).
  • hooks.suppress_hook_warning and RTK_SUPPRESS_HOOK_WARNING=1 still silence Missing, and Outdated still shows under them.
  • rtk init --show output is identical on both sides for all 66 states.
  • There is no measurable cost on the hooked commands: rtk ls . takes 5.23 ms vs 5.45 ms with OpenCode only, and 5.25 ms vs 5.09 ms with Cursor.
  • Antigravity's hard-coded paths match what run_antigravity_mode_at writes, in both scopes.

Your questions

  • None asked since the PR opened.

Next

Blockers 1–3: develop's Claude matcher, remove the Cursor outdated arm, and tests that kill the mutations in 3. After that, this merges. The Optional items are your call.
Threads: 3 open (inline, waiting on the author), 0 resolved.

Comment thread src/hooks/init/claude.rs
if let Ok(content) = fs::read_to_string(&settings_path) {
let content = strip_leading_bom(&content);
if let Ok(root) = from_json_str::<serde_json::Value>(content.trim())
&& [PRE_TOOL_USE_KEY, POST_TOOL_USE_KEY]

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.

Blocker 1. hook_already_present is not the removed binary_hook_registered: it also accepts any command containing rtk-rewrite.sh, and this checks PostToolUse as well. Merge-base vs this head: a settings entry for a deleted ~/.claude/hooks/rtk-rewrite.sh goes from No hook installed to silent, a registered v4 script goes from silent to Hook outdated, a PostToolUse-only entry goes from No hook installed to silent, and an unreadable script goes from Outdated to Missing. rtk discover and the init footer read the same value. Keeping develop's PreToolUse + is_claude_hook_command matcher here makes all four rows match develop.

Comment thread src/hooks/init/cursor.rs
}

/// Returns true if Cursor hook is installed via legacy script instead of binary command.
pub fn is_outdated() -> bool {

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.

Blocker 2 (cut by the narrowed scope). cursor_hook_already_present matches rtk-rewrite.sh commands too, so on a real legacy install (hooks.json → ./hooks/rtk-rewrite.sh plus the script) this returns false. It is true only for a script nothing references. A Cursor-only user then gets Hook outdated — run rtk init -g daily, and following it installs the Claude hook. A version that fires on real legacy installs would lead into #3465/#3693. Please remove it. is_any_hook_outdated() then equals status() == Outdated and can go with it.

Comment thread src/hooks/hook_check.rs
let suppress_missing =
status == HookStatus::Missing && crate::core::config::hook_warning_suppressed();
let suppress_missing = status == HookStatus::Missing
&& (crate::core::config::hook_warning_suppressed() || is_any_agent_configured());

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.

Blocker 3 (unpinned). Dropping || is_any_agent_configured() here passes the whole suite: every new test runs rtk gain, none runs the daily check. Test must: rtk ls . with ~/.claude and an OpenCode plugin → stderr without No hook installed, and the same command without the plugin still shows it. The probe mutations in the review body need the same treatment.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Persistent "No hook installed" warning on Windows after rtk init -g --opencode

2 participants