Skip to content

Preserve default hooks forwarding in wrapper mode#771

Draft
svarlamov wants to merge 2 commits intomainfrom
codex/fix-wrapper-default-hooks-forwarding
Draft

Preserve default hooks forwarding in wrapper mode#771
svarlamov wants to merge 2 commits intomainfrom
codex/fix-wrapper-default-hooks-forwarding

Conversation

@svarlamov
Copy link
Member

@svarlamov svarlamov commented Mar 22, 2026

Summary

  • preserve the repo default .git/hooks directory as a forward target when git-ai hook state exists without an explicit core.hooksPath
  • fall back existing forward_mode: none repos to the default hooks dir in wrapper-managed child git invocations
  • add a regression test covering wrapped post-checkout forwarding in both normal repos and worktrees

Testing

  • cargo test --test integration wrapper_mode_preserves_default_post_checkout_hook_after_ensure -- --nocapture
  • cargo test --test integration hook_forwarding -- --nocapture
  • cargo test --test integration hook_modes -- --nocapture

Open with Devin

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +866 to +872
if let Some(default_hooks_dir) = default_forward_hooks_path(repo, managed_hooks_dir) {
return (
ForwardMode::RepoLocal,
Some(default_hooks_dir.to_string_lossy().to_string()),
None,
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 default_forward_hooks_path always returns Some, making ForwardMode::None preservation dead code and dropping original_local_hooks_path

default_forward_hooks_path (git_hook_handlers.rs:780-786) always returns Some because .git/hooks can never be classified as disallowed by is_disallowed_forward_hooks_path — it's not inside .git-ai, not inside .git/ai, and not equal to the managed dir .git/ai/hooks. This means the ForwardMode::None preservation block at lines 874-882 is unreachable dead code.

The practical consequence: when prior state had ForwardMode::None with a saved original_local_hooks_path (set during externally-managed mode at git_hook_handlers.rs:818), the default hooks dir fallback at lines 866-872 always fires first and returns original_local_hooks_path: None. This silently drops the saved path. Later, remove_repo_hooks (git_hook_handlers.rs:989-998) uses original_local_hooks_path to restore the user's original core.hooksPath — if it's been dropped to None, the config is unset instead of restored.

Scenario that triggers the bug
  1. User has core.hooksPath = /custom/hooks
  2. git-ai installs hooks (sets core.hooksPath to managed dir, saves original path)
  3. git_hooks_externally_managed flag is enabled → state saved with ForwardMode::None + original_local_hooks_path
  4. Flag is disabled, ensure runs → default hooks fallback always fires → original_local_hooks_path lost
  5. git-hooks remove → can't restore /custom/hooks, unsets config instead
Prompt for agents
In src/commands/git_hook_handlers.rs, the default_forward_hooks_path fallback at lines 866-872 in select_forward_target_for_repo always returns Some, which means the ForwardMode::None block at lines 874-882 is dead code and original_local_hooks_path from prior state is silently dropped.

To fix this, the default hooks dir fallback should preserve original_local_hooks_path from prior state. Change lines 866-872 from:

    if let Some(default_hooks_dir) = default_forward_hooks_path(repo, managed_hooks_dir) {
        return (
            ForwardMode::RepoLocal,
            Some(default_hooks_dir.to_string_lossy().to_string()),
            None,
        );
    }

To something like:

    if let Some(default_hooks_dir) = default_forward_hooks_path(repo, managed_hooks_dir) {
        let preserved_original = prior_state
            .and_then(|state| state.original_local_hooks_path.clone());
        return (
            ForwardMode::RepoLocal,
            Some(default_hooks_dir.to_string_lossy().to_string()),
            preserved_original,
        );
    }

This ensures that when falling back to the default hooks directory, any previously saved original_local_hooks_path is carried forward so that git-hooks remove can still restore the user's prior hooksPath.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@svarlamov svarlamov marked this pull request as draft March 22, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant