From 62f2c4c59a8bfdb18a0b890855526e707865996a Mon Sep 17 00:00:00 2001 From: Andre <10810328+myx0m0p@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:39:23 +0700 Subject: [PATCH] fix(init): allow --hook-only for local (project-scoped) hook installation Previously `rtk init --hook-only` rejected non-global mode with a warning. Now it installs the hook to `.claude/hooks/rtk-rewrite.sh` and patches `.claude/settings.json` with the PreToolUse hook entry, enabling project-level RTK setup without modifying CLAUDE.md. Closes #1069 Co-Authored-By: Claude Opus 4.6 --- src/hooks/init.rs | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/hooks/init.rs b/src/hooks/init.rs index c65465962..959391fcd 100644 --- a/src/hooks/init.rs +++ b/src/hooks/init.rs @@ -297,8 +297,7 @@ pub fn run( } /// Prepare hook directory and return paths (hook_dir, hook_path) -fn prepare_hook_paths() -> Result<(PathBuf, PathBuf)> { - let claude_dir = resolve_claude_dir()?; +fn prepare_hook_paths(claude_dir: &Path) -> Result<(PathBuf, PathBuf)> { let hook_dir = claude_dir.join("hooks"); fs::create_dir_all(&hook_dir) .with_context(|| format!("Failed to create hook directory: {}", hook_dir.display()))?; @@ -437,8 +436,8 @@ fn prompt_user_consent(settings_path: &Path) -> Result { } /// Print manual instructions for settings.json patching -fn print_manual_instructions(hook_path: &Path, include_opencode: bool) { - println!("\n MANUAL STEP: Add this to ~/.claude/settings.json:"); +fn print_manual_instructions(hook_path: &Path, settings_path: &Path, include_opencode: bool) { + println!("\n MANUAL STEP: Add this to {}:", settings_path.display()); println!(" {{"); println!(" \"hooks\": {{ \"PreToolUse\": [{{"); println!(" \"matcher\": \"Bash\","); @@ -696,11 +695,11 @@ fn uninstall_codex_at(codex_dir: &Path, verbose: u8) -> Result> { /// Handles reading, checking, prompting, merging, backing up, and atomic writing fn patch_settings_json( hook_path: &Path, + claude_dir: &Path, mode: PatchMode, verbose: u8, include_opencode: bool, ) -> Result { - let claude_dir = resolve_claude_dir()?; let settings_path = claude_dir.join(SETTINGS_JSON); let hook_command = hook_path .to_str() @@ -732,12 +731,12 @@ fn patch_settings_json( // Handle mode match mode { PatchMode::Skip => { - print_manual_instructions(hook_path, include_opencode); + print_manual_instructions(hook_path, &settings_path, include_opencode); return Ok(PatchResult::Skipped); } PatchMode::Ask => { if !prompt_user_consent(&settings_path)? { - print_manual_instructions(hook_path, include_opencode); + print_manual_instructions(hook_path, &settings_path, include_opencode); return Ok(PatchResult::Declined); } } @@ -902,7 +901,7 @@ fn run_default_mode( let claude_md_path = claude_dir.join(CLAUDE_MD); // 1. Prepare hook directory and install hook - let (_hook_dir, hook_path) = prepare_hook_paths()?; + let (_hook_dir, hook_path) = prepare_hook_paths(&claude_dir)?; let hook_changed = ensure_hook_installed(&hook_path, verbose)?; // 2. Write RTK.md @@ -939,7 +938,13 @@ fn run_default_mode( } // 5. Patch settings.json - let patch_result = patch_settings_json(&hook_path, patch_mode, verbose, install_opencode)?; + let patch_result = patch_settings_json( + &hook_path, + &claude_dir, + patch_mode, + verbose, + install_opencode, + )?; // Report result match patch_result { @@ -1034,14 +1039,16 @@ fn run_hook_only_mode( verbose: u8, install_opencode: bool, ) -> Result<()> { - if !global { - eprintln!("[warn] Warning: --hook-only only makes sense with --global"); - eprintln!(" For local projects, use default mode or --claude-md"); - return Ok(()); - } + let claude_dir = if global { + resolve_claude_dir()? + } else { + std::env::current_dir()?.join(".claude") + }; + + let scope = if global { "global" } else { "project-scoped" }; // Prepare and install hook - let (_hook_dir, hook_path) = prepare_hook_paths()?; + let (_hook_dir, hook_path) = prepare_hook_paths(&claude_dir)?; let hook_changed = ensure_hook_installed(&hook_path, verbose)?; let opencode_plugin_path = if install_opencode { @@ -1057,7 +1064,7 @@ fn run_hook_only_mode( } else { "already up to date" }; - println!("\nRTK hook {} (hook-only mode).\n", hook_status); + println!("\nRTK hook {} (hook-only mode, {}).\n", hook_status, scope); println!(" Hook: {}", hook_path.display()); if let Some(path) = &opencode_plugin_path { println!(" OpenCode: {}", path.display()); @@ -1067,7 +1074,13 @@ fn run_hook_only_mode( ); // Patch settings.json - let patch_result = patch_settings_json(&hook_path, patch_mode, verbose, install_opencode)?; + let patch_result = patch_settings_json( + &hook_path, + &claude_dir, + patch_mode, + verbose, + install_opencode, + )?; // Report result match patch_result {