Module 15 — Hooks, Maintenance, and Git at Scale

Updated 7 September 2026

Module 15 — Hooks, Maintenance, and Git at Scale. The final module takes Git from a personal tool to fleet infrastructure. Hooks automate and enforce policy at commit and push time; gc/fsck/packfiles keep repositories healthy and small; and shallow, partial, and sparse clones — plus the hard realities of monorepos — are how Git survives repositories with millions of files and commits. Everything from Modules 1–14 converges here, at the scale where it actually runs a company.

🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)

Before you start. You need Module 3 (objects, packfiles, refs — the raw material gc and fsck manage), Module 8 (remotes, push — where server hooks and clones live), and Module 9 (protected branches — hooks are the mechanism beneath them). The exercises build local repos; some scale flags (--depth, --filter) need a file:// URL to be honored, which the exercises use.

Part A — Hooks: automating and enforcing

A1. Client-side hooks

A hook is a script Git runs automatically at a defined moment. They live in .git/hooks/ (Module 3's floor-plan directory, finally opened), named for the event they fire on: pre-commit runs before a commit is recorded — exit nonzero and the commit is aborted; commit-msg validates the message; pre-push runs before a push leaves. These client-side hooks are for convenience and early feedback: run the linter, block a commit with a syntax error or a leftover conflict marker (Module 6) or a secret, enforce the message format from Module 2. A hook is any executable — bash, Python, anything — and it must be marked executable. Crucially, client hooks are not committed (they live in .git, which doesn't travel), so they can't be relied on for enforcement — and can be bypassed with --no-verify.

🧪 Exercise 15.1 — a pre-commit hook that blocks a marker, and its bypass
bash
cd ~/git-course/kitchen                # any repo with a commit
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/bash
# refuse to commit staged changes containing FIXME
if git diff --cached | grep -q "FIXME"; then
  echo "pre-commit: refusing commit — staged changes contain FIXME"
  exit 1
fi
exit 0
EOF
chmod +x .git/hooks/pre-commit         # hooks MUST be executable
echo "x = 1  # FIXME later" >> app.py && git add app.py
git commit -m "Add x with a FIXME"     # blocked by the hook
echo "exit code: $?"
sed -i 's/  # FIXME later//' app.py && git add app.py
git commit -m "Add x"                  # now it passes
git commit --no-verify -m "..." --allow-empty   # --no-verify SKIPS the hook
Expected result — click to reveal (the first commit is blocked on purpose)
plain text
pre-commit: refusing commit — staged changes contain FIXME
exit code: 1
[main 1c8f1dc] Add x
 1 file changed, 1 insertion(+)
[main f86fee8] ...

What to read out of it: the first commit failed — the hook printed its message and exited 1, and Git aborted the commit (exit code 1; nothing was recorded). After removing the FIXME, the same commit succeeded — the hook exited 0 and got out of the way. The last command demonstrates the escape hatch: --no-verify skips client-side hooks entirely, and the commit went through despite the hook. That bypass is the whole reason client hooks are "helpful reminders, not enforcement": they run on the developer's machine, aren't committed to the repo, and any developer can skip them. For rules that must hold, you need the server (A2).

Real-world analogy — the spellchecker vs the printing press's editor

A pre-commit hook is your word processor's spellcheck: it flags problems as you type, red-underlining mistakes before you print. Fast, helpful, catches typos early. But you can click "ignore" on any squiggle, and the spellchecker only exists on your computer — a coworker's document isn't checked by yours.

Where the analogy stops working. Spellcheck is purely advisory and everyone knows it. The dangerous illusion with client hooks is treating them as a gate — "our pre-commit hook prevents secrets from being committed." It prevents nothing enforceable: it's not committed to the repo (new clones don't have it), and --no-verify skips it. The actual gate is at the press itself — the server-side hook (A2) or the host's protected-branch rules (Module 9) that check every push regardless of what any developer's machine did. Client hooks help authors; server hooks enforce policy. Confusing the two is a real security failure.

