From 857312c2a05dcf38b9182db1c668cac04d7759ff Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 1 Oct 2026 03:36:28 +0800 Subject: [PATCH] fix(core): install Gemini hooks under GEMINI_CLI_HOME when it is set Gemini CLI roots its user-level .gemini directory at GEMINI_CLI_HOME in place of the home directory. tty7 always wrote and checked ~/.gemini/settings.json, so for such a user the hooks it installed never ran, and the state it reported described a file Gemini does not read. Local-only, like the other config-dir overrides. --- crates/tty7-core/src/core/agent_hooks.rs | 43 +++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/tty7-core/src/core/agent_hooks.rs b/crates/tty7-core/src/core/agent_hooks.rs index 6c648dc5..28e80d25 100644 --- a/crates/tty7-core/src/core/agent_hooks.rs +++ b/crates/tty7-core/src/core/agent_hooks.rs @@ -722,7 +722,7 @@ impl HookAgent { HookAgent::OhMyPi => { target.under_home(&[".omp", "agent", "extensions", "tty7", "index.ts"]) } - HookAgent::Gemini => target.under_home(&[".gemini", "settings.json"]), + HookAgent::Gemini => target.gemini_settings_path(), HookAgent::Droid => target.under_home(&[".factory", "settings.json"]), HookAgent::Qwen => target.under_home(&[".qwen", "settings.json"]), // The Open Plugins layout, which Goose implements rather than @@ -835,6 +835,18 @@ impl<'a> HookTarget<'a> { self.under_home(&[".codex", "hooks.json"]) } + /// Gemini CLI roots its user-level `.gemini` directory, settings included, + /// at `GEMINI_CLI_HOME` in place of the user's home when that is set. + /// Local-only, like the other overrides. + fn gemini_settings_path(&self) -> PathBuf { + if self.is_local() + && let Some(dir) = std::env::var_os("GEMINI_CLI_HOME").filter(|d| !d.is_empty()) + { + return PathBuf::from(dir).join(".gemini").join("settings.json"); + } + self.under_home(&[".gemini", "settings.json"]) + } + fn xdg_config_dir(&self) -> PathBuf { if self.is_local() && let Some(dir) = std::env::var_os("XDG_CONFIG_HOME").filter(|d| !d.is_empty()) @@ -4243,6 +4255,35 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } + /// A Gemini CLI moved to `GEMINI_CLI_HOME` keeps its `.gemini` directory + /// there, and `~/.gemini/settings.json` is a file it never reads. + #[test] + fn gemini_hooks_live_under_gemini_cli_home_when_it_is_set() { + let dir = + std::env::temp_dir().join(format!("tty7-gemini-home-test-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + unsafe { std::env::set_var("GEMINI_CLI_HOME", &dir) }; + + let host = local_host(); + let t = HookTarget::local(&*host).expect("home resolves in tests"); + assert_eq!( + HookAgent::Gemini.target_path(&t), + dir.join(".gemini").join("settings.json") + ); + assert_eq!(hooks_state(&t, HookAgent::Gemini), HooksState::NotInstalled); + let remote_host = FakeRemote::shared(); + let remote = HookTarget::remote(&*remote_host, PathBuf::from("/home/me")); + assert_eq!( + HookAgent::Gemini.target_path(&remote), + PathBuf::from("/home/me/.gemini/settings.json"), + "a local GEMINI_CLI_HOME says nothing about a remote machine" + ); + + unsafe { std::env::remove_var("GEMINI_CLI_HOME") }; + let _ = std::fs::remove_dir_all(&dir); + } + /// Kimi's hooks share `config.toml` with the user's providers and models, /// so the merge must leave everything that is not ours — including /// comments and formatting — byte-for-byte alone.