-
Notifications
You must be signed in to change notification settings - Fork 874
Description
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
-usersuffix - 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 ofcore.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-testHooks 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-testHooks 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-testResult — .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:
-
Detect existing hook managers before overwriting. If
pre-commit,post-checkout, orpre-pushare already owned by a recognised hook manager (prek, pre-commit, husky, etc.), print a warning and instructions rather than silently renaming. -
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. -
Support
--no-hooks(or a config flag) to skip all hook installation for teams that manage hooks independently. -
Handle
pre-pushconsistently — either wrap it the same way aspre-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.