A2. Server-side hooks

Server-side hooks live in the bare repository (Module 8) and run when a push arrives, so they apply to everyone and can't be bypassed by a developer's --no-verify. pre-receive runs once per push before anything is accepted — exit nonzero and the entire push is rejected; this is where you enforce non-negotiables: branch protection, required commit-message format, rejecting force-pushes to main, blocking large files or secrets. update runs per-ref; post-receive runs after acceptance — the trigger for deploys, CI, and notifications. On hosting platforms you rarely write these directly (they're exposed as branch protection rules, required checks, and webhooks — Module 9's enforcement layer is server hooks, productized), but knowing they're the underlying mechanism is what the interview probes.

🧪 Exercise 15.2 — a pre-receive hook that rejects a push
bash
cd ~/git-course && git init --bare -q origin.git   # a server
git clone -q origin.git work && cd work
echo "code" > f.txt && git add f.txt && git commit -q -m "initial"
cat > ../origin.git/hooks/pre-receive <<'EOF'
#!/bin/bash
echo "pre-receive: this server is frozen for the release"
exit 1
EOF
chmod +x ../origin.git/hooks/pre-receive
git push origin main                   # the server refuses
Expected result — click to reveal (the push is rejected on purpose)
plain text
remote: pre-receive: this server is frozen for the release
 ! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to '...'

What to read out of it: the hook's output is prefixed remote: — it ran on the server, and its message traveled back to the pusher. The push was [remote rejected] with the reason (pre-receive hook declined), and git push failed. Note what's different from a client hook: there is no --no-verify that helps here--no-verify skips the pusher's local hooks, not the server's; the server decides unilaterally. This is real enforcement: every push, every developer, no bypass. A one-line pre-receive is how "no force-push to main" or "every commit message must reference a ticket" becomes a rule the repository cannot violate, not a guideline it hopes people follow. (Remove the hook afterward: rm ../origin.git/hooks/pre-receive.)

🎯 Interview questions — Part A

🎯 "What are Git hooks, and can you give an example of how they might be used?" — asked verbatim in LabEx's Git Interview Questions, 2025; also "How do you configure and use Git hooks for pre-commit checks?" in Interview Coder, Sep 2025

Git hooks are scripts Git runs automatically at defined events, stored in .git/hooks/ and named for their trigger. Client-side hooks fire on the developer's machine — pre-commit (run linters/tests, block bad commits — exit nonzero aborts), commit-msg (enforce message format), pre-push — and are for fast local feedback. Server-side hooks fire in the bare repo when a push arrives — pre-receive/update (accept or reject the push), post-receive (trigger CI/deploy/notifications). Example: a pre-commit hook that greps staged changes for leftover conflict markers or secrets and refuses the commit; a pre-receive hook that rejects any push to main that isn't a fast-forward.

The details that separate candidates: an average answer says "scripts that run on Git events, like pre-commit for linting." A strong answer draws the client-vs-server line sharply: client hooks aren't committed (don't travel with clones) and are bypassable with --no-verify, so they're convenience not enforcement; only server-side hooks (or the host's protected-branch/required-check features that wrap them) actually enforce policy for everyone. Naming a tooling manager like pre-commit (the framework) for sharing client hooks across a team, since raw .git/hooks don't clone, shows real-world practice.

Part B — Maintenance: gc, packfiles, fsck

B1. Garbage collection and packing

