Git hooks
The dotfiles pre-commit hook checks what you have staged, before Git creates the commit. It catches common mistakes early; it does not replace CI, pre-push validation, or a review for sensitive data.
There are two independent hook systems:
| System | Purpose | Source |
|---|---|---|
| This repository’s pre-commit hook | Check staged changes | hooks/ |
| Personal Git templates | Protect pushes and clean up merged branches in other repositories too | symlinks/config/git/templates/ |
Before committing
Section titled “Before committing”Review the intended commit, then let Git invoke the installed hook:
git diff --cachedgit commitTo check the current index without creating a commit:
sh hooks/pre-commitThese checks do not stage fixes or stash your work. If a file is partially staged,
fixing its working copy is not enough: review and stage the intended fix, then
retry. Do not use git add . as a generic cure.
Default pre-commit flow
Section titled “Default pre-commit flow”The entrypoint runs the sensitive scan first, then Rust/PowerShell checks. A failure stops the commit. Checks are selected by staged paths:
| Staged change | Check |
|---|---|
| Added text in the staged diff | Sensitive-pattern scan |
Rust source, including deletion or rename away from .rs | Whole-crate cargo fmt --check and host Clippy |
| Cargo manifest/lockfile, Cargo config, or toolchain file | The same Rust checks, using the staged compiler pin |
Present .ps1, .psm1, or .psd1 file | PSScriptAnalyzer on those staged files |
| Only TOML data, shell, or workflow files | No Rust compilation solely because of that path; extra guards need full mode |
Clippy uses the ci profile, all targets, a locked dependency graph, and warnings
as errors. The hook checks the compiler against the staged
cli/rust-toolchain.toml; it does not install or switch toolchains.
PowerShell analysis uses the shared analyzer prerequisites. A skipped check is not coverage.
Scan scope
Section titled “Scan scope”The index is the input
Section titled “The index is the input”The sensitive scanner reads the diff from HEAD to the index, or from an empty
tree for the first commit. It scans added lines only. An edited rename is still
scanned without treating every unchanged line as newly added.
Rust and CI guards export the entire index to an isolated snapshot. Consequently:
- An unstaged fix cannot hide a staged error.
- An unstaged deletion does not remove the staged file from checks.
- An untracked file cannot satisfy a staged build.
- The working tree is not overwritten or stashed.
Builds reuse the checkout’s Cargo target cache, so checks still write build artifacts. Shell and PowerShell input handling preserves literal filenames, including spaces and non-ASCII characters; quoted Git display names are not passed to analyzers.
The verifier itself comes from the working tree. The installed entrypoint
finds the checkout with git rev-parse --show-toplevel and runs its hooks/
helpers. Pattern files and analyzer helpers also come from that checkout.
Review and stage verifier changes separately; an index snapshot does not make
the verifier immutable or trusted.
What the sensitive scan can detect
Section titled “What the sensitive scan can detect”Detection rules are case-insensitive extended regular expressions. They match
each added line’s text, so ^ and $ do not anchor to a diagnostic filename or
line number. Findings show file/line locations but redact matched content.
Allowlist matches replace only the safe span before detection. The rest of the line is still scanned: allowing an Action SHA or a package checksum must not hide an appended credential. Invalid detection or allowlist expressions fail the scan, even for an empty diff.
This is not a general secret detector. It does not cover binary data, unchanged history, or every encoded/unknown credential format. Inspect findings locally; do not share raw sensitive input or unsanitized logs. See Secrets.
Full guard mode
Section titled “Full guard mode”Opt into broader, still path-triggered, checks:
DOTFILES_HOOKS_FULL=1 sh hooks/pre-commit# Or for the commit itself:DOTFILES_HOOKS_FULL=1 git committrue and yes also enable the flag.
| Trigger | Additional coverage |
|---|---|
| Rust/build inputs | Windows GNU-target Clippy when installed, then the full Rust test suite |
conf/*.toml or symlinks/ | Typed configuration validation and config_drift |
| Cargo manifest, lockfile, or deny policy | Wildcard dependency guard and cargo-deny bans/licenses/sources when available |
| Shell or hook paths | ShellCheck when available, on affected shell inputs |
| POSIX wrapper or its test script | Linux wrapper tests |
| Release workflow | Staged publishing invariants: provenance, exact commit handoff, permissions, serialization, and attestation publication |
Deletions and both sides of renames participate in trigger selection. Release guards read the staged workflow directly; the other guards use the index snapshot.
You can run sh hooks/check-ci-guards.sh directly for its basic targeted checks.
Without full mode it omits config drift, cargo-deny, and wrapper tests.
Neither invocation runs every CI job or checks the full ci-success contract.
Use Testing for the canonical local stages and
Before pushing for push validation.
Installation and removal
Section titled “Installation and removal”The git-hooks task copies extensionless files from hooks/ into Git’s hook
directory. Currently that means pre-commit, not the .sh helpers or .ini
data. On Unix the installed copy is executable.
Before installing, inspect the destination:
git config --show-origin --get core.hooksPathgit rev-parse --git-common-dirNo output and status 1 from the first command means there is no override.
Dotfiles honors core.hooksPath, resolving relative values from the repository
root. Otherwise it uses the common Git directory’s hooks/, including for
linked worktrees. A global/shared hooks path can affect other repositories.
When you deliberately want to deploy:
dotfiles update --only git-hooks --dry-rundotfiles update --only git-hooksInstall can replace a differing destination hook. Preserve custom hooks
first. A changed copied pre-commit needs reinstallation; helper-only changes
take effect directly from the checkout.
To inspect removal, then apply only after review:
dotfiles remove --only git-hooks --dry-rundotfiles remove --only git-hooksRemoval deletes only hooks matching the current managed source/state. Modified
or mismatched hooks survive. If the configured destination is the source itself,
including through a filesystem alias, install/removal leaves it intact.
Neither operation resets core.hooksPath. Missing hooks/ or Git metadata makes
the task inapplicable.
Application-wide templates
Section titled “Application-wide templates”Git copies template entrypoints when initializing or cloning a repository.
Their shared helpers resolve from the managed template directory at runtime.
These templates are not installed by the repository’s git-hooks task.
Merged-branch cleanup
Section titled “Merged-branch cleanup”post-merge and post-rewrite after a rebase run the shared
clean-merged-branches helper. It considers local branches merged into
origin/HEAD, while preserving:
- The current branch and the default branch.
main,master,dev, anddevelop.- Branches checked out in any worktree.
- Any branch Git refuses to delete safely with
git branch -d.
Without origin/HEAD, cleanup does nothing. An amend does not trigger it.
Existing repositories need a newly introduced template entrypoint, such as
post-rewrite, installed explicitly; do not overwrite a repository-specific
hook to do so.
Push protection
Section titled “Push protection”The template pre-push hook blocks branch updates to main, master, develop,
and the destination remote’s default branch. It prefers that remote’s cached
HEAD, then tries a read-only, noninteractive ls-remote --symref lookup.
If discovery fails, the named protections still apply.
Add exact branch names, not globs, with:
git config --add dotfiles.protectedBranch release/stablegit config --add dotfiles.protectedBranch devGitHub remotes whose URL owner matches github.user (or github.username) are
exempt. This is a local configured-name check, not a GitHub permission check.
It is separate from both CI and the repository’s pre-push validation script.
Diagnosing a failure
Section titled “Diagnosing a failure”| Symptom | Check next |
|---|---|
| “I fixed it, but it still fails” | Compare git diff and git diff --cached; stage only the intended fix |
| Formatting failure | Run cargo fmt in cli/, review the diff, and restage the appropriate hunks |
| Build fails only in the hook | Look for an unstaged or untracked source, dependency, or config needed by the staged code |
| A check was skipped | Read the missing-tool reason and run the required coverage separately |
| Entrypoint edits have no effect | Inspect the real hook destination; reinstall the copied entrypoint deliberately |
| No hook runs | Check core.hooksPath, the installed filename, and the Unix executable bit |
| Sensitive finding | Inspect the staged line locally; remove/rotate a real secret or test a narrow safe-span exception |
Changing the checks
Section titled “Changing the checks”| File | Responsibility |
|---|---|
pre-commit | POSIX sh orchestration |
check-sensitive.sh | Added-line scanning and redacted diagnostics |
sensitive-patterns.ini | Detection expressions |
sensitive-allowlist.ini | Safe-span exceptions |
check-rust.sh | Staged Rust and PowerShell checks |
check-toolchain.sh | Staged compiler-pin verification |
check-ci-guards.sh | Targeted configuration, dependency, shell, wrapper, and release guards |
Keep scripts POSIX sh compatible and preserve partially staged input,
rename/deletion handling, unusual filenames, and empty-diff behavior.
Installation lives in git/hooks.rs and
resources/hook.rs.
Changing sensitive patterns
Section titled “Changing sensitive patterns”Use synthetic fixtures, never live credentials. Test both a detection and the intended safe case. Anchor an exception to its surrounding syntax, redact the smallest safe span, and prove that a secret elsewhere on the same line is still detected.
Existing exceptions include Action SHA pins, Cargo/PKGBUILD checksums, fixture
addresses at test.local, and GitHub SSH transport syntax. Documentation
addresses at example.com are not generally allowlisted.
Running the hook test suite
Section titled “Running the hook test suite”Follow Wrapper and hook tests. The full integration suite creates commits and can leave a fixture committed when a test fails to block it. Run it only in the prescribed fresh disposable repository, never this checkout, even when clean. A dirty-tree guard is not isolation.
Bypassing
Section titled “Bypassing”git commit --no-verify skips the entire pre-commit safeguard, not just one
false positive. It does not skip CI. Reserve it for a deliberately reviewed
recovery from a broken hook, never to commit known sensitive data. Prefer fixing
the check or adding a narrow, tested exception.