Workflow · Git 2.55 · Conventional Commits 1.0.0 · commitlint 21 · Lefthook 2.1
Commits & Pull Requests
Small, atomic, conventional commits and PRs a reviewer can actually read.
Updated 5 Jul 2026 · CC0
AGENTS.mdrepo rootYou author the version-control history: Conventional Commits, atomic diffs, small single-purpose PRs. "Good" means a git log --oneline that reads like a changelog, every commit builds and passes tests on its own, and every PR is reviewable in one sitting.
Stack
- Git 2.55 (
git --version>= 2.55.0). Use native config-based hooks introduced in 2.54 — a hook is keyed by an arbitrary friendly name, with the trigger set separately:[hook "lint"] event = pre-commit; command = …, i.e.hook.<name>.commandplushook.<name>.event(the multi-valuedeventkey, not the name, picks the moment). Or pointcore.hooksPathat a tracked dir. No wrapper needed for simple gates. Git 3.0 is on the horizon; keep off any removed-in-3.0 behavior (e.g. rely ongit switch/git restore, not overloadedgit checkout). - Conventional Commits 1.0.0 — the message grammar. Every commit conforms.
- commitlint 21.2 (
@commitlint/cli+@commitlint/config-conventional) — enforces the grammar incommit-msg. - Hook runner: prefer Lefthook 2.1 (single Go binary, parallel, one
lefthook.yml) for JS/poly repos. For a zero-dependency Node repo, Husky 9.1 is acceptable. On a Git-2.54+ team, nativecore.hooksPath+ a tracked.githooks/dir replaces both — pick one and only one. - lint-staged 17 — run formatters/linters only on staged files in
pre-commit. - GitHub CLI
gh2.96 — create/review PRs from the terminal (gh pr create,gh pr diff,gh pr checks). - Release automation (pick one, do not mix): release-please 17 or semantic-release 25 for fully commit-driven releases; @changesets/cli 2 for monorepos needing intent files. All three consume Conventional Commits.
- Secret scanner: gitleaks 8.30 in
pre-commitand CI. Use the currentgitleaks gitinterface —detect/protectwere deprecated and hidden in v8.19.
Project conventions
- Config lives at repo root, tracked:
commitlint.config.js(or.mjs),lefthook.yml(or.husky/),.gitleaks.toml,.gitignore,.gitattributes,.github/pull_request_template.md,CODEOWNERS. - Minimal
commitlint.config.js:export default { extends: ['@commitlint/config-conventional'], rules: { 'scope-enum': [2, 'always', ['api', 'ui', 'auth', 'db', 'ci', 'deps']] }, }; - Minimal
lefthook.yml:pre-commit: parallel: true commands: lint: { glob: '*.{ts,tsx,js}', run: 'npx lint-staged' } secrets: { run: 'gitleaks git --pre-commit --staged --redact --no-banner' } commit-msg: commands: lint: { run: 'npx commitlint --edit {1}' } .gitignoreMUST cover.env*(except.env.example),node_modules/,dist/,build/,coverage/,*.log,.DS_Store, IDE dirs. Never negate-ignore a real secret file..gitattributes: set* text=auto eol=lfto normalize line endings; mark generated fileslinguist-generatedand lockfilesmerge=binaryif churn is a problem.- Commit scopes are a small closed set (enforced by
scope-enum), lowercase, matching your top-level modules — not arbitrary free text.
Commit format
Grammar: type(scope): summary, optional body, optional footer.
feat(auth): add refresh-token rotation
Access tokens now rotate on every refresh so a leaked token is
valid for at most 15 minutes. Old tokens are revoked server-side.
Closes #482
- Types (only these):
feat(new capability, → MINOR),fix(bug patch, → PATCH),refactor(behavior-preserving code change),perf(faster, no behavior change),style(whitespace/formatting only, no code-meaning change),docs,test,build(build system/deps),ci(pipeline config),chore(housekeeping, no src/test change). This is theconfig-conventionalset; do not invent types (update,misc,wipare banned). - Scope: optional, one word from the enum, in parens. Omit rather than guess.
- Summary: imperative mood ("add", not "added"/"adds"), lowercase after the colon, no trailing period, <= 72 chars total header. It completes the sentence "This commit will ...".
- Body: optional, separated by a blank line, wrapped at 72 cols. Explain why and any non-obvious tradeoff — never restate the diff. The diff shows what.
- Footer:
BREAKING CHANGE: <desc>(or!after type/scope, e.g.feat(api)!:) → MAJOR bump; issue refsCloses #123,Refs #45;Co-authored-by:for pairs. Per CC 1.0.0 a!alone signals the break and theBREAKING CHANGE:footer MAY be omitted (the subject describes it). Team convention here: still write the footer to spell out the migration path. - One header line == one logical change. If your summary needs "and", split the commit.
Atomicity
- One logical change per commit. A commit must build and pass tests in isolation — a bisect landing on it is never broken.
- Stage selectively with
git add -p(hunk-level) orgit add <path>; never blindgit add -Awhen you touched unrelated things. - Keep refactors mechanical and separate: never mix a
refactor(rename/move) with afeat/fixin the same commit — reviewers can't tell what actually changed. - No
wip,fixup here,misc,stuff, or "address review" as final messages. Iterate locally withgit commit --fixup <sha>, then collapse before pushing:git commit --fixup=abc123 git rebase -i --autosquash origin/main - Formatting-only churn goes in its own
style/chorecommit (or better, is auto-applied by the formatter hook so it never appears in a feature diff). - Lockfile/dependency bumps ride in a
build(deps):commit, separate from feature code.
Branches
- Never commit on
main/master. Branch first:git switch -c feat/short-desc. - Name
type/kebab-description, matching commit types:feat/user-avatar-upload,fix/null-cart-total,chore/bump-vitest. Optionally prefix an issue:feat/482-token-rotation. - Short-lived: rebase onto
maindaily (git fetch && git rebase origin/main) to keep the diff small and conflict-free. Prefer rebase over merge for feature branches — linear history bisects cleanly. - Delete the branch after merge (
gh pr merge --squash --delete-branch).
Pull requests
- Small and single-purpose: target < ~400 changed lines. If it's bigger, split into stacked PRs (
baseof PR 2 = branch of PR 1). - Open as draft while iterating; mark ready only when CI is green and you've self-reviewed.
- Self-review the full diff before requesting review:
gh pr diff— look for stray debug logs, commented code, unrelated reformatting, TODOs, and accidental file additions. - Description states: context/why, what changed, how tested (commands run + result), and screenshots for UI. Link the issue with
Closes #.gh pr create --fill --draft \ --title 'feat(auth): rotate refresh tokens' \ --body 'Why: leaked tokens stayed valid 30d. How tested: `npm test`, manual refresh loop.' - No unrelated reformatting. If the formatter wants to touch lines you didn't change, that belongs in a separate
chore(format):PR, not buried in a feature diff. - Squash-merge so
maingets one Conventional Commit per PR — set the PR title to a valid conventional message (CI lints it). Merge commits and rebase-merge are fine only if every commit is already conventional and atomic. - Watch CI, don't merge red:
gh pr checks --watch.
Hard rules
- Never
git push --forcea shared branch (main,develop, anything others branch from). On your own feature branch usegit push --force-with-leaseonly — it refuses if someone else pushed. - Never commit secrets:
.env, API keys, tokens, private keys,*.pem, connection strings.gitleaks git --pre-commit --stagedmust pass before every commit. If a secret was committed, treat it as leaked: rotate it, then purge history (git filter-repo) — deleting the file in a new commit does not remove it. - Never commit build output or generated artifacts (
dist/,build/,.next/,coverage/) — they belong in.gitignore, not history. - Never
git commit --no-verify/-nto skip hooks, and nevergit rebase --skippast a failing gate. Fix the cause. - Never rewrite public history.
rebase -i/--amendonly on commits you have not pushed to a shared branch. - Never force a merge to bypass required checks. No
--adminmerge to dodge failing CI. - Sign commits: enable
git config commit.gpgsign truewith an SSH or GPG signing key so the "Verified" badge is real.
Testing
- Enforce, don't hope. Hooks are the contract:
commit-msg:commitlint --editrejects a malformed message before it exists.pre-commit:lint-stagedruns typecheck/lint/format +gitleakson staged files only (fast, sub-second on small diffs).pre-push: run the fast unit suite (vitest run --changedor equivalent) so you never push a red branch.
- CI is the backstop, not the first line: it re-lints the commit/PR title, re-runs
gitleaks, and runs the full test suite. Required status checks block merge. - After scaffolding hooks, verify they fire: attempt a bad commit (
git commit -m "bad message") and a staged fake secret, and confirm both are rejected. A hook that silently no-ops is worse than none. - Test the release config on a scratch branch before trusting it on
main: confirm afeat:bumps MINOR andfix:bumps PATCH in the dry-run output.
Security
gitleaks(or GitHub secret scanning + push protection) gates every commit and every push. Enable push protection on the remote so a secret can't reach GitHub even if a local hook is bypassed.- Commit signing enforced by branch protection (
Require signed commits). Configure once:git config --global gpg.format ssh+git config --global user.signingkey <key>. - Branch protection on
main: require PR review, passing status checks, linear history, signed commits, and no force-push / no deletion. - Pin GitHub Actions to a commit SHA, not a floating tag (
uses: actions/checkout@<sha>), so a compromised tag can't inject code into your CI. - Review the
diff, not just filenames, before staging — a copy-pasted config or fixture is the most common way a live credential slips in. - Least-privilege CI tokens: a
permissions:block scoped per job; never a repo-widewrite-all.
Do
- Run
git diff --stagedand read it fully before everygit commit. - Write the body when the why isn't obvious from the summary; skip it when the change is self-evident.
- Rebase your feature branch onto
mainto resolve conflicts before opening/updating a PR. - Use
git switch/git restore(Git 2.23+) over the overloadedgit checkout. - Reference the issue in the footer (
Closes #) so the tracker closes on merge. - Keep the PR description's "how tested" honest — paste the actual command and its result.
- Use
--force-with-lease(never bare--force) when you must rewrite your own branch.
Avoid
- Vague/banned summaries:
fix: bug,update code,misc changes,wip,final,asdf. Every summary names the actual change. - Past-tense or capitalized-with-period headers (
Fixed the login.) — use imperative, lowercase, no period. git add -A/git add .when your working tree has unrelated edits — stage hunks withadd -p.- Mega-PRs mixing refactor + feature + formatting. Split them; reviewers can't verify a 2000-line diff.
- Merge commits from
maininto your feature branch to "update" it (git merge main) — it pollutes history; rebase instead. --no-verifyto "just get it in". If the hook is wrong, fix the hook.- Skipping the body on a breaking change — every
BREAKING CHANGE/!needs migration notes in the footer. - Committing lockfiles and feature code together, or bumping deps inside a feature commit — separate
build(deps):. - Husky's deprecated pre-v9 shell shims (sourcing
husky.sh) — v9 uses plain scripts in.husky/.
When you code
- Propose small, reviewable commits with correct Conventional Commit messages. When a change grows, stop and split it into a commit sequence rather than one blob.
- Before committing: run typecheck + lint + the relevant tests; stage only the files that belong to this logical change; read the staged diff.
- Choose the type deliberately — is this
feat,fix, orrefactor? The type drives the version bump; getting it wrong ships a wrong release. - Before opening a PR: rebase on
main, ensure green local checks, self-reviewgh pr diff, write a description with why + how-tested. - Ask before: force-pushing anything, rewriting shared history, changing the branch/PR strategy, merging with failing checks, or committing a file that looks generated or secret-bearing. When unsure whether a change is one commit or several, default to several.
Drop it in your repo
Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on Git 2.55 · Conventional Commits 1.0.0 · commitlint 21 · Lefthook 2.1.