Every object you create starts as a loose object — one zlib-compressed file under .git/objects/ (Module 3's warehouse). Thousands of tiny files are slow and waste disk (each takes a filesystem block). git gc (garbage collection) does two things: it packs loose objects into packfiles — single files bundling many objects with delta compression between similar objects (Module 1's promised deep-storage saver: a file's versions stored as deltas against each other, so a repo of text history compresses enormously), and it prunes unreachable objects past a grace period (the abandoned commits from resets and rebases — Module 7 — that no ref or reflog points to anymore). Git runs gc automatically when loose objects pile up; you rarely call it by hand, but knowing what it does explains why .git sometimes shrinks dramatically and why an old commit becomes truly unrecoverable after weeks (its object was pruned).

🧪 Exercise 15.2b — watch loose objects get packed
bash
cd ~/git-course && git init -q packdemo && cd packdemo
for i in $(seq 1 20); do echo "content $i" > f$i.txt; git add f$i.txt; git commit -q -m "commit $i"; done
git count-objects -v | head -3        # loose objects, before
git gc
git count-objects -v | head -5        # after: loose → packed
Expected result — click to reveal
plain text
count: 71
size: 284
in-pack: 0
count: 0
size: 0
in-pack: 71
packs: 1

What to read out of it: before gc, count: 71 loose objects (in-pack: 0) — 71 separate compressed files for 20 commits' worth of blobs, trees, and commits. After gc, count: 0 loose objects and in-pack: 71 — every object moved into a packfile (packs: 1), a single file with an index. On a real repo the size difference is dramatic: delta compression means a file's 100 revisions are stored roughly as 99 small deltas plus one base, not 100 full copies. This is the storage optimization Module 1 promised and Module 3 deferred, now visible: snapshots are the model, packfiles with deltas are the storage. Git does this automatically as loose objects accumulate; git gc just triggers it on demand.

B2. Integrity checking with fsck

Official docs: git-fsck manual

git fsck ("file system check") verifies the object database: it recomputes every object's hash and confirms it matches its name (Module 3's content-addressing = built-in integrity — corruption is a hash mismatch), and it reports objects that are dangling or unreachable (present but pointed to by no ref — the residue of resets, rebases, and deletions, findable before gc prunes them). It's both a corruption detector (disk errors, incomplete transfers) and, usefully, a recovery tool: git fsck --unreachable or --lost-found surfaces orphaned commits the reflog might have missed — a last resort when Module 7's reflog isn't enough.

🧪 Exercise 15.3 — check integrity, then find an orphaned commit
bash
cd ~/git-course/packdemo
git fsck                               # health check — dangling objects are normal
echo "temp" > temp.txt && git add temp.txt && git commit -q -m "temp work"
git reset --hard HEAD~1                # abandon that commit (Module 7)
git fsck --unreachable                 # empty! the reflog still holds it (Module 7)
git fsck --unreachable --no-reflogs | head -4   # ignore the reflog: now they surface
Expected result — click to reveal
plain text
dangling commit e1c0ffee...
unreachable commit 441404abe210475b47529ab47e98aa356116df1c
unreachable tree cb6539195517a051bd578ce273c85801d01ddde0
unreachable blob 9c595a6fb7692405a5c4a10e1caf93d7a5bd9c37

What to read out of it: plain git fsck reported no errors (a healthy repo) — dangling notices are normal, not corruption. Here's the subtlety worth catching: git fsck --unreachable right after the reset prints nothing — because the abandoned commit is still referenced by the reflog (HEAD@{1}, Module 7's safety net), so by default fsck counts reflog-held objects as reachable. Adding --no-reflogs tells fsck to ignore the reflog, and only then do the orphaned commit, tree, and blob surface. That's the same work Module 7's reflog rescues — here found by scanning the object database directly, and a live demonstration that the reflog is why a just-reset commit isn't really lost yet. If fsck ever reported an actual error (missing/bad object, hash mismatch), that would be corruption — restore the object from any other clone (Module 1: every clone is a full backup). Integrity isn't a feature Git adds; it's arithmetic Git can check on demand.

