From 469c0c0411acc2f60a4024e8c9a0f9d4d260490e Mon Sep 17 00:00:00 2001 From: Gert Burger Date: Fri, 6 Mar 2026 09:43:56 +0000 Subject: [PATCH 1/2] Add support for CLAUDE_CONFIG_DIR --- INSTALL.md | 2 + README.md | 2 + src/config.rs | 32 ++++++++++++++- src/discover/provider.rs | 3 +- src/hook_check.rs | 6 ++- src/init.rs | 87 +++++++++++++++++++++++++--------------- src/integrity.rs | 20 ++++----- src/main.rs | 2 +- 8 files changed, 104 insertions(+), 50 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 98457d09..31b6395c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -89,6 +89,8 @@ rtk gain # MUST show token savings, not "command not found" ### Recommended: Global Hook-First Setup +> **Custom config directory**: If you've set `CLAUDE_CONFIG_DIR`, RTK respects it — all paths below use that directory instead of `~/.claude`. + **Best for: All projects, automatic RTK usage** ```bash diff --git a/README.md b/README.md index 8158e1ce..13627352 100644 --- a/README.md +++ b/README.md @@ -544,6 +544,8 @@ The hook runs as a Claude Code [PreToolUse hook](https://docs.anthropic.com/en/d ### Quick Install (Automated) +> **Custom config directory**: If you've set `CLAUDE_CONFIG_DIR`, RTK respects it — all paths below use that directory instead of `~/.claude`. + ```bash rtk init -g # → Installs hook to ~/.claude/hooks/rtk-rewrite.sh (with executable permissions) diff --git a/src/config.rs b/src/config.rs index 94917a5e..398faa82 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,21 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +/// Resolve the Claude Code config directory. +/// +/// Mirrors Claude Code's own logic: +/// 1. `CLAUDE_CONFIG_DIR` env var (if set) +/// 2. Fallback: `~/.claude` +pub fn claude_config_dir() -> Result { + if let Ok(dir) = std::env::var("CLAUDE_CONFIG_DIR") { + return Ok(PathBuf::from(dir)); + } + dirs::home_dir() + .map(|h| h.join(".claude")) + .context("Cannot determine home directory. Is $HOME set?") +} + #[derive(Debug, Serialize, Deserialize, Default)] pub struct Config { #[serde(default)] @@ -184,4 +198,20 @@ history_days = 90 let config: Config = toml::from_str(toml).expect("valid toml"); assert!(config.hooks.exclude_commands.is_empty()); } + + #[test] + fn test_claude_config_dir_env_override() { + std::env::set_var("CLAUDE_CONFIG_DIR", "/tmp/custom-claude"); + let dir = claude_config_dir().unwrap(); + std::env::remove_var("CLAUDE_CONFIG_DIR"); + assert_eq!(dir, PathBuf::from("/tmp/custom-claude")); + } + + #[test] + fn test_claude_config_dir_default() { + std::env::remove_var("CLAUDE_CONFIG_DIR"); + let dir = claude_config_dir().unwrap(); + let home = dirs::home_dir().unwrap(); + assert_eq!(dir, home.join(".claude")); + } } diff --git a/src/discover/provider.rs b/src/discover/provider.rs index e9218b2d..850507b7 100644 --- a/src/discover/provider.rs +++ b/src/discover/provider.rs @@ -36,8 +36,7 @@ pub struct ClaudeProvider; impl ClaudeProvider { /// Get the base directory for Claude Code projects. fn projects_dir() -> Result { - let home = dirs::home_dir().context("could not determine home directory")?; - let dir = home.join(".claude").join("projects"); + let dir = crate::config::claude_config_dir()?.join("projects"); if !dir.exists() { anyhow::bail!( "Claude Code projects directory not found: {}\nMake sure Claude Code has been used at least once.", diff --git a/src/hook_check.rs b/src/hook_check.rs index 969128d3..45da94fa 100644 --- a/src/hook_check.rs +++ b/src/hook_check.rs @@ -50,8 +50,10 @@ pub fn parse_hook_version(content: &str) -> u8 { } fn hook_installed_path() -> Option { - let home = dirs::home_dir()?; - let path = home.join(".claude").join("hooks").join("rtk-rewrite.sh"); + let path = crate::config::claude_config_dir() + .ok()? + .join("hooks") + .join("rtk-rewrite.sh"); if path.exists() { Some(path) } else { diff --git a/src/init.rs b/src/init.rs index 3ff2622b..d0c48b75 100644 --- a/src/init.rs +++ b/src/init.rs @@ -145,7 +145,7 @@ rtk gain --history # View command history with savings rtk discover # Analyze Claude Code sessions for missed RTK usage rtk proxy # Run command without filtering (for debugging) rtk init # Add RTK instructions to CLAUDE.md -rtk init --global # Add RTK to ~/.claude/CLAUDE.md +rtk init --global # Add RTK to global CLAUDE.md ``` ## Token Savings Overview @@ -183,7 +183,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()?; + let claude_dir = crate::config::claude_config_dir()?; let hook_dir = claude_dir.join("hooks"); fs::create_dir_all(&hook_dir) .with_context(|| format!("Failed to create hook directory: {}", hook_dir.display()))?; @@ -322,8 +322,8 @@ fn prompt_user_consent(settings_path: &Path) -> Result { } /// Print manual instructions for settings.json patching -fn print_manual_instructions(hook_path: &Path) { - println!("\n MANUAL STEP: Add this to ~/.claude/settings.json:"); +fn print_manual_instructions(hook_path: &Path, settings_path: &Path) { + println!("\n MANUAL STEP: Add this to {}:", settings_path.display()); println!(" {{"); println!(" \"hooks\": {{ \"PreToolUse\": [{{"); println!(" \"matcher\": \"Bash\","); @@ -369,7 +369,7 @@ fn remove_hook_from_json(root: &mut serde_json::Value) -> bool { /// Remove RTK hook from settings.json file /// Backs up before modification, returns true if hook was found and removed fn remove_hook_from_settings(verbose: u8) -> Result { - let claude_dir = resolve_claude_dir()?; + let claude_dir = crate::config::claude_config_dir()?; let settings_path = claude_dir.join("settings.json"); if !settings_path.exists() { @@ -416,7 +416,7 @@ pub fn uninstall(global: bool, verbose: u8) -> Result<()> { anyhow::bail!("Uninstall only works with --global flag. For local projects, manually remove RTK from CLAUDE.md"); } - let claude_dir = resolve_claude_dir()?; + let claude_dir = crate::config::claude_config_dir()?; let mut removed = Vec::new(); // 1. Remove hook file @@ -485,7 +485,7 @@ pub fn uninstall(global: bool, verbose: u8) -> Result<()> { /// Orchestrator: patch settings.json with RTK hook /// Handles reading, checking, prompting, merging, backing up, and atomic writing fn patch_settings_json(hook_path: &Path, mode: PatchMode, verbose: u8) -> Result { - let claude_dir = resolve_claude_dir()?; + let claude_dir = crate::config::claude_config_dir()?; let settings_path = claude_dir.join("settings.json"); let hook_command = hook_path .to_str() @@ -517,12 +517,12 @@ fn patch_settings_json(hook_path: &Path, mode: PatchMode, verbose: u8) -> Result // Handle mode match mode { PatchMode::Skip => { - print_manual_instructions(hook_path); + print_manual_instructions(hook_path, &settings_path); return Ok(PatchResult::Skipped); } PatchMode::Ask => { if !prompt_user_consent(&settings_path)? { - print_manual_instructions(hook_path); + print_manual_instructions(hook_path, &settings_path); return Ok(PatchResult::Declined); } } @@ -670,7 +670,7 @@ fn run_default_mode(global: bool, patch_mode: PatchMode, verbose: u8) -> Result< return run_claude_md_mode(false, verbose); } - let claude_dir = resolve_claude_dir()?; + let claude_dir = crate::config::claude_config_dir()?; let rtk_md_path = claude_dir.join("RTK.md"); let claude_md_path = claude_dir.join("CLAUDE.md"); @@ -776,7 +776,7 @@ fn run_hook_only_mode(global: bool, patch_mode: PatchMode, verbose: u8) -> Resul /// Legacy mode: full 137-line injection into CLAUDE.md fn run_claude_md_mode(global: bool, verbose: u8) -> Result<()> { let path = if global { - resolve_claude_dir()?.join("CLAUDE.md") + crate::config::claude_config_dir()?.join("CLAUDE.md") } else { PathBuf::from("CLAUDE.md") }; @@ -999,16 +999,9 @@ fn remove_rtk_block(content: &str) -> (String, bool) { } } -/// Resolve ~/.claude directory with proper home expansion -fn resolve_claude_dir() -> Result { - dirs::home_dir() - .map(|h| h.join(".claude")) - .context("Cannot determine home directory. Is $HOME set?") -} - /// Show current rtk configuration pub fn show_config() -> Result<()> { - let claude_dir = resolve_claude_dir()?; + let claude_dir = crate::config::claude_config_dir()?; let hook_path = claude_dir.join("hooks").join("rtk-rewrite.sh"); let rtk_md_path = claude_dir.join("RTK.md"); let global_claude_md = claude_dir.join("CLAUDE.md"); @@ -1016,6 +1009,17 @@ pub fn show_config() -> Result<()> { println!("📋 rtk Configuration:\n"); + if !claude_dir.exists() { + println!( + "⚠️ Config directory does not exist: {}", + claude_dir.display() + ); + if std::env::var("CLAUDE_CONFIG_DIR").is_ok() { + println!(" (set via CLAUDE_CONFIG_DIR)"); + } + println!(); + } + // Check hook if hook_path.exists() { #[cfg(unix)] @@ -1060,14 +1064,14 @@ pub fn show_config() -> Result<()> { println!("✅ Hook: {} (exists)", hook_path.display()); } } else { - println!("⚪ Hook: not found"); + println!("⚪ Hook ({}): not found", hook_path.display()); } // Check RTK.md if rtk_md_path.exists() { println!("✅ RTK.md: {} (slim mode)", rtk_md_path.display()); } else { - println!("⚪ RTK.md: not found"); + println!("⚪ RTK.md ({}): not found", rtk_md_path.display()); } // Check hook integrity @@ -1094,16 +1098,23 @@ pub fn show_config() -> Result<()> { if global_claude_md.exists() { let content = fs::read_to_string(&global_claude_md)?; if content.contains("@RTK.md") { - println!("✅ Global (~/.claude/CLAUDE.md): @RTK.md reference"); + println!( + "✅ Global ({}): @RTK.md reference", + global_claude_md.display() + ); } else if content.contains("