gitbutlerapp/gitbutler

`but setup` displaces hooks owned by hook managers (prek, pre-commit, husky) without detecting them, and provides no escape hatch

Open

#12,748 opened on 2026年3月10日

GitHub で見る
 (20 comments) (3 reactions) (0 assignees)Rust (19,787 stars) (862 forks)batch import
bughelp wanted

説明

Version

0.19.5

Operating System

macOS

Distribution Method

dmg (Mac OS - Apple Silicon)

Describe the issue

Summary

but setup unconditionally overwrites pre-commit and post-checkout hooks without checking whether a hook manager (prek, pre-commit, husky, lefthook, etc.) already owns them. It renames existing hooks with a -user suffix and installs GITBUTLER_MANAGED_HOOK_V1 wrappers in their place. While but setup does print a note that it is "Installing Git hooks", it does not name the affected files, does not warn that existing hooks will be displaced, and does not detect pre-existing hook managers.

The resulting state is fragile in several distinct ways.

Failure modes

1. Insufficient warning on but setup

but setup does print a note:

Installing Git hooks to help manage commits on the workspace branch

However it does not:

  • Name which hook files will be affected (pre-commit, post-checkout)
  • Mention that existing hooks will be renamed to a -user suffix
  • Detect whether a hook manager (prek, pre-commit, husky) is already installed and owns those files
  • Offer a way to skip hook installation (--no-hooks) for projects that manage hooks independently

A developer running but setup in a project where prek already owns pre-commit gets no indication that their hook runner has just been demoted to pre-commit-user and wrapped by a GitButler shim.

2. Inconsistent coverage — pre-push is left untouched

GitButler wraps pre-commit and post-checkout, but ignores pre-push entirely. This means pre-push hooks continue running through the hook manager's full pipeline (including stash-before-hooks). When combined with a stale gitbutler/workspace HEAD commit, this causes linters to run against stale file content — see the stale-workspace-HEAD issue filed separately (#12750).

3. prek install after but setup silently breaks workspace guard

Running prek install a second time (e.g., after git clone on a new machine following the project's README) produces:

Hook already exists at `.git/hooks/pre-commit`, moved it to `.git/hooks/pre-commit.legacy`
Migration mode: prek will also run legacy hook `.git/hooks/pre-commit.legacy`
prek installed at `.git/hooks/pre-commit`

Result:

pre-commit         → fresh prek runner
pre-commit.legacy  → GitButler wrapper  ← prek calls this in migration mode, which calls...
pre-commit-user    → old prek runner    ← ...this orphaned duplicate: prek runs TWICE
post-checkout      → fresh prek runner  ← GitButler cleanup logic GONE
post-checkout-user → prek runner (orphaned)

In prek's default migration mode, the GitButler workspace guard still runs (via .legacy), but prek is invoked twice — once directly, once via the wrapper calling pre-commit-user. Running prek install --overwrite removes .legacy entirely, at which point the workspace guard is silently gone with no error or warning.

4. No escape hatch

There is no but setup --no-hooks flag, no config option to disable hook management, and no way to tell GitButler "I already manage hooks through prek — provide your logic as a hook I can call."

Environment

  • GitButler CLI: but (latest)
  • OS: macOS 15 / macOS 26 beta
  • Hook manager tested: prek v0.3+ (pre-commit compatible)
  • Also affects: pre-commit (uses .git/hooks/), Husky (both .git/hooks/ for v4 and .husky/ for v5+ — same displacement pattern regardless of core.hooksPath)

How to reproduce (Optional)

Reproduction

With prek (hooks in .git/hooks/)

# Fresh repo with prek already managing hooks
git init /tmp/hook-test
git commit --allow-empty -m "init"
cat > /tmp/hook-test/prek.toml << 'EOF'
minimum_prek_version = "0.3.3"
default_install_hook_types = ["pre-commit", "pre-push", "post-checkout"]
[[repos]]
repo = "builtin"
hooks = [{ id = "trailing-whitespace" }]
EOF
prek install -C /tmp/hook-test

Hooks before but setup:

.git/hooks/pre-commit    → prek runner
.git/hooks/pre-push      → prek runner
.git/hooks/post-checkout → prek runner
but setup   # run in /tmp/hook-test

Hooks after but setup:

.git/hooks/pre-commit         → GITBUTLER_MANAGED_HOOK_V1 wrapper  ← replaced, no file-level warning
.git/hooks/pre-commit-user    → prek runner (demoted, non-standard filename)
.git/hooks/pre-push           → prek runner                         ← untouched (inconsistent!)
.git/hooks/post-checkout      → GITBUTLER_MANAGED_HOOK_V1 wrapper  ← replaced, no file-level warning
.git/hooks/post-checkout-user → prek runner (demoted, non-standard filename)

With Husky (hooks in .husky/, core.hooksPath = .husky)

but setup is aware of core.hooksPath — when set to .husky, it correctly installs its wrappers into .husky/ rather than .git/hooks/. However, the same displacement pattern applies:

# Fresh repo with Husky-style hooks
git init /tmp/husky-test && git commit --allow-empty -m "init"
mkdir /tmp/husky-test/.husky
echo '#!/bin/sh\necho "husky: running pre-commit"' > /tmp/husky-test/.husky/pre-commit
chmod +x /tmp/husky-test/.husky/pre-commit
git -C /tmp/husky-test config core.hooksPath .husky
but setup   # run in /tmp/husky-test

Result — .husky/ after but setup:

.husky/pre-commit      → GITBUTLER_MANAGED_HOOK_V1 wrapper  ← original displaced
.husky/pre-commit-user → original Husky hook (50 bytes)
.husky/post-checkout   → GitButler post-checkout handler    ← injected
.git/hooks/            → empty (correctly unaffected)

The GitButler GUI setting "Enable Husky hooks" is orthogonal to this — it controls whether GitButler's internal hook execution calls .husky/ scripts, not how but setup behaves.

Expected behavior (Optional)

Expected behaviour

but setup should:

  1. Detect existing hook managers before overwriting. If pre-commit, post-checkout, or pre-push are already owned by a recognised hook manager (prek, pre-commit, husky, etc.), print a warning and instructions rather than silently renaming.

  2. Provide composable hook scripts — ship the workspace guard and post-checkout cleanup as standalone scripts (e.g., installed to .git/gitbutler/hooks/) that can be called from any hook manager config.

  3. Support --no-hooks (or a config flag) to skip all hook installation for teams that manage hooks independently.

  4. Handle pre-push consistently — either wrap it the same way as pre-commit/post-checkout, or don't wrap any hooks at all and rely on the composable scripts approach.

Suggested integration pattern

The correct integration for a prek-managed project looks like this (currently requires manual setup with no official guidance):

# prek.toml
[[repos]]
repo = "local"
hooks = [
    { id = "gitbutler-workspace-guard",
      name = "GitButler Workspace Guard",
      language = "system",
      entry = "nu .git/gitbutler/hooks/workspace-guard.nu",   # hypothetical
      pass_filenames = false,
      always_run = true,
      stages = ["pre-commit"] },
]

GitButler should ship these scripts and document this pattern as the recommended integration for projects using hook managers.

Relevant log output (Optional)

コントリビューターガイド