feat(hook): native Claude Code hook for Windows (no bash/jq required)#954
feat(hook): native Claude Code hook for Windows (no bash/jq required)#954Warrio111 wants to merge 1 commit intortk-ai:developfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a Windows-native Claude Code hook entrypoint so rtk init -g can install a hook without relying on bash/jq, enabling automatic command rewriting in Claude Code on Windows.
Changes:
- Add
rtk hook claudesubcommand routing (native hook processor). - Update Windows
rtk init -gdefault path to install a native hook command into~/.claude/settings.json. - Extend hook detection/removal logic to recognize
"hook claude"entries.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
src/main.rs |
Adds hook claude subcommand to the CLI router and documents intended settings.json usage. |
src/hooks/init.rs |
Implements Windows-native hook install flow (native_hook_command, patch_settings_native) and recognizes/removes hook claude entries. |
src/hooks/hook_cmd.rs |
Adds run_claude() wrapper that reuses existing Copilot/Claude-compatible hook JSON handling. |
| println!("\n MANUAL STEP: Add this to ~/.claude/settings.json:"); | ||
| println!(" {{"); | ||
| println!(" \"hooks\": {{ \"PreToolUse\": [{{"); | ||
| println!(" \"matcher\": \"Bash\","); | ||
| println!(" \"hooks\": [{{ \"type\": \"command\","); | ||
| println!(" \"command\": \"{}\"", hook_command); | ||
| println!(" }}]"); |
There was a problem hiding this comment.
In PatchMode::Skip, the manual settings.json snippet prints the hook_command inside JSON quotes, but hook_command itself contains embedded double-quotes ("") so the resulting example line is invalid JSON (unescaped quotes). Consider JSON-escaping the command when printing (e.g., serialize hook_command as a JSON string) or avoid embedding quotes in the displayed snippet.
| let claude_dir = resolve_claude_dir()?; | ||
| let rtk_md_path = claude_dir.join("RTK.md"); | ||
| let claude_md_path = claude_dir.join("CLAUDE.md"); | ||
|
|
||
| // 1. Write RTK.md | ||
| write_if_changed(&rtk_md_path, RTK_SLIM, "RTK.md", verbose)?; |
There was a problem hiding this comment.
Windows run_default_mode writes ~/.claude/RTK.md and ~/.claude/CLAUDE.md via write_if_changed/patch_claude_md, but unlike the unix path it never creates the ~/.claude directory first. If the user runs rtk init -g before Claude has created the folder, these writes will fail. Consider fs::create_dir_all(&claude_dir) right after resolve_claude_dir() (and/or ensure parents exist before each write).
src/hooks/init.rs
Outdated
| println!("\nRTK hook installed (Windows native).\n"); | ||
| println!(" Hook: {} hook claude", hook_command); | ||
| println!(" RTK.md: {} (10 lines)", rtk_md_path.display()); | ||
| println!(" CLAUDE.md: @RTK.md reference added"); | ||
|
|
||
| if migrated { | ||
| println!("\n [ok] Migrated: removed RTK block from CLAUDE.md, replaced with @RTK.md"); | ||
| } | ||
|
|
||
| match patch_result { | ||
| PatchResult::Patched => {} | ||
| PatchResult::AlreadyPresent => { | ||
| println!("\n settings.json: hook already present"); | ||
| println!(" Restart Claude Code. Test with: git status"); | ||
| } | ||
| PatchResult::Declined | PatchResult::Skipped => {} | ||
| } | ||
|
|
There was a problem hiding this comment.
This success banner is printed unconditionally, even when patch_settings_native returns Declined/Skipped (user opted out or manual mode). That can mislead users into thinking the hook is installed when settings.json wasn’t patched. Consider tailoring the message based on PatchResult (e.g., only say “installed” on Patched/AlreadyPresent, and otherwise say “manual step required”).
| println!("\nRTK hook installed (Windows native).\n"); | |
| println!(" Hook: {} hook claude", hook_command); | |
| println!(" RTK.md: {} (10 lines)", rtk_md_path.display()); | |
| println!(" CLAUDE.md: @RTK.md reference added"); | |
| if migrated { | |
| println!("\n [ok] Migrated: removed RTK block from CLAUDE.md, replaced with @RTK.md"); | |
| } | |
| match patch_result { | |
| PatchResult::Patched => {} | |
| PatchResult::AlreadyPresent => { | |
| println!("\n settings.json: hook already present"); | |
| println!(" Restart Claude Code. Test with: git status"); | |
| } | |
| PatchResult::Declined | PatchResult::Skipped => {} | |
| } | |
| match patch_result { | |
| PatchResult::Patched => { | |
| println!("\nRTK hook installed (Windows native).\n"); | |
| println!(" Hook: {} hook claude", hook_command); | |
| println!(" RTK.md: {} (10 lines)", rtk_md_path.display()); | |
| println!(" CLAUDE.md: @RTK.md reference added"); | |
| println!(" settings.json: hook added"); | |
| println!(" Restart Claude Code. Test with: git status"); | |
| } | |
| PatchResult::AlreadyPresent => { | |
| println!("\nRTK hook installed (Windows native).\n"); | |
| println!(" Hook: {} hook claude", hook_command); | |
| println!(" RTK.md: {} (10 lines)", rtk_md_path.display()); | |
| println!(" CLAUDE.md: @RTK.md reference added"); | |
| println!(" settings.json: hook already present"); | |
| println!(" Restart Claude Code. Test with: git status"); | |
| } | |
| PatchResult::Declined | PatchResult::Skipped => { | |
| println!("\nRTK hook not installed into settings.json (user opted out / manual mode).\n"); | |
| println!(" Hook command (for manual setup): {} hook claude", hook_command); | |
| println!(" RTK.md: {} (10 lines)", rtk_md_path.display()); | |
| println!(" CLAUDE.md: @RTK.md reference added"); | |
| println!(" Next step: update settings.json manually to call the hook command above."); | |
| } | |
| } | |
| if migrated { | |
| println!("\n [ok] Migrated: removed RTK block from CLAUDE.md, replaced with @RTK.md"); | |
| } |
| .any(|cmd| { | ||
| // Exact match OR both contain rtk-rewrite.sh | ||
| // Exact match OR both contain same hook fingerprint | ||
| cmd == hook_command | ||
| || (cmd.contains("rtk-rewrite.sh") && hook_command.contains("rtk-rewrite.sh")) | ||
| || (cmd.contains("hook claude") && hook_command.contains("hook claude")) | ||
| }) |
There was a problem hiding this comment.
hook_already_present now treats any command containing hook claude as an installed RTK hook, but the existing unit tests only cover the rtk-rewrite.sh fingerprint. Please add a test case for a native Windows command (e.g., "C:\\...\\rtk.exe" hook claude) to ensure idempotency works for the new hook path.
src/hooks/init.rs
Outdated
| if let Some(hooks_array) = entry.get("hooks").and_then(|h| h.as_array()) { | ||
| for hook in hooks_array { | ||
| if let Some(command) = hook.get("command").and_then(|c| c.as_str()) { | ||
| if command.contains("rtk-rewrite.sh") { | ||
| if command.contains("rtk-rewrite.sh") || command.contains("hook claude") { | ||
| return false; // Remove this entry |
There was a problem hiding this comment.
remove_hook_from_json now removes entries whose command contains hook claude, but the existing remove_hook_from_json tests only cover removal of rtk-rewrite.sh. Please add a test that verifies native hook commands are removed on uninstall without affecting other hooks.
b6abc40 to
e831a63
Compare
e831a63 to
02d9683
Compare
|
@copilot apply changes based on the comments in this thread |
|
Hello, i have a few things to solve on others PR then i can get to you for this review You can run all test on your machine and repush once solved because you got failing a check |
…red) Adds `rtk hook claude` subcommand that reads Claude Code PreToolUse JSON from stdin and rewrites commands natively — no bash or jq dependency. On Windows, `rtk init -g` now installs `rtk hook claude` directly in settings.json instead of printing a warning and falling back to --claude-md. Changes: - src/hooks/hook_cmd.rs: add run_claude() (wraps run_copilot(), same format) - src/main.rs: add HookCommands::Claude routing to run_claude() - src/hooks/init.rs: - native_hook_command(): resolves rtk binary path - patch_settings_native(): installs command string in settings.json - fs::create_dir_all before writing RTK.md/CLAUDE.md on Windows - JSON-escape hook_command in manual snippet to avoid invalid JSON - Conditional banner based on PatchResult (not unconditional) - hook_already_present/remove_hook_from_json recognize 'hook claude' - Tests: hook_already_present and remove_hook_from_json for native hook
e96e4a3 to
f631af6
Compare
|
Sorry @aeppling I didn't saw the failure last day. |
Problem
On Windows,
rtk init -gprints a warning and silently falls back to--claude-mdmode because the Claude Code hook was implemented as a bash script (rtk-rewrite.sh) requiringbash+jq— neither available natively on Windows.This means Windows users get no automatic command rewriting in Claude Code sessions.
Solution
Add
rtk hook claude— a native Rust subcommand that reads Claude Code'sPreToolUseJSON from stdin and rewrites commands, with zero external dependencies.The implementation reuses the existing
run_copilot()logic (which already handles Claude Code's JSON format:tool_name: "Bash"+tool_input.command).On Windows,
rtk init -gnow installs"<rtk.exe>" hook claudedirectly insettings.jsoninstead of falling back.Changes
src/hooks/hook_cmd.rspub fn run_claude()— wrapsrun_copilot(), same JSON formatsrc/main.rsHookCommands::Claudesubcommand routing torun_claude()src/hooks/init.rsnative_hook_command(),patch_settings_native(), Windowsrun_default_modeinstalls native hook,hook_already_present/remove_hook_from_jsonrecognize"hook claude"entriesBefore / After
Before (Windows):
After (Windows):
Relation to PR #150
PR #150 proposes a similar fix with a full standalone reimplementation (1126 lines). This PR takes a simpler approach: the existing
run_copilot()already handles Claude Code's hook format, sorun_claude()is a thin wrapper. The result is ~160 lines of changes vs ~1200, with no new dependencies and no structural refactor.Test plan
cargo checkpasses on Windows (0 errors)cargo test hooks— 124 tests, 0 failuresrtk hook claudesubcommand routes correctlyhook_already_presentrecognizes existing"hook claude"entries (idempotent)remove_hook_from_jsonremoves"hook claude"entries on uninstall🤖 Generated with Claude Code