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-msg
commit-msg
post-commit
post-rewrite
pre-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 doctor can report Entire Git hooks as missing even when the manager's source hook already includes Entire.
entire enable --force can 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; following core.hooksPath alone 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's common/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:
.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 prepare or equivalent).
Implementation notes / test plan:
- Settings schema — Accept
direct and external (+ required external_dir); reject external without external_dir; default unchanged when git_hooks is absent.
- Direct parity — Existing install/health tests pass with no
git_hooks setting.
- External health — Rush:
.git/hooks/* wrappers + marked common/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 / --force writes only under external_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 run pnpm prepare) and ignore entire doctor Git hook warnings. Fragile and easy to drift from Entire updates.
direct + overwrite wrappers: Run entire enable --force against .git/hooks or core.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
external backend with configurable external_dir covers Rush, Husky, and custom layouts.
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 hooksand checking the five managed hook files for theEntire CLI hooksmarker:prepare-commit-msgcommit-msgpost-commitpost-rewritepre-pushThat 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.Rush: Rush generates active wrappers in
.git/hooks/*, while user-owned hooks live incommon/git-hooks/*. The generated wrappers should not be treated as the only installation truth.Husky (v9, standard
husky init): Runningprepare/huskysetscore.hooksPathto.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:
.husky/_/*. Replacing those stubs breaks Husky's dispatch chain; followingcore.hooksPathalone is especially dangerous here..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=.huskywithout the_/split; external mode should rely on configuredexternal_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 orentire enable):.git/hooks/prepare-commit-msg(wrapper — Rush owns this file):common/git-hooks/prepare-commit-msg(source — user / Entire own this file):common/git-hooks/commit-msg(source may chain multiple tools):Execution (what actually runs on
git commit):Output (today vs desired):
Husky (v9)
Input (on disk after
pnpm prepare/husky init, before Entire):.husky/pre-commit:.husky/_/pre-commit:.husky/_/h(abbreviated):Input (after
entire enablewithexternal_dir: ".husky"):.husky/prepare-commit-msg(new or updated — Entire appended, existing lines preserved):.husky/_/prepare-commit-msg(unchanged stub):Execution (what actually runs on
git commit):Output (today vs desired):
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:
Husky example:
After enabling in external mode, users refresh the external manager when needed—for example
rush update/rush install(Rush) orpnpm 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_hooksstays unchanged (directbackend, active hooks directory only).Proposed solution
Add a top-level
git_hookssetting:{ "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: Resolveexternal_dirrelative to the repository root unless absolute. Check each managed hook underexternal_dirfor 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 whenentire enable --forceis used.Manager-specific install behavior under
external:external_dir: "common/git-hooks"): write or update full hook scripts there; Rush wrappers in.git/hooks/*stay untouched.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:
pnpm prepareor equivalent).Implementation notes / test plan:
directandexternal(+ requiredexternal_dir); rejectexternalwithoutexternal_dir; default unchanged whengit_hooksis absent.git_hookssetting..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.entire enable/--forcewrites only underexternal_dir(.husky/*, not.husky/_/*); Rush.git/hooks/*and Husky.husky/_/*bytes unchanged.Risks:
rush update,pnpm prepare, etc.; wrapper generation stays manager-specific unless a future explicit integration is designed.Alternatives or workarounds
.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.externalbackend with configurableexternal_dircovers Rush, Husky, and custom layouts.