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.
This commit is contained in:
l0ng-ai
2026-10-01 15:28:00 +08:00
parent cdc3b47914
commit 857312c2a0
+42 -1
View File
@@ -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.