Now imagine this at 500 hosts. Repository maintenance is a fleet-health discipline. On busy servers hosting thousands of repos, uncontrolled loose-object growth and un-repacked histories degrade every clone and fetch. Modern Git ships git maintenance — a scheduler (git maintenance start registers background tasks via cron/systemd) that runs incremental repacking, commit-graph writing (a cache that makes git log/merge-base fast on huge histories), and prefetching, without the stop-the-world pauses a full gc causes. Hosting platforms run aggressive server-side maintenance continuously. The operational lesson: on a large monorepo, an unmaintained .git is a performance bug — commit-graph and incremental repack are why git log stays instant on a repo with two million commits, and configuring git maintenance (or trusting your host's) is real infrastructure work, not housekeeping trivia.

🎯 Interview questions — Part B

🎯 Corpus note — git gc, packfiles, and git fsck

In the surveyed corpus (Interview Coder, LabEx), git gc, packfiles, and fsck appear only inside broader answers (usually the large-repo/maintenance question, e.g. "git repack -ad && git gc --prune=now"), not as verbatim standalone questions — so this Part carries a corpus note rather than a fabricated block. Cold-recall facts: git gc packs loose objects into delta-compressed packfiles and prunes unreachable ones past a grace period; packfiles are Git's real storage (snapshots are the model, deltas the storage — Modules 1/3); git fsck verifies object integrity by recomputing hashes and surfaces unreachable objects for recovery; git maintenance schedules incremental, non-blocking upkeep at scale. The interview-worthy connection is that all of this is why content-addressing pays off — integrity is checkable and storage is dedup-plus-delta because objects are named by their content.

Part C — Cloning at scale

C1. Shallow, partial, and sparse clones

A full clone downloads all history of all files (Module 8) — perfect for a small repo, ruinous for a huge one. Three techniques trade completeness for speed. A shallow clone (--depth N) fetches only the last N commits — no deep history, so CI (which only needs the current commit to build) clones in seconds; deepen later with git fetch --depth or get everything with git fetch --unshallow. A partial clone (--filter=blob:none) fetches all commit/tree history but no file contents up front — blobs are downloaded lazily, on demand, when you actually check out or diff them; ideal for a large repo where you work on a fraction of the files. A sparse checkout goes further: fetch normally but only materialize a subset of directories in your working tree — essential in monorepos where nobody needs all 200 services checked out.

🧪 Exercise 15.4 — a shallow clone, deepened, then completed

First build a demo remote with real history and a few service directories (used by both 15.4 and 15.5). Shallow/partial flags are ignored for plain local paths, so the clones use a file:// URL, which exercises the real transport.

bash
cd ~/git-course
git init --bare -q bigrepo.git                 # a demo server
git clone -q bigrepo.git seed && cd seed
for i in $(seq 1 29); do echo "line $i" >> history.txt; git add history.txt; git commit -q -m "commit $i"; done
mkdir -p backend frontend docs
echo "be" > backend/server.py; echo "fe" > frontend/app.js; echo "d" > docs/readme.md
git add . && git commit -q -m "Add service directories"   # 30 commits total
git push -q origin main
cd ~/git-course
git clone --depth 1 "file://$HOME/git-course/bigrepo.git" shallow-clone   # last commit only
cd shallow-clone
echo "commits: $(git log --oneline | wc -l)"
git rev-parse --is-shallow-repository
git fetch --depth 5 && echo "after --depth 5: $(git log --oneline | wc -l)"
git fetch --unshallow && echo "after --unshallow: $(git log --oneline | wc -l)"
git rev-parse --is-shallow-repository
Expected result — click to reveal
plain text
commits: 1
true
after --depth 5: 5
after --unshallow: 30
false

What to read out of it: --depth 1 cloned just the tip — 1 commit of a 30-commit history, and git rev-parse --is-shallow-repository confirms true. This is the CI clone: seconds instead of minutes on a big repo, because deep history never transferred. Then it deepens on demand — --depth 5 fetches back to 5 commits, --unshallow fetches the rest (all 30), and is-shallow-repository flips to false: the clone is now complete, indistinguishable from a full one. So shallowness is reversible — start minimal, fetch more only if you need it. (Partial clone, --filter=blob:none, is the sibling trick: full commit history but blobs fetched lazily — it needs a server that advertises the filter, so a modern host, not this bare-file demo.)

🧪 Exercise 15.5 — a sparse checkout: only the directory you need
bash
cd ~/git-course
git clone --no-checkout "file://$HOME/git-course/bigrepo.git" sparse-clone
cd sparse-clone
git sparse-checkout init --cone       # cone mode: whole directories
git sparse-checkout set backend       # materialize ONLY backend/ (+ root files)
git checkout main
ls                                     # what's actually in the working tree?
Expected result — click to reveal (the demo remote from 15.4 has backend/, frontend/, docs/)
plain text
backend
history.txt

What to read out of it: despite the repo containing backend/, frontend/, and docs/, only backend/ (plus top-level files like history.txt) appears in the working tree — git sparse-checkout set backend told Git to materialize just that directory. The full history and objects are still there (you can widen the checkout anytime with another set), but your working tree holds a fraction of the files. In a monorepo of 200 services this is the difference between a working tree of millions of files and one of a few thousand — your editor, file search, and build only see what you actually work on. Cone mode (--cone) keeps this fast by working at directory granularity rather than arbitrary patterns.

🎯 Interview questions — Part C

🎯 Corpus note — shallow / partial / sparse clones

These appear in the corpus (Interview Coder, LabEx) only inside the large-repo/monorepo answer (e.g. "use partial clone and shallow clone to reduce checkout size; git clone --filter=blob:none; sparse checkout for monorepos"), never as verbatim standalone questions — hence a corpus note. What to have ready: shallow (--depth N) = fewer commits (CI's trick, reversible via --unshallow); partial (--filter=blob:none) = all history metadata but lazy blob fetch (needs server support); sparse checkout = materialize only chosen directories (--cone mode) for a manageable working tree. The differentiator when this comes up as the "how do you work with a huge repo" follow-up is knowing which axis each cuts — history depth (shallow), blob content (partial), working-tree breadth (sparse) — because a real monorepo setup combines all three.

Part D — Monorepos and the edges of Git

D1. Where Git strains, and how teams cope

Git was built for the Linux kernel — large, but source code. Push it to a monorepo (one repository holding an entire company's code — thousands of projects, millions of files, years of history) and its assumptions strain in specific ways worth naming. Operations that scan the whole working tree (git status, git add) slow as file count grows, because Git stats every file. History operations slow as commit count grows, unless the commit-graph cache (Part B's maintenance) is built. A full clone becomes prohibitively large. And a single lock on the repo can bottleneck thousands of developers. The coping toolkit is everything in Part C combined — partial clone (no blobs up front) + sparse checkout (few directories) + shallow-ish fetches + aggressive server maintenance — plus purpose-built extensions: Microsoft's Scalar (now upstreamed into Git) and filesystem-virtualization layers (VFS for Git) that fetch objects on access, letting a developer "check out" a 300 GB repo and only ever download the files they touch. The honest interview answer isn't "Git can't do monorepos" (Google, Microsoft, and Meta run the largest repos on earth on Git or Git-derived systems) — it's knowing which specific operations degrade and which specific features address each.

Counter-intuitive: the thing that makes Git strain at monorepo scale is not history size — it's working-tree file count. Git's model (Modules 1–3) makes deep history cheap: commits are pointers, gc delta-compresses, and the commit-graph caches traversal, so a repo with ten million commits can stay fast. But many everyday commands (git status, git add -A, git checkout) must examine every file in the working tree to detect changes — and that cost scales with the number of files you have checked out, not with history. So the highest-leverage scale fix is often sparse checkout (fewer files materialized), not history pruning — counter to the intuition that "big repo = too much history." Teams that shrink their working tree see git status go from 30 seconds to instant while the full history stays intact. Diagnose the right dimension: file count in the working tree, not commits in the past.

🎯 Interview questions — Part D

🎯 Corpus note — monorepos and Git at scale

Monorepo strategy appears in the corpus (Interview Coder, LabEx) only inside the broad large-repo answer ("for monorepos, use sparse checkout and partial clone"), never as a verbatim standalone question — a corpus note, then. The interview-ready shape of the answer: name which operations degrade (working-tree scans like status/add with file count; history traversal without the commit-graph; full-clone size) and which feature addresses each (sparse checkout for file count, commit-graph/maintenance for traversal, partial+shallow clone for transfer), and know that Git does run at extreme scale (Scalar/VFS, upstreamed maintenance) rather than claiming it can't. The senior signal — as the counter-intuitive callout says — is identifying working-tree file count, not history depth, as the usual bottleneck.

Part E — Production practice

E1. Symptom → cause → diagnosis → fix

SymptomWhat is really happeningWhat to runThe fix
"Our pre-commit hook lets secrets through sometimes"Client hooks aren't committed (new clones lack them) and --no-verify skips themCheck whether the hook is even present in each cloneEnforce server-side (pre-receive / protected branches / required checks); client hook is only early feedback
A shared hook works for some teammates, not others.git/hooks/ doesn't travel with clones — everyone installed it (or didn't) by handls .git/hooks/ on each machineManage client hooks with a framework (pre-commit, Husky) checked into the repo + a bootstrap step
.git is huge / clones are slow despite a small working treeLoose-object buildup, un-repacked history, or committed large files (Module 14)git count-objects -vH (loose count + sizes)git gc (or git maintenance start for ongoing); migrate big blobs to LFS + history rewrite
error: object file … is empty / fatal: … is corruptDisk-level corruption — an object's bytes no longer match its hashgit fsck (reports missing/bad objects)Restore the corrupt objects from any healthy clone (every clone is a full backup — Module 1)
git status takes 20+ seconds in a big repoWorking-tree file count, not history — every file is stat'd on each statusCount checked-out files; check if sparse-checkout is in useSparse checkout to materialize fewer directories; enable core.fsmonitor; build the commit-graph
CI clones take minutes and pull years of history it never usesFull clone of a large repo when only the current commit is neededTime the clone; check its depthgit clone --depth 1 (shallow) — often with --filter=blob:none; deepen only if a step needs history
Recovered commit "vanished for good" weeks after a bad resetThe unreachable object was pruned by gc past its grace periodgit fsck --unreachable (empty — it's gone)Restore from a clone/backup that still has it; recover promptly next time (reflog window is finite — Module 7)

E2. Capstone — four tickets

Ticket 1 — "Enforce that no commit ever contains a secret." Security wants a hard guarantee, not a suggestion. A junior proposes a pre-commit hook grepping for API-key patterns. Evaluate and design the real solution.

Worked answer: the pre-commit hook is a good first layer but not the guarantee — it's not committed to the repo (new clones lack it), and --no-verify bypasses it, so it can only ever be developer convenience (A1's whole lesson). Layered design: (1) Client feedback — a pre-commit framework (the pre-commit tool, or Husky) with a secret-scanner (gitleaks, detect-secrets), config checked into the repo and installed via a documented bootstrap, so most secrets are caught before they're even committed. (2) The actual gate — server-side enforcement: a pre-receive hook (or the host's push-protection / secret-scanning feature) that rejects the push if a secret is detected, applying to everyone with no bypass. (3) Defense in depth — CI secret-scanning on every PR, and org-wide push protection. State the security truth plainly: any rule that must hold lives on the server, because the client is the attacker's machine. And pair it with the Module 14 reality — if a secret does land, .gitignore won't remove it; rotate + filter-repo.

