Skip to content

feat(install): multiplex every git hook so repo-local hook managers coexist - #9

Merged
lollipop-onl merged 5 commits into
mainfrom
claude/lefthook-coexistence-yphsnv
Jul 18, 2026
Merged

feat(install): multiplex every git hook so repo-local hook managers coexist#9
lollipop-onl merged 5 commits into
mainfrom
claude/lefthook-coexistence-yphsnv

Conversation

@lollipop-onl

Copy link
Copy Markdown
Member

A global core.hooksPath makes git look ONLY in ghostq's dir and shadows
every repo-local .git/hooks/* of every type. The dispatcher forwarded only
post-checkout, so pre-commit / pre-push / commit-msg / ... installed by
lefthook, pre-commit, husky, or plain scripts silently never fired — the
"coexists with lefthook" claim held for post-checkout alone.

This is a git-level limitation, not a lefthook quirk, so the fix is general:
ghostq install now writes a dispatcher for every client-side hook. Each one
chains to the shadowed hook (previous global hooksPath, then repo-local
.git/hooks/) and propagates a non-zero exit so gating hooks still gate;
post-checkout additionally keeps its null-ref apply gate and, as before, does
not gate on the chained exit.

Document the one wrinkle that remains lefthook-specific: its installer refuses
to target .git/hooks while core.hooksPath is set and --force would clobber
ghostq's global dir, so point it at the repo hooks dir for the install and drop
the local override afterward.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01GP3aV5RFzd8L4iQTeuACnF

… coexist

ghostq set a global core.hooksPath, which makes git look ONLY there and shadow
every repo-local .git/hooks — silently killing lefthook/husky/pre-commit hooks,
and tripping their install guards (they refuse to install, or --force clobbers
ghostq's dir). Coexistence never actually held.

Switch to init.templateDir: git copies ghostq's post-checkout into .git/hooks on
every clone / git init, and ghostq stays out of the hooks path. git keeps using
the repo's own .git/hooks, so any hook manager installs and fires untouched with
zero workaround. New clones and worktrees auto-link as before; repos cloned
before install take a one-time `ghostq apply`.

To avoid the override silently stripping git's default template, install seeds
the currently-effective template (default, or the user's own init.templateDir)
via a throwaway `git init`, so clones keep .git/info/exclude, description, and
sample hooks. A foreign post-checkout the template carried is preserved as
post-checkout.ghostq-orig and chained.

Accepted limitation: `git worktree add` runs the one shared
.git/hooks/post-checkout, so a tool whose config also declares post-checkout
displaces ghostq for new worktrees of that repo (manual `ghostq apply`); the
clone is unaffected and all other hooks coexist with no caveat. This is a git
single-slot limitation, not one ghostq can design away.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GP3aV5RFzd8L4iQTeuACnF
@lollipop-onl
lollipop-onl force-pushed the claude/lefthook-coexistence-yphsnv branch from 73d6e03 to b23f474 Compare July 18, 2026 04:11
claude added 4 commits July 18, 2026 04:40
Drop the mechanism walkthrough and the templateDir single-slot caveats; keep
only "your hooks work as usual" and the two cases that need a manual
`ghostq apply`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GP3aV5RFzd8L4iQTeuACnF
The released ghostq set a global core.hooksPath pointing at ~/.config/ghostq/
hooks. Upgrading to the init.templateDir build left that config in place, so git
kept looking only there — shadowing .git/hooks and silently defeating the new
template hook (and the coexistence fix) for every existing user, while leaving
the stale hooks dir behind.

install (and uninstall) now detect a core.hooksPath still pointing at ghostq's
legacy hooks dir, restore the core.hooksPath it had recorded (or unset it), and
remove the stale dir. Found in self-review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GP3aV5RFzd8L4iQTeuACnF
The caveat was buried as a to-do bullet; label it as a Limitation, state the
concrete trigger and remedy, and note that clones and all other hooks are
unaffected. Keep the pre-existing-clone case as a separate one-time setup note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GP3aV5RFzd8L4iQTeuACnF
…OPMENT

Lead with Getting started (install + adopt-your-first-file + the auto-restore
payoff) so a reader can act immediately, instead of burying Install below the
mechanism sections. Cut the "How it works" walkthrough down to observable
behavior (trigger, symlink-each, quiet-skip) and move the hook internals
(init.templateDir, seeding, null-ref gate, legacy migration) into DEVELOPMENT.md
where the rest of the implementation notes live. Keep the coexistence Limitation
in the README — that's a usage constraint, not an internal detail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GP3aV5RFzd8L4iQTeuACnF
@lollipop-onl
lollipop-onl merged commit acd5d78 into main Jul 18, 2026
1 check passed
@lollipop-onl
lollipop-onl deleted the claude/lefthook-coexistence-yphsnv branch July 18, 2026 06:11
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.

2 participants