What happens when you commit
Before a single change reaches the remote, git hooks run on your machine on every commit. They
are installed by husky when you run npm run setup — a bare
npm install wires nothing, because the repo blocks npm lifecycle scripts. Two hooks do the work:
pre-commit and commit-msg. When one of them blocks you, how to read a rejected
commit is the map.
The pre-commit hook
pre-commit runs lint-staged, which applies checks to
your staged files only — so the cost scales with your change, not the repository. What runs
depends on the file type. With the default configuration:
graph TD
Commit["git commit"] --> Staged{"staged file types"}
Staged -->|every file| SECRETS["scan for credentials"]
Staged -->|docs / markdown| MD["regenerate TOC, format, lint markdown"]
Staged -->|JavaScript / LWC| JS["format, lint, run related Jest tests"]
Staged -->|Apex| APEX["format, lint (PMD)"]
Staged -->|XML / YAML / JSON / CSS| OTHER["format"]
SECRETS --> Build["npm run validate (build check)"]
MD --> Build
JS --> Build
APEX --> Build
OTHER --> Build
Build --> Done["commit proceeds"]
Worth knowing:
- The credential scan is the one step that ignores file type: every staged file is matched
against the secret rules, and a hit on a line the commit introduces blocks the commit. It is the
only hook step without an
.opt-outkey, because the same credential would be caught by the pull-request scan a few minutes later, when it is already on the remote and has to be rotated. Prevention here is cheap; rotation is not. - The JavaScript step runs only the Jest tests related to the files you changed
(
--findRelatedTests), so it stays fast. - After lint-staged,
npm run validateruns a build check. Lint and tests are not repeated there — lint-staged already ran them per file — so the pre-commit pass does not lint or test the whole project. - A large commit is linted in concurrent chunks so that a thousand staged files take minutes, not an hour; one failing chunk cancels the rest. Reading that output is covered in how to read a rejected commit.
Lint failures are measured against a baseline
npm run lint does not demand a violation-free project. code-analyzer/code-analyzer.yml carries a
generated baseline: for each folder and rule, a cap on how many pre-existing violations it may
hold. A run passes while every folder stays at or under its caps and fails as soon as one goes over —
so existing debt is tolerated, new debt is not. Caps match recursively, so a parent-folder entry
already covers its descendants.
The baseline is meant to move in one direction. Regenerating it (npm run lint:createbaseline)
records whatever the scan currently finds, which after genuine fixes means smaller caps, and the
removed violations can then never creep back without failing the check. Regenerating on a red run
would record the new violations too — which is why the Update Code Analyzer Baseline pipeline is
scheduled against the default branch, where only green runs have merged: on that branch a
regeneration can only tighten, so the schedule is an automatic ratchet rather than a loophole. The
steps are in how to baseline the code analyzer.
Not every check is wired in: .opt-out
Every hook step is individually switchable through the .opt-out file at the repository root — one
key per line; a listed key removes the corresponding step from the hook entirely. The project ships
with two steps opted out by default:
createapextestsuiteprecommit— regenerating theCiTestSuiteApex test suite from the project's test classes. See curated test suites vs RunRelevantTests for what that suite is used for.testapexprecommit— running Apex tests on commit.
Both are org-touching: Apex tests execute against your default org, so leaving them on would make
every Apex commit assume an authenticated org and pay a per-commit org round-trip. The default
therefore keeps commits fast and org-independent; the pipeline still runs the Apex tests where an
org is guaranteed. Removing the keys from .opt-out turns the steps back on for projects that want
the earlier feedback.
The same mechanism reaches beyond the pre-commit hook: keys exist for the formatting and lint steps,
for the sub-steps of npm run lint and npm run test, and even for the commit-message check. The
full key list is in the opt-out keys reference.
A failing check aborts the commit; the staged files stay staged, and the same git commit runs the
checks again once the cause is fixed.
Bypassing the hooks only moves the failure
git commit --no-verify skips both hooks. What it does not skip is the pipeline: the same gates —
and the org-touching ones the hooks leave out — run on every push and pull request, so a bypassed
local rejection resurfaces as a CI failure minutes later, where it is slower to read and, for a
credential, already on the remote. The local hooks are the cheap place to be told.
The commit-msg hook
commit-msg runs commitlint against your message, enforcing the
Conventional Commits format defined in .commitlintrc.js. A
message that does not conform aborts the commit. This check too honours .opt-out (key
lintcommitmsg), though it is on by default. npm run cm opens a
Commitizen prompt that assembles a conforming
message interactively.
Why it runs locally at all
The hooks give you fast feedback on your own machine, before you push or open a pull request. They are the first gate in a chain: the pipeline re-runs these checks (and the org-touching ones the hooks skip) on every push and PR. A clean local commit is therefore unlikely to bounce later — which is the whole point of running them here first.