Ticket 2 — "CI is slow because every job clones the whole 8GB repo." Builds spend minutes cloning history they never use. Redesign the CI checkout.

Worked answer: shallow + partial + sparse, matched to what each job needs. Most CI jobs build one commit and need no history: git clone --depth 1 --filter=blob:none --branch <ref> <url> — shallow (last commit only) and partial (blobs lazy) collapses the transfer to a fraction. For a monorepo where a job touches one service, add --sparse (or git sparse-checkout set <service>) so the working tree materializes only that service — faster checkout and faster status/build-file-scanning. The few jobs that do need history (a job computing git describe, or one that diffs against a base) deepen on demand: git fetch --deepen N or fetch the specific base ref, rather than cloning full up front. Cache the .git between runs where the CI system allows (incremental fetch beats re-clone). Quantify it: 8GB full clone → tens of MB shallow+partial+sparse, minutes → seconds, multiplied across thousands of daily jobs. This is Part C's three techniques deployed together — the standard modern-CI checkout.

Ticket 3 — "git status takes 25 seconds and devs are miserable." A 2-million-file monorepo. Leadership assumes "too much history" and asks about pruning old commits. Diagnose correctly and fix.

Worked answer: correct the premise first — the bottleneck is almost certainly working-tree file count, not history (the counter-intuitive callout, as the ticket's crux). git status stats every file in the working tree; 2 million files is the cost, and pruning commits wouldn't touch it (and Git handles deep history fine with the commit-graph). Diagnose: how many files are checked out, and is core.fsmonitor / commit-graph enabled? Fixes, in impact order: (1) sparse checkout — most devs need a handful of the services; materializing only those can drop the working tree from 2M files to thousands, turning status instant (biggest lever). (2) core.fsmonitor (built-in filesystem monitor) — Git asks the OS what changed instead of stat-ing everything, huge on large trees. (3) git maintenance start — background commit-graph + incremental repack keeps history ops fast. (4) At extreme scale, Scalar (upstreamed) bundles these best-practice settings. Pushing back on "prune history" with "it's file count, here's the data" is the senior move — solving the right dimension.

Ticket 4 — "Write our repository-health runbook." The team wants a standing document: how to keep repos fast and healthy, what to automate, what to check when something's wrong.

Worked answer (the runbook): Automated upkeep — enable git maintenance start (or trust the host's server-side maintenance) for background repacking + commit-graph; don't hand-run gc unless diagnosing. Keep history lean — LFS for binaries from day one (Module 14), CI rejecting large non-LFS files; no secrets, ever (server-side push protection). Enforce with the right layer — policy that must hold goes server-side (pre-receive / protected branches, not client hooks). For big repos — sparse checkout for working-tree size, partial/shallow clone for CI and onboarding, core.fsmonitor + commit-graph for command speed. Health checks when something's wronggit count-objects -vH (bloat), git fsck (corruption; restore from a clone — every clone is a full backup), git gc (manual pack if loose objects piled up), git rev-parse --is-shallow-repository (unexpected shallowness). The meta-rules to pin on top: get storage/ignore/LFS policy right before the first bad commit (retrofitting means history rewrites); enforce on the server, help on the client; and diagnose the right dimension — working-tree file count for command speed, object size for clone speed, history rewrites only as a last resort. This runbook is Modules 1–15 compressed into an operations page.

E3. Documentation reference

TopicOfficial sourceWhat it covers
Git hooksGit Book §8.3 — Git Hooks · githooks manualEvery hook, client and server, with arguments
git gc / packfilesgit-gc manual · Git Book §10.4 — PackfilesPacking, pruning, delta compression, tuning
git fsckgit-fsck manualIntegrity checks, unreachable/dangling, recovery
git maintenancegit-maintenance manualScheduled background upkeep, commit-graph, tasks
Partial & shallow clonepartial-clone manual · git-clone manual--filter, --depth, deepening, promisor remotes

E4. Self-assessment

Answer each aloud, from memory, before moving on. Every one is answered on this page.

  1. Where do hooks live, and why does that location mean client hooks can't be relied on for enforcement?
  2. What does a nonzero exit from pre-commit do, and how does --no-verify interact with it?
  3. Client vs server hooks: which enforces policy for everyone, and why can't the other?
  4. Name a server-side hook for rejecting a push and one for reacting to an accepted push.
  5. What two things does git gc do, and what is a packfile?
  6. Why does a repo's .git sometimes shrink dramatically, and what makes an abandoned commit eventually unrecoverable?
  7. What does git fsck verify, and how can it help recover lost work?
  8. Shallow vs partial vs sparse: which axis does each one cut (history / blobs / working-tree files)?
  9. How do you turn a --depth 1 clone back into a full one?
  10. At monorepo scale, what is the usual bottleneck for git status — and why is "prune old history" the wrong fix?
  11. Name the feature that addresses each: slow status from file count; slow history traversal; huge clone transfer.
  12. State the meta-rule this whole track keeps returning to about when to get storage/ignore/enforcement policy right.

E5. Sources

Interview questions in this module were captured verbatim from: LabEx — Git Interview Questions and Answers (2025) and Interview Coder — 90+ Common Git Interview Questions (Sep 20, 2025). A corpus note: Git hooks are a verbatim published question, but gc/packfiles/fsck, the clone-scaling techniques, and monorepo strategy appear only inside broader large-repo answers — so Parts B, C, and D carry corpus notes rather than fabricated interview blocks (the track's rule throughout: never invent a question and present it as published). Technical claims were verified against the official documentation in E3; every command and output on this page was executed on Git 2.43.0 on Linux, including the hook rejections, gc packing, fsck recovery, and shallow/sparse clones (the latter over file:// so the flags are honored). Object counts, hashes, and paths will differ on your machine.

🗒️ Cheat sheet — Module 15

Command / conceptWhat it does
.git/hooks/pre-commit (executable, exit≠0 aborts)Client hook: local checks before a commit — convenience, bypassable with --no-verify
.git/hooks/pre-receive / update / post-receiveServer hooks: reject/accept a push · per-ref · react (deploy/CI) — real enforcement
git commit --no-verifySkip client hooks (proves they're not a gate)
git gc · git count-objects -vHPack loose objects + prune unreachable · see loose count and sizes
git fsck · --unreachable · --lost-foundVerify integrity · list orphaned objects · recover them (last-resort after reflog)
git maintenance start / stopSchedule background repack + commit-graph + prefetch (non-blocking upkeep)
git clone --depth 1 · git fetch --unshallowShallow (fewer commits) · fill it back to full history
git clone --filter=blob:nonePartial: all history metadata, blobs fetched lazily (needs server support)
git sparse-checkout init --cone · set <dir>Materialize only chosen directories in the working tree (monorepos)
core.fsmonitor · commit-graph · ScalarFast status on huge trees · fast history traversal · bundled scale settings

Key concepts: hooks are scripts in .git/hooks/ firing on events; client hooks (pre-commit, commit-msg, pre-push) give local feedback but aren't committed and are bypassable (--no-verify) — convenience, not enforcement; server hooks (pre-receive/post-receive) enforce for everyone (protected branches are these productized) · git gc packs loose objects into delta-compressed packfiles and prunes unreachable ones — the storage layer under Modules 1/3; git fsck checks integrity (content-addressing = checkable) and recovers orphans · scale-cutting: shallow (--depth, history), partial (--filter=blob:none, blobs), sparse checkout (working-tree files) — combine for CI and monorepos · the monorepo bottleneck is usually working-tree file count (fix with sparse checkout + core.fsmonitor), not history depth · git maintenance automates upkeep · meta-rule: enforce on the server, help on the client, and get storage/ignore/LFS policy right before the first bad commit.

You've reached the end of the track. Fifteen modules ago, a directory of app.conf.final2 files had no history. You can now: reason about Git from its object model (blobs, trees, commits, refs — Module 3) rather than memorized commands; record, read, branch, merge, and rewrite history deliberately; collaborate through remotes, PRs, and workflows; recover from nearly any mistake (reflog, revert, fsck); and operate Git at fleet scale (LFS, hooks, sparse/partial clones, maintenance). Every command in this track was one of four verbs — record, compare, restore, combine — and every behavior fell out of one idea: content-addressed snapshots. Re-read any module's 🎯 interview questions until the "details that separate candidates" are yours, and you're ready for any Git interview a DevOps role can pose.
Back to the hub: Version Control GIT — the roadmap, all fifteen modules, and how to use this track.
Spotted a mistake or want something added? Send me a note.