Linting
Learn how Biome formats and lints the codebase, what bun run lint and bun run verify actually run, and which lefthook hooks fire on commit and push.
Biome is the only formatter and the only general-purpose linter. There is no ESLint or Prettier configuration in the repository.
Everything structural sits in the scripts/check-*.ts harness instead. Biome decides what the code looks like; the harness decides what the code is allowed to do.
package.json composes both into three commands — format, lint and verify — and Lefthook wires two of them to git.
Biome
biome.json pins the 1.9.4 schema and matches the installed @biomejs/biome (^1.9.4).
"formatter": {
"enabled": true,
"lineWidth": 100,
"indentStyle": "space",
"indentWidth": 2,
"formatWithErrors": true
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "asNeeded",
"arrowParentheses": "asNeeded",
"bracketSpacing": true,
"trailingCommas": "all"
}
}For JavaScript and TypeScript that means:
- 100-column lines
- Two-space indent
- Double quotes
- No semicolons unless required
- No parentheses around a single arrow parameter
- Trailing commas everywhere
formatWithErrors means a file that does not parse is still formatted as far as it can be.
JSON is formatted separately, with trailing commas allowed by the parser but never written by the formatter, and comments rejected.
organizeImports is enabled, so import order is Biome's job rather than a convention anyone maintains by hand.
vcs.useIgnoreFile is on with clientKind: "git", so .gitignore is honoured. files.ignore adds three more paths:
**/.next
**/.source
**/woocommerce-product-feeds/**Rules that are deliberately off
The linter runs the recommended set with eleven rules disabled:
noExplicitAny(suspicious)noArrayIndexKey(suspicious)noConfusingLabels(suspicious)noShadowRestrictedNames(suspicious)useExhaustiveDependencies(correctness)noDangerouslySetInnerHtml(security)noNonNullAssertion(style)noBlankTarget(a11y)useAnchorContent(a11y)useValidAnchor(a11y)useAltText(a11y)
Two of them are off because something else owns the concern.
noDangerouslySetInnerHtml is off because the HTML being rendered has already been sanitised through sanitize-html.
noExplicitAny is off because scripts/check-no-as-any.ts owns this instead, and it ratchets.
Turning noExplicitAny off does not make any free. check-no-as-any.ts scans the whole source tree for as any and as unknown as, baselines what exists, and fails on anything new.
The three commands
bun run formatRuns bun biome format --write .. It only formats.
bun run lintRuns biome check --write ., then six named checks, then the lint:cleanliness aggregate.
bun run verifyRuns biome check ., then eleven named checks, then lint:cleanliness, then vitest run, then typecheck.
lint is the working loop. After Biome writes its fixes, it runs:
lint:cachelint:disciplinelint:deadlint:flexlint:critical-testslint:no-as-any- the
lint:cleanlinessaggregate
It is not the full gate. It skips theme boundaries, event completeness and the three visibility checks.
verify is the gate. The difference that matters most is the first leg: it runs biome check . without --write, so an unformatted file fails rather than being quietly fixed.
lint:cleanliness, which both commands run, is itself thirteen scripts:
lint:orphanslint:dead-actionslint:dead-schemalint:field-balancelint:dupeslint:depslint:hackslint:eventslint:cache-tagslint:guaranteeslint:client-boundarylint:client-bundlelint:hygiene
bun run verify:parallelRuns the same legs concurrently through scripts/verify.ts.
Typecheck is separate from Biome, and needs headroom to complete:
"typecheck": "NODE_OPTIONS='--max-old-space-size=12288' tsc --noEmit",
"typecheck:strict": "NODE_OPTIONS='--max-old-space-size=12288' tsc --noEmit --strict --noImplicitAny --noImplicitReturns --noImplicitThis --noUnusedLocals --noUnusedParameters"typecheck:strict is not part of verify.
Git hooks
lefthook.yml defines three hooks. They install once, through the prepare script (lefthook install || true), which Bun runs on bun install.
pre-commit:
parallel: false
commands:
discipline:
run: bun run lint:discipline
critical-tests:
run: bun run lint:critical-tests
unit:
run: bun run vitest runPre-commit is sequential and deliberately excludes tsc, which is the slow leg. It runs the discipline rules, the critical-tests check and the unit suite, so committing stays fast.
pre-push:
commands:
verify:
run: bun run verifyPre-push runs the full verify, adding typecheck and the rest of the checks before anything leaves the machine.
The commit-message hook
commit-msg rejects keyboard-mash subject lines, because they destroy git bisect and git blame archaeology.
The first line must be at least 10 characters and at least 2 words.
msg=$(head -1 {1})
case "$msg" in
Merge*|Revert*|fixup!*|squash!*) exit 0 ;;
esac
words=$(echo "$msg" | wc -w)
if [ ${#msg} -lt 10 ] || [ "$words" -lt 2 ]; then
echo "commit-msg: \"$msg\" is too short — write what changed and why (≥2 words, ≥10 chars)."
exit 1
fiThe four exemptions are prefix matches on generated messages:
Merge
Revert
fixup!
squash!Anything Git or an interactive rebase writes for you passes without the length and word checks.
The hook inspects only the first line, so a body is neither required nor counted.
Lefthook is a local convenience, not the enforcement boundary. Hooks are skipped by
--no-verify and are absent until someone runs bun install, so bun run verify is
what has to pass in CI.
Which command to run
Run format when you only want the file reshaped, and lint while you are working — it fixes formatting in place and runs the cheap structural checks with it.
Run verify before you push. It is the same set of checks CI runs, and it is the one that fails on an unformatted file instead of correcting it.
Run verify:parallel when several things are already broken and you want every failure in one pass.
The rule the hooks encode is worth keeping in mind even with --no-verify in your fingers:
Fast checks belong on commit, the full gate belongs before the code leaves your machine.
Related
Style
Learn the naming, import and file-layout conventions the codebase follows, and the two scripts — check-no-as-any.ts and check-discipline.ts — that fail on a new violation.
Harness
Learn about the 25 CI checks that run on your fork, and the 8-guide rule set behind them.
Commands
Learn what every script in package.json runs — dev, database, the lint checks and the verify gate.
Dependencies
Learn what the 81 runtime dependencies and 26 dev dependencies do, why zsa is patched, and how knip.json and check-unused-deps.ts keep the list from growing dead entries.
Guides
Learn what the eight guides in .claude/guides/ contain, how they hang off .claude/CLAUDE.md, and which one to read before a page, a model, a module or a refactor.