Auditing local Git configuration drift Jump to heading

Configuration that was correct in March is not necessarily correct in September. Machines get rebuilt, tools rewrite files, a pairing session leaves someone else’s identity behind, and a repository cloned in a hurry never got the setup command. None of it announces itself β€” the first symptom is usually a hook that did not run or a commit with the wrong author. An audit turns that into something noticed at the right time, within Git configuration management at scale.

When to use this approach Jump to heading

  • Team configuration exists and you want to know whether it is in effect.
  • Hooks are relied upon and you cannot tell who has them installed.
  • Someone’s commits recently arrived with the wrong identity.
  • Machines are rebuilt or reimaged regularly.
  • If nothing in your setup depends on local configuration, there is nothing to drift.

Step 1 β€” Decide what is policy and what is preference Jump to heading

Auditing preferences is how an audit gets ignored. Check only the settings where a wrong value produces a wrong result.

# scripts/audit-config β€” the policy set, and nothing else.
POLICY="
core.hooksPath|.githooks|hooks would not run
merge.conflictStyle|zdiff3|conflicts hide the common ancestor
fetch.prune|true|deleted branches linger as stale refs
rebase.updateRefs|true|stacked branches break on rebase
"
# Verification: every entry answers 'what goes wrong if this is unset?'
printf '%s\n' "$POLICY" | awk -F'|' 'NF==3 {print $1 " β†’ " $3}'
What belongs in an audit and what does notA setting whose wrong value changes what is committed, or silently disables a check, belongs in the audit. A setting that only affects how output looks to one person does not, and including it trains people to ignore the report.Worth auditingLeave alonecore.hooksPathchecks silently skippedβ€”user.emailwrong author in historyβ€”core.pagerβ€”personal tastealiasesβ€”nobody else affectedan audit that flags aliases is an audit people learn to close

Step 2 β€” Write a check that reports rather than nags Jump to heading

#!/usr/bin/env sh
# scripts/audit-config β€” prints a short report; exits non-zero only on real problems.
set -eu
fail=0

check() {
  key=$1; want=$2; why=$3
  have=$(git config --get "$key" 2>/dev/null || echo 'UNSET')
  if [ "$have" = "$want" ]; then
    printf '  ok    %-22s %s\n' "$key" "$have"
  else
    printf '  DRIFT %-22s %-12s (want %s β€” %s)\n' "$key" "$have" "$want" "$why"
    fail=1
  fi
}

echo "configuration audit for $(git rev-parse --show-toplevel)"
check core.hooksPath      .githooks "hooks would not run"
check merge.conflictStyle zdiff3    "conflicts hide the common ancestor"
check fetch.prune         true      "stale remote refs accumulate"
check rebase.updateRefs   true      "stacked branches break on rebase"

email=$(git config --get user.email || echo UNSET)
case "$email" in
  *@acme.com|*@users.noreply.github.com) printf '  ok    %-22s %s\n' user.email "$email" ;;
  *) printf '  DRIFT %-22s %s (expected a work address)\n' user.email "$email"; fail=1 ;;
esac

[ "$fail" -eq 0 ] && echo "  all policy settings in effect" || echo "  run ./scripts/setup to fix"
exit "$fail"
# Verification: the audit passes on a correctly configured clone
sh scripts/audit-config

Step 3 β€” Run it where people already are Jump to heading

An audit nobody runs finds nothing. The two places it costs nothing are a hook and the pipeline.

# .githooks/pre-push β€” cheap, occasional, and at a moment where a fix is convenient
if ! sh "$(git rev-parse --show-toplevel)/scripts/audit-config" >/tmp/audit.txt 2>&1; then
  cat /tmp/audit.txt >&2
  echo "pre-push: configuration drift detected β€” pushing anyway." >&2
  # Deliberately does not block: drift is a problem, not an emergency.
fi
# Verification: the report appears on push without blocking it
git push --dry-run origin HEAD 2>&1 | head

Not blocking is a deliberate choice. Blocking a push over a configuration setting produces a rushed workaround; reporting it at a moment when the person has thirty seconds produces a fix.

How loudly should drift be reported?A setting that silently disables a check deserves to fail loudly, because its absence means other guarantees are not holding. A cosmetic difference deserves a line in a report. Most policy settings sit in between and belong in a non-blocking warning.What does this drift actually cause?checks silently skippedFailother guarantees are voidwrong data committedWarn loudlyfixable, but act nowcosmetic differenceReport onlyno urgencymatching volume to consequence is what keeps the report credible

Step 4 β€” Report the fleet, not just one clone Jump to heading

One person’s clone is a sample of one. A short aggregation tells you whether the setup process is working.

# Each machine appends one line to a shared file, via the pre-push hook
report=$(printf '%s|%s|%s|%s' \
  "$(git config --get user.email || echo unset)" \
  "$(git --version | awk '{print $3}')" \
  "$(git config --get core.hooksPath || echo unset)" \
  "$(date -u +%Y-%m-%d)")
echo "$report" >> "$HOME/.cache/git-audit.log"
# Or, without collecting anything centrally: ask once, tally the answers
# A one-command report people can paste:
printf '%s | git %s | hooks=%s | style=%s\n' \
  "$(git config --get user.email)" "$(git --version | awk '{print $3}')" \
  "$(git config --get core.hooksPath || echo unset)" \
  "$(git config --get merge.conflictStyle || echo unset)"

SAFETY WARNING β€” do not collect configuration reports centrally without saying so. A Git configuration contains identities, key paths, remote URLs and sometimes credential helper settings, so quietly gathering it from developer machines is surveillance regardless of intent. Ask people to run a command and share the output, or collect only an anonymous pass/fail count.

What a first audit across a team usually showsMost clones are correct. The common failures are a hooks path that was never set because the setup step was skipped, and an identity left behind by a pairing session or a machine rebuild.clones by audit resultall policy settings correct14hooks path unset5identity wrong3client below the floor2five clones running no hooks is five people whose local checks were never happening

Step 5 β€” Fix the cause, not just the instance Jump to heading

Recurring drift means the setup process has a gap. Treat each finding as evidence about the process.

# The three questions worth asking about any recurring drift:
#   Was the setup step ever run on this clone?      β†’ make setup unavoidable
#   Did a tool overwrite the value?                 β†’ move it to attributes
#   Did the value change and nobody re-ran setup?   β†’ make the audit noisier
# Making setup harder to skip: run it from the package manager's install step
npm pkg set scripts.prepare='./scripts/setup'
# Verification: a fresh clone is configured after a plain dependency install
git clone "$(git remote get-url origin)" /tmp/fresh && (cd /tmp/fresh && npm ci >/dev/null && sh scripts/audit-config)

Hooking setup to the dependency install is the single most effective change available, because it turns a step people must remember into one that happens as a side effect of something they already do. It is also how hook managers install themselves, which is why hooks tend to be present on machines where other configuration is not.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Should the audit fail a build? Jump to heading

Only for settings whose absence means a guarantee you rely on is not holding β€” an unset hooks path, for example, means local checks are not running at all. For everything else, a report is more effective, because a failing build over a cosmetic setting teaches people to look for the flag that disables the audit.

How often should it run? Jump to heading

On push is frequent enough to catch drift within a day and rare enough to stay unobtrusive. Running on every commit produces a message people stop reading within a week, which is the outcome to avoid.

What if someone disagrees with a policy setting? Jump to heading

Then it is a pull request against the committed configuration, with a reason. That is the main advantage of keeping policy in a file rather than in documentation: disagreement produces a discussion and a decision rather than a quiet local override nobody knows about.