Why don't Git hooks in .git/hooks travel with a clone, and how do teams share them?
answer
- Not everything in the repo directory is content
- Cloning copies commits, not configuration
- One config key redirects the hooks directory
- core.hooksPath, set once per clone
basics
~20 sHooks live in .git/hooks, part of the repository metadata that is never committed, so a clone never copies them. Teams instead commit hook scripts to a tracked directory and point Git at it with the core.hooksPath config setting.
solid answer
~40 sGit runs hooks from `$GIT_DIR/hooks` — `.git/hooks` in a normal clone. That directory is metadata, not content: it is not part of any tree, so nothing in it is versioned, pushed, or fetched. A fresh clone just gets the disabled `.sample` files. The standard fix is `core.hooksPath`, which tells Git to look in a different directory: commit the scripts to something like `.githooks/` and run `git config core.hooksPath .githooks`. Because config lives in `.git/config`, which is also not cloned, every clone still needs that one command — so teams automate it with a bootstrap step or a package lifecycle script, which is exactly what husky, lefthook and the pre-commit framework wrap. That per-clone step is a safety property: cloning alone must never be able to install code that runs on your machine.
code
bash · 9 linesmkdir -p .githooks
printf '#!/bin/sh\nexec npm run lint:staged\n' > .githooks/pre-commit
chmod +x .githooks/pre-commit
git add .githooks/pre-commit
git commit -m "chore: add shared pre-commit hook"
# every clone still needs this one line
git config core.hooksPath .githooks
git rev-parse --git-path hooksgo deeper
Be ready to say where Git looks for hooks and that a clone does not bring your teammate's hooks with it. Knowing the directory is .git/hooks and that .git is not versioned is enough at this level.
Explain the mechanism: core.hooksPath redirects the hooks directory to tracked files, and because config is not cloned, each clone still needs one enable step. Mention that it replaces .git/hooks rather than adding to it.
Show you have operated this. Talk about how the install is wired so new joiners get it, how you detect clones where hooks never got enabled, and why two managers in one repository silently cancel each other out.
Own the tradeoff: versioned hooks buy fast local feedback and a reviewable definition, but the enable step is opt-in by design and can never be an enforcement boundary. Decide what that means for where real policy lives.
## Why .git/hooks is not shared Git looks for hooks in the repository's hooks directory, which is `.git/hooks` in an ordinary clone. Everything under `.git` is repository *metadata* — the object database, refs, the index, config — not repository *content*. Only content that has been added to a tree and committed is transferred by `git clone`, `git fetch` or `git push`. Hook scripts placed in `.git/hooks` are therefore invisible to every other clone. What a new clone actually gets is a copy of Git's template directory: a set of example scripts named `pre-commit.sample`, `commit-msg.sample`, and so on. The `.sample` suffix means Git will not execute them, because Git only runs a hook file whose name matches the hook exactly and which is executable. ## core.hooksPath Recent Git supports the `core.hooksPath` configuration key, which overrides the hooks directory. Set it once per clone: `git config core.hooksPath .githooks` Now the hook scripts can be ordinary tracked files under `.githooks/`, reviewed in pull requests and updated for everyone by a normal commit. Two details matter. First, `core.hooksPath` **replaces** `.git/hooks` — it does not add a second search location, so any hook still sitting in `.git/hooks` silently stops running. Second, the executable bit is tracked by Git (mode `100755` versus `100644`), so committing the scripts with the executable bit set is what makes them runnable on checkout. You can confirm what Git will actually use with `git rev-parse --git-path hooks`, which prints the effective hooks directory after config is applied, or `git config --get core.hooksPath`. ## The install step, and why it cannot be avoided `core.hooksPath` is a config value, and configuration lives in `.git/config`, which is not transferred by a clone either. So there is always a one-time step per clone. This is deliberate. If a repository could ship configuration that made Git execute a script, then merely cloning an untrusted repository would be arbitrary code execution. Git keeps the decision to enable hooks on the machine that runs them. Teams automate the step rather than remove it: a documented `make setup`, a bootstrap script, or a package-manager lifecycle script such as npm's `prepare`, which runs after a normal dependency install. That is the trick most JavaScript hook managers use. ## What the managers add - **husky** keeps hooks in a tracked directory (conventionally `.husky/`) and points `core.hooksPath` at it; its install is normally wired to the `prepare` lifecycle script so a dependency install enables hooks. - **lefthook** reads a tracked `lefthook.yml` describing which commands run for which hook, and its install command writes the hook entries; it can run the listed commands in parallel. - **pre-commit** (the Python framework) reads `.pre-commit-config.yaml`, where each hook is pinned to a revision of the tool's repository and run in an isolated environment; its install writes a dispatch script into `.git/hooks`, which is why it conflicts with a manager that sets `core.hooksPath`. All three solve the same two problems: make the hook definitions versioned content, and make enabling them one command. ## Failure modes to expect - A developer clones and never runs the install step. Nothing warns them; the hooks are simply absent, and their commits skip every local check. - Two managers in one repository. The one that sets `core.hooksPath` wins and the one that writes into `.git/hooks` is ignored entirely — a confusing silent failure. - Automated or CI clones frequently install dependencies with scripts disabled, or install production dependencies only, so lifecycle-script-based hook installation does not happen there. That is usually the desired outcome, not a bug, but it means the hooks are not a guarantee anywhere. - Someone copies a script into `.git/hooks` for a quick fix while `core.hooksPath` is set, and cannot work out why it never runs. The upshot: versioned hooks plus one explicit enable step give you a shared, reviewable local check, but never an enforced one.
- Why can't a repository configure hooks for you automatically at clone time?Because configuration is not transferred by a clone, and that is intentional. If a repository could ship a setting that made Git execute a script, cloning an untrusted repository would be remote code execution. Git deliberately requires a local action — running the install command, or setting `core.hooksPath` yourself — before any repository-supplied script can run on your machine.
- What happens to scripts still sitting in .git/hooks once core.hooksPath is set?They stop running. `core.hooksPath` replaces the hooks directory rather than adding a second one, so Git never looks at `.git/hooks` again. This is the usual cause of two hook managers fighting: one redirects the path, the other writes into `.git/hooks`, and its hooks silently never fire. Check the effective directory with `git rev-parse --git-path hooks`.
- How do you verify on a colleague's machine that the shared hooks are actually active?Run `git config --get core.hooksPath` to see whether the redirect is set, and `git rev-parse --git-path hooks` to print the directory Git will really use. Then confirm the hook file exists there with the executable bit set — Git ignores a hook file that is not executable and ignores any name ending in `.sample`.
saying these in an interview costs you the question
- Claims hooks are committed and clone automatically with the repo
- Thinks .git/hooks is version-controlled like any other folder
- Believes core.hooksPath adds a directory instead of replacing .git/hooks
- Assumes the sample hooks in a fresh clone are active
- Says a repo can enable its own hooks without any local step