Add an external Git hooks backend for Rush, Husky, and other hook managers
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 5.1k
- Forks
- 475
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 178
Description
Problem or use case
Entire CLI currently decides whether Git hooks are installed by resolving Git's active hooks directory with git rev-parse --git-path hooks and checking the five managed hook files for the Entire CLI hooks marker:
prepare-commit-msgcommit-msgpost-commitpost-rewritepre-push
That works when Entire owns the active hook files directly. It misreports health in repositories where another tool owns the active hook wrappers and dispatches to source hooks stored elsewhere.
The result is a false negative or unsafe repair path:
entire status/entire doctorcan report Entire Git hooks as missing even when the manager's source hook already includes Entire.entire enable --forcecan replace active wrapper files owned by Rush, Husky, or another manager.- Users cannot express that the source of truth lives outside Git's active hooks directory.
Rush: Rush generates active wrappers in .git/hooks/*, while user-owned hooks live in common/git-hooks/*. The generated wrappers should not be treated as the only installation truth.
Husky (v9, standard husky init): Running prepare / husky sets core.hooksPath to .husky/_. Git executes thin stubs in .husky/_/* (each sources .husky/_/h); Husky then runs the user hook at .husky/<hook-name> if it exists. The _/ tree is generated and usually gitignored—users edit .husky/pre-commit, not .husky/_/pre-commit.
This differs from Rush in two important ways:
- Entire must never write into
.husky/_/*. Replacing those stubs breaks Husky's dispatch chain; followingcore.hooksPathalone is especially dangerous here. - Husky repos often start with only
.husky/pre-commit. Entire's five lifecycle hooks may be missing, partial, or merged with existing commands (lint, tests, etc.)—not five standalone wrapper files like Rush'scommon/git-hooks/*.
Legacy Husky v8 installs may still use core.hooksPath=.husky without the _/ split; external mode should rely on configured external_dir, not hard-coded Husky layout detection.
Examples: input → execution → output
Fictional repo layouts below. Wrapper shapes follow common Rush / Husky v9 behavior; names and scripts are made up.
Rush
Input (on disk after rush update, Entire appended to source hooks by hand or entire enable):
my-monorepo/
├── .git/hooks/prepare-commit-msg # Rush-generated wrapper (no Entire marker)
├── .git/hooks/commit-msg # Rush-generated wrapper (no Entire marker)
├── .git/hooks/post-commit # …
├── .git/hooks/post-rewrite # …
├── .git/hooks/pre-push # …
└── common/git-hooks/
├── prepare-commit-msg # source of truth (Entire + optional team hooks)
├── commit-msg
├── post-commit
├── post-rewrite
└── pre-push
.git/hooks/prepare-commit-msg (wrapper — Rush owns this file):
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_IMPLEMENTATION_PATH="$SCRIPT_DIR/../../common/git-hooks/prepare-commit-msg"
if [[ -f "$SCRIPT_IMPLEMENTATION_PATH" ]]; then
"$SCRIPT_IMPLEMENTATION_PATH" "$@"
else
echo "Git hook missing in repo; run rush install or rush update." >&2
exit 1
fi
common/git-hooks/prepare-commit-msg (source — user / Entire own this file):
#!/bin/sh
# Entire CLI hooks
if command -v entire >/dev/null 2>&1; then
entire hooks git prepare-commit-msg "$1" "$2" 2>/dev/null || true
fi
common/git-hooks/commit-msg (source may chain multiple tools):
#!/bin/sh
set -e
# Entire CLI hooks
if command -v entire >/dev/null 2>&1; then
entire hooks git commit-msg "$1" || true
fi
node scripts/validate-commit-msg.js --edit "$1"
Execution (what actually runs on git commit):
git commit
→ .git/hooks/prepare-commit-msg # wrapper (no Entire marker here)
→ common/git-hooks/prepare-commit-msg
→ entire hooks git prepare-commit-msg …
→ .git/hooks/commit-msg
→ common/git-hooks/commit-msg
→ entire hooks git commit-msg …
→ node scripts/validate-commit-msg.js …
Output (today vs desired):
# Today — direct backend follows core.hooksPath / .git/hooks only
$ entire doctor
Entire Git hooks: missing (0/5 managed hooks marked in .git/hooks)
# Desired — external backend with external_dir: "common/git-hooks"
$ entire doctor
Git hooks backend: external (common/git-hooks)
Entire Git hooks: installed (5/5 source hooks include Entire)
Note: active wrappers live in .git/hooks; refresh with rush install or rush update if source hooks changed
Husky (v9)
Input (on disk after pnpm prepare / husky init, before Entire):
my-app/
├── .git/config # core.hooksPath = .husky/_
├── .husky/
│ ├── pre-commit # user hook (e.g. lint)
│ └── _/
│ ├── h # Husky dispatcher (generated)
│ ├── pre-commit # stub → sources h → runs ../pre-commit
│ ├── prepare-commit-msg # stub (no ../prepare-commit-msg yet)
│ └── …
.husky/pre-commit:
npm run lint-staged
.husky/_/pre-commit:
#!/usr/bin/env sh
. "$(dirname "$0")/h"
.husky/_/h (abbreviated):
n=$(basename "$0")
s=$(dirname "$(dirname "$0")")/$n # e.g. .husky/pre-commit
[ ! -f "$s" ] && exit 0
sh -e "$s" "$@"
Input (after entire enable with external_dir: ".husky"):
.husky/prepare-commit-msg (new or updated — Entire appended, existing lines preserved):
#!/bin/sh
# Entire CLI hooks
if command -v entire >/dev/null 2>&1; then
entire hooks git prepare-commit-msg "$1" "$2" 2>/dev/null || true
fi
.husky/_/prepare-commit-msg (unchanged stub):
#!/usr/bin/env sh
. "$(dirname "$0")/h"
Execution (what actually runs on git commit):
git commit
→ .husky/_/pre-commit # Git hooksPath points here
→ .husky/_/h
→ .husky/pre-commit
→ npm run lint-staged
git commit # after Entire hooks added for prepare-commit-msg / commit-msg / …
→ .husky/_/prepare-commit-msg
→ .husky/_/h
→ .husky/prepare-commit-msg
→ entire hooks git prepare-commit-msg …
Output (today vs desired):
# Today — direct backend installs into core.hooksPath (.husky/_)
$ entire enable
Installed 5 hooks into .husky/_ # breaks Husky stubs that source h
$ entire doctor
Entire Git hooks: installed (5/5 in .husky/_) # misleading; Husky dispatch may be broken
# Desired — external backend with external_dir: ".husky"
$ entire enable
Appended Entire to .husky/prepare-commit-msg, .husky/commit-msg, …
Skipped .husky/_/* (Husky-managed stubs)
$ entire doctor
Git hooks backend: external (.husky)
Entire Git hooks: partial (3/5 hooks include Entire)
Note: Git executes .husky/_/*; run pnpm prepare after editing .husky/*
Desired behavior
When an external hook manager owns the active wrappers, Entire should read and write source hooks in a configured directory, report accurate health, and never destroy manager-owned wrappers—even with --force.
Rush example:
# .entire/settings.json
{
"git_hooks": {
"backend": "external",
"external_dir": "common/git-hooks"
}
}
$ entire doctor
Git hooks backend: external (common/git-hooks)
Entire Git hooks: installed (5/5 source hooks marked)
$ entire enable
Writing Entire hooks to common/git-hooks/ (external backend)
Skipped .git/hooks/* — managed by Rush wrappers
$ entire enable --force
Updating common/git-hooks/* only
Did not modify .git/hooks/* or other manager-owned wrappers
Husky example:
# .entire/settings.json
{
"git_hooks": {
"backend": "external",
"external_dir": ".husky"
}
}
$ entire doctor
Git hooks backend: external (.husky)
Entire Git hooks: partial (3/5 hooks include Entire)
Note: Husky runs hooks from .husky/_; refresh with `pnpm prepare` after editing .husky/*
$ entire enable
Appended Entire commands to .husky/prepare-commit-msg, .husky/commit-msg, ...
Created .husky/post-commit (new)
Skipped .husky/_/* — Husky-managed stubs
$ entire enable --force
Updated .husky/* only; did not modify .husky/_/*
After enabling in external mode, users refresh the external manager when needed—for example rush update / rush install (Rush) or pnpm prepare / husky (Husky, regenerates .husky/_/*). Doctor should say so when source hooks include Entire but active wrapper linkage cannot be proven.
Default behavior for repositories without git_hooks stays unchanged (direct backend, active hooks directory only).
Proposed solution
Add a top-level git_hooks setting:
{
"git_hooks": {
"backend": "external",
"external_dir": "common/git-hooks"
}
}
Default when absent or explicit:
{
"git_hooks": {
"backend": "direct"
}
}
direct (default): Preserve existing behavior—resolve Git's active hooks directory and check/install Entire's five managed hooks there.
external: Resolve external_dir relative to the repository root unless absolute. Check each managed hook under external_dir for Entire integration (marker and/or known command lines). Do not treat .git/hooks, core.hooksPath, or manager-generated paths like .husky/_ as the only source of truth. Do not overwrite active wrappers owned by the external manager, including when entire enable --force is used.
Manager-specific install behavior under external:
- Rush-style layouts (
external_dir: "common/git-hooks"): write or update full hook scripts there; Rush wrappers in.git/hooks/*stay untouched. - Husky-style layouts (
external_dir: ".husky"): create or update user hooks in.husky/*only—append/chain Entire commands into existing hook files when present; never replace.husky/_/*stubs.
Health output should distinguish:
- Entire integration present in configured source hooks (installed / partial).
- Source hooks updated but manager wrappers may need refresh (actionable hint, not a hard failure—for Husky, suggest
pnpm prepareor equivalent).
Implementation notes / test plan:
- Settings schema — Accept
directandexternal(+ requiredexternal_dir); rejectexternalwithoutexternal_dir; default unchanged whengit_hooksis absent. - Direct parity — Existing install/health tests pass with no
git_hookssetting. - External health — Rush:
.git/hooks/*wrappers + markedcommon/git-hooks/*→ installed. Husky:core.hooksPath=.husky/_, Entire lines in.husky/*→ installed/partial; stubs in.husky/_/*are ignored for marker checks. Missing Entire integration on any required hook → partial/missing. - External install safety —
entire enable/--forcewrites only underexternal_dir(.husky/*, not.husky/_/*); Rush.git/hooks/*and Husky.husky/_/*bytes unchanged. - Doctor/status — Report backend + external directory; suggest manager refresh when linkage cannot be proven.
Risks:
- Marker-only detection is compatible with existing hook files but can be spoofed; a future managed block with explicit begin/end markers would be more robust.
- External health is intentionally weaker than direct—it proves configured source files include Entire, not that Git will execute them after the manager regenerates wrappers.
- Entire should not auto-run
rush update,pnpm prepare, etc.; wrapper generation stays manager-specific unless a future explicit integration is designed.
Alternatives or workarounds
- Manual hooks only: Copy Entire hook snippets into the manager's source directory by hand (for Husky, append lines to
.husky/<hook>and runpnpm prepare) and ignoreentire doctorGit hook warnings. Fragile and easy to drift from Entire updates. direct+ overwrite wrappers: Runentire enable --forceagainst.git/hooksorcore.hooksPath. Breaks Rush/Husky wrapper chains and gets overwritten on the next manager refresh.- Disable Entire Git hooks: Use agent-only integration without lifecycle hooks; loses automatic checkpoint capture on commit/push.
- Fork hook install logic per manager: Hard-code Rush vs Husky paths in Entire. Does not scale; a single
externalbackend with configurableexternal_dircovers Rush, Husky, and custom layouts.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing the existing Git hook installation and health paths used by entire enable, entire doctor, and entire status, then inspect settings loading and validation. Add coverage for direct-backend parity, external Rush and Husky layouts, source-hook health reporting, and the guarantee that manager-owned wrappers remain unchanged, including with --force.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, go
- Domain
- cli, developer-experience, tooling
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100