Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ No. Git AI does not use Git hooks and it does not wrap Git, so you won't see any
#### Do I have to set up agent hooks?
Nope — Git AI manages the agent hooks and checks/updates them daily. If you want to trigger this yourself (ie just installed a new agent) run `git ai install-hooks`.

#### Can I install the agent hooks without the IDE extensions?
Yes. Run `git ai install-hooks --no-editor-extensions` to skip the IDE extension/plugin installs (VS Code, Cursor, Windsurf, JetBrains) while still installing the agent hooks. To make this permanent — including for the daily hook refresh and updates — set `git ai config set disable_editor_extensions true`.

#### Who uses this?
Hundreds of engineering teams (including many in the Fortune 100) use Git AI to understand their AI usage and make agents more effective on their codebase.

Expand Down
21 changes: 21 additions & 0 deletions src/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ fn print_config_help() {
println!(" telemetry_enterprise_dsn Enterprise telemetry DSN");
println!(" disable_version_checks Disable version checks (bool)");
println!(" disable_auto_updates Disable auto updates (bool)");
println!(
" disable_editor_extensions Skip IDE extension/plugin installs during install-hooks (bool)"
);
println!(" update_channel Update channel (latest/next)");
println!(" feature_flags Feature flags (object)");
println!(" api_base_url API base URL (default: https://usegitai.com)");
Expand Down Expand Up @@ -303,6 +306,10 @@ fn show_all_config() -> Result<(), String> {
"disable_auto_updates".to_string(),
Value::Bool(runtime_config.auto_updates_disabled()),
);
effective_config.insert(
"disable_editor_extensions".to_string(),
Value::Bool(runtime_config.editor_extensions_disabled()),
);

// Optional strings
if let Some(ref dsn) = file_config.telemetry_enterprise_dsn {
Expand Down Expand Up @@ -459,6 +466,7 @@ fn get_config_value(key: &str) -> Result<(), String> {
}
"disable_version_checks" => Value::Bool(runtime_config.version_checks_disabled()),
"disable_auto_updates" => Value::Bool(runtime_config.auto_updates_disabled()),
"disable_editor_extensions" => Value::Bool(runtime_config.editor_extensions_disabled()),
"update_channel" => Value::String(runtime_config.update_channel().as_str().to_string()),
"feature_flags" => {
// Show effective flags with defaults applied
Expand Down Expand Up @@ -682,6 +690,12 @@ fn set_config_value(key: &str, value: &str, add_mode: bool) -> Result<(), String
crate::config::save_file_config(&file_config)?;
println!("[disable_auto_updates]: {}", bool_value);
}
"disable_editor_extensions" => {
let bool_value = parse_bool(value)?;
file_config.disable_editor_extensions = Some(bool_value);
crate::config::save_file_config(&file_config)?;
println!("[disable_editor_extensions]: {}", bool_value);
}
"update_channel" => {
// Validate update channel
if value != "latest" && value != "next" {
Expand Down Expand Up @@ -1051,6 +1065,13 @@ fn unset_config_value(key: &str) -> Result<(), String> {
println!("- [disable_auto_updates]: {}", v);
}
}
"disable_editor_extensions" => {
let old_value = file_config.disable_editor_extensions.take();
crate::config::save_file_config(&file_config)?;
if let Some(v) = old_value {
println!("- [disable_editor_extensions]: {}", v);
}
}
"update_channel" => {
let old_value = file_config.update_channel.take();
crate::config::save_file_config(&file_config)?;
Expand Down
4 changes: 4 additions & 0 deletions src/commands/git_ai_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,10 @@ fn print_help() {
eprintln!(" --skills Also install agent skill files");
eprintln!(" --visual-studio-extension");
eprintln!(" Also install the Visual Studio extension on Windows");
eprintln!(" --no-editor-extensions");
eprintln!(
" Skip IDE extension/plugin installs (agent hooks still installed)"
);
eprintln!(" uninstall-hooks Remove git-ai hooks from all detected tools");
eprintln!(" ci Continuous integration utilities");
eprintln!(" github GitHub CI helpers");
Expand Down
34 changes: 31 additions & 3 deletions src/commands/install_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ struct InstallOptions {
verbose: bool,
install_skills: bool,
include_visual_studio_extension: bool,
skip_editor_extensions: bool,
}

/// Installation status for a tool
Expand Down Expand Up @@ -303,7 +304,14 @@ fn ensure_daemon(dry_run: bool) {

/// Main entry point for install-hooks command
pub fn run(args: &[String]) -> Result<HashMap<String, String>, GitAiError> {
let options = parse_install_options(args);
let mut options = parse_install_options(args);

// The daemon and the install script re-run install-hooks without flags
// (e.g. the daily hooks refresh and auto-updates), so the opt-out must
// also be honored when persisted in the config file.
if !options.skip_editor_extensions {
options.skip_editor_extensions = config::Config::fresh().editor_extensions_disabled();
}

// Daemon trace2 config must be in place before any install work starts.
// Non-fatal: the global git config may be read-only (e.g. Nix store symlink).
Expand All @@ -321,7 +329,10 @@ pub fn run(args: &[String]) -> Result<HashMap<String, String>, GitAiError> {
// Get absolute path to the current binary
let binary_path = get_current_binary_path()?;
persist_install_config(&binary_path, options.dry_run)?;
let params = HookInstallerParams { binary_path };
let params = HookInstallerParams {
binary_path,
skip_editor_extensions: options.skip_editor_extensions,
};

// Run async operations and convert result.
let statuses = crate::tokio_runtime::block_on(async_run_install(&params, &options))?;
Expand All @@ -344,6 +355,7 @@ fn parse_install_options(args: &[String]) -> InstallOptions {
"--verbose" | "-v" => options.verbose = true,
"--skills" => options.install_skills = true,
"--visual-studio-extension" => options.include_visual_studio_extension = true,
"--no-editor-extensions" => options.skip_editor_extensions = true,
_ => {}
}
}
Expand Down Expand Up @@ -450,7 +462,10 @@ pub fn run_uninstall(args: &[String]) -> Result<HashMap<String, String>, GitAiEr

// Get absolute path to the current binary
let binary_path = get_current_binary_path()?;
let params = HookInstallerParams { binary_path };
let params = HookInstallerParams {
binary_path,
skip_editor_extensions: false,
};

// Run async operations and convert result.
let statuses = crate::tokio_runtime::block_on(async_run_uninstall(&params, dry_run, verbose))?;
Expand Down Expand Up @@ -1036,6 +1051,19 @@ mod tests {
));
}

#[test]
fn parse_install_options_defaults_editor_extensions_to_enabled() {
let options = parse_install_options(&[]);
assert!(!options.skip_editor_extensions);
}

#[test]
fn parse_install_options_enables_no_editor_extensions_flag() {
let args = vec!["--no-editor-extensions".to_string()];
let options = parse_install_options(&args);
assert!(options.skip_editor_extensions);
}

#[test]
#[serial]
fn persist_install_config_updates_api_base_and_backfills_git_path() {
Expand Down
19 changes: 19 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ pub struct Config {
telemetry_enterprise_dsn: Option<String>,
disable_version_checks: bool,
disable_auto_updates: bool,
disable_editor_extensions: bool,
update_channel: UpdateChannel,
feature_flags: FeatureFlags,
api_base_url: String,
Expand Down Expand Up @@ -235,6 +236,8 @@ pub struct FileConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_auto_updates: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_editor_extensions: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub update_channel: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feature_flags: Option<serde_json::Value>,
Expand Down Expand Up @@ -513,6 +516,12 @@ impl Config {
self.disable_auto_updates
}

/// Returns true if IDE extension/plugin installation should be skipped
/// during `install-hooks` (agent hooks are still installed).
pub fn editor_extensions_disabled(&self) -> bool {
self.disable_editor_extensions
}

pub fn update_channel(&self) -> UpdateChannel {
self.update_channel
}
Expand Down Expand Up @@ -1001,6 +1010,11 @@ fn build_config() -> Config {
.as_ref()
.and_then(|c| c.disable_auto_updates)
.unwrap_or(auto_update_flags_default_disabled);
// Editor extensions install by default; opting out never blocks agent hooks.
let disable_editor_extensions = file_cfg
.as_ref()
.and_then(|c| c.disable_editor_extensions)
.unwrap_or(false);
let update_channel = file_cfg
.as_ref()
.and_then(|c| c.update_channel.as_deref())
Expand Down Expand Up @@ -1163,6 +1177,7 @@ fn build_config() -> Config {
telemetry_enterprise_dsn,
disable_version_checks,
disable_auto_updates,
disable_editor_extensions,
update_channel,
feature_flags,
api_base_url,
Expand Down Expand Up @@ -1193,6 +1208,7 @@ fn build_config() -> Config {
telemetry_enterprise_dsn,
disable_version_checks,
disable_auto_updates,
disable_editor_extensions,
update_channel,
feature_flags,
api_base_url,
Expand Down Expand Up @@ -1680,6 +1696,7 @@ mod tests {
telemetry_enterprise_dsn: None,
disable_version_checks: false,
disable_auto_updates: false,
disable_editor_extensions: false,
update_channel: UpdateChannel::Latest,
feature_flags: FeatureFlags::default(),
api_base_url: DEFAULT_API_BASE_URL.to_string(),
Expand Down Expand Up @@ -1922,6 +1939,7 @@ mod tests {
telemetry_enterprise_dsn: None,
disable_version_checks: false,
disable_auto_updates: false,
disable_editor_extensions: false,
update_channel: UpdateChannel::Latest,
feature_flags: FeatureFlags::default(),
api_base_url: DEFAULT_API_BASE_URL.to_string(),
Expand Down Expand Up @@ -2067,6 +2085,7 @@ mod tests {
telemetry_enterprise_dsn: None,
disable_version_checks: false,
disable_auto_updates: false,
disable_editor_extensions: false,
update_channel: UpdateChannel::Latest,
feature_flags: FeatureFlags::default(),
api_base_url: DEFAULT_API_BASE_URL.to_string(),
Expand Down
1 change: 1 addition & 0 deletions src/mdm/agents/claude_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ mod tests {
fn params() -> HookInstallerParams {
HookInstallerParams {
binary_path: binary_path(),
skip_editor_extensions: false,
}
}

Expand Down
19 changes: 19 additions & 0 deletions src/mdm/agents/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,7 @@ codex_hooks = true
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

let diff = installer
Expand Down Expand Up @@ -1458,6 +1459,7 @@ codex_hooks = true
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

let diff = installer
Expand Down Expand Up @@ -1517,6 +1519,7 @@ notify = ["/usr/local/bin/git-ai", "checkpoint", "codex", "--hook-input"]
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

installer
Expand Down Expand Up @@ -1563,6 +1566,7 @@ notify = ["/Users/svarlamov/.git-ai/bin/git-ai", "checkpoint", "codex", "--via-c
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

installer
Expand Down Expand Up @@ -1597,6 +1601,7 @@ notify = ["notify-send", "Codex finished"]
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

installer
Expand Down Expand Up @@ -1653,6 +1658,7 @@ notify = ["notify-send", "Codex finished"]
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

let diff = installer
Expand Down Expand Up @@ -1680,6 +1686,7 @@ notify = ["notify-send", "Codex finished"]
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

let first = installer
Expand Down Expand Up @@ -1753,6 +1760,7 @@ codex_hooks = true
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

installer
Expand Down Expand Up @@ -1818,6 +1826,7 @@ codex_hooks = true
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

installer
Expand Down Expand Up @@ -1888,6 +1897,7 @@ codex_hooks = true
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

installer
Expand Down Expand Up @@ -1981,6 +1991,7 @@ command = "/usr/local/bin/git-ai checkpoint codex --hook-input stdin"
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

installer
Expand Down Expand Up @@ -2067,6 +2078,7 @@ command = "/usr/local/bin/git-ai checkpoint codex --hook-input stdin"
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

let diff = installer
Expand Down Expand Up @@ -2126,6 +2138,7 @@ codex_hooks = true
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

let diff = installer
Expand Down Expand Up @@ -2157,6 +2170,7 @@ codex_hooks = true
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

let result = installer.install_hooks(&params, false).unwrap();
Expand Down Expand Up @@ -2215,6 +2229,7 @@ codex_hooks = true
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

let check = installer
Expand Down Expand Up @@ -2310,6 +2325,7 @@ codex_hooks = true
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

installer
Expand Down Expand Up @@ -2358,6 +2374,7 @@ codex_hooks = true
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

installer
Expand Down Expand Up @@ -2411,6 +2428,7 @@ trusted_hash = "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef123
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

installer
Expand Down Expand Up @@ -2452,6 +2470,7 @@ trusted_hash = "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef123
let installer = CodexInstaller;
let params = HookInstallerParams {
binary_path: test_binary_path(),
skip_editor_extensions: false,
};

installer
Expand Down
Loading