Code Quality
Git hooks, linting, formatting, and commit conventions.
Overview
ZeroStarter enforces code quality through automated tools that run on every commit. This ensures consistent code style, catches errors early, and maintains a clean commit history.
Tools
| Tool | Purpose |
|---|---|
| Lefthook | Git hooks manager |
| lint-staged | Run linters on staged files |
| Oxlint | Fast JavaScript/TypeScript linter |
| Oxfmt | Fast code formatter |
| Commitlint | Commit message linter |
Git Hooks
Git hooks are configured in lefthook.yml:
pre-commit:
piped: true
commands:
lint-staged:
run: bunx lint-staged --verbose
stage_fixed: true
build:
run: bun run build
use_stdin: true
commit-msg:
commands:
commitlint:
run: bunx commitlint --edit {1}
pre-push:
commands:
audit:
run: bun audit --audit-level high
only:
- ref: canary
ensure-main:
run: bun .github/scripts/ensure-remote-main.ts {1}Pre-commit Hook
Runs before each commit in sequence (piped):
- lint-staged: Format and lint only staged files. The command runs with
stage_fixed: true, so any files the formatter rewrites are re-staged automatically and the fixes are included in the same commit. - Build: Verify the project builds successfully
Pre-push Hook
Runs before git push. The security audit (bun audit --audit-level high) runs here, scoped to canary (only: ref: canary), so it checks for vulnerable dependencies before the branch leaves your machine without blocking offline commits. It also runs ensure-main (.github/scripts/ensure-remote-main.ts), on GitHub remotes only. Your first git push origin canary creates canary, which GitHub makes the default branch; on your next push the hook seeds main (a separate ref, so it never collides with the canary push) so auto-canary-into-main opens the release PR. It needs no gh or repo-admin (the default branch falls out of push order); the only manual step it prints is enabling read-write Actions permissions. A local per-remote git config zerostarter.mainSeeded.<remote> marker makes it a no-op once done.
Audit overrides (AUDIT.md)
bun audit --audit-level high is the gate that decides whether a vulnerable dependency blocks a push. When an advisory can only be resolved with a dependency overrides entry in package.json (for example a transitive package that has no patched release yet), that override must be recorded in AUDIT.md at the repo root.
AUDIT.md is the canonical record of every active override. Each entry documents the advisory, why an override is needed, the residual risk, and the exit criteria for removing it. The file is intentionally kept even when there are no active overrides, so do not delete it.
Commit-msg Hook
Validates commit messages follow conventional commit format.
Lint-Staged Configuration
The .lintstagedrc.json defines what runs on staged files:
{
"*": ["oxfmt --no-error-on-unmatched-pattern", "oxlint --no-error-on-unmatched-pattern"],
"package.json": ["bun .github/scripts/deps-manager.ts"]
}- All files: Format with Oxfmt, then lint with Oxlint
- package.json: Run dependency manager script (ensures consistent dependency versions)
Formatting with Oxfmt
Oxfmt configuration in .oxfmtrc.jsonc:
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"semi": false,
"experimentalSortImports": {},
"experimentalTailwindcss": {},
"ignorePatterns": ["**/*.lock", ".agents/**", ".claude/**"],
}| Setting | Value | Description |
|---|---|---|
semi | false | No semicolons (like Prettier with semi: false) |
experimentalSortImports | {} | Auto-sort imports |
experimentalTailwindcss | {} | Sort Tailwind CSS classes |
ignorePatterns | ["**/*.lock", ".agents/**", ".claude/**"] | Skip lock files and AI skill directories |
Manual Formatting
# Format all files
bun run format
# Check formatting without changes
bun run format:checkLinting with Oxlint
Oxlint runs automatically via lint-staged on staged files at commit time. To lint the entire monorepo in a single pass, run bun run lint (this is also what CI runs):
# Lint the entire tree
bun run lintOxlint is significantly faster than ESLint while catching common issues.
Oxlint configuration in .oxlintrc.jsonc:
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": ["**/*.lock", ".agents/**", ".claude/**"],
}| Setting | Value | Description |
|---|---|---|
ignorePatterns | ["**/*.lock", ".agents/**", ".claude/**"] | Skip lock files and AI skill directories |
Commit Message Convention
Commits must follow Conventional Commits:
<type>(<scope>): <subject>
<body>Types
Commitlint extends @commitlint/config-conventional, which accepts these 11 Conventional Commits types:
| Type | Description |
|---|---|
feat | New feature |
fix | Bug fix |
docs | Documentation only |
style | Code style (formatting, semicolons) |
refactor | Code change that neither fixes a bug nor adds a feature |
perf | Performance improvement |
test | Adding or updating tests |
chore | Maintenance (dependencies, build scripts) |
ci | CI/CD changes |
build | Build system or external dependency changes |
revert | Reverting a previous commit |
Examples
feat(auth): add Google OAuth provider
fix(api): handle null user in session middleware
docs(readme): update installation instructions
chore(deps): bump dependencies to latest versions
refactor(web): extract form validation into hookBreaking Changes
Append ! after type for breaking changes:
feat!: remove deprecated API endpointsRunning Checks Manually
# What CI runs (auto-check-build.yml). CI exports NODE_ENV=production and
# SKIP_ENV_VALIDATION=true so the build runs before real secrets are present.
NODE_ENV=production SKIP_ENV_VALIDATION=true bun audit --audit-level high
NODE_ENV=production SKIP_ENV_VALIDATION=true bun run lint
NODE_ENV=production SKIP_ENV_VALIDATION=true bun run build
# Type checking (run locally; not part of the CI build workflow)
bun run check-types
# Format all files
bun run formatUnlike CI, the local pre-commit build step runs without SKIP_ENV_VALIDATION, so it validates your real .env. A missing or invalid required variable will fail the commit locally even though the same build passes in CI.
CI/CD Integration
GitHub Actions runs these checks on every PR:
# .github/workflows/auto-check-build.yml
- name: Audit, Lint, and Build
run: |
bun audit --audit-level high
bun run lint
bun run build
env:
NODE_ENV: production
SKIP_ENV_VALIDATION: trueSkipping Hooks
In rare cases, you can skip hooks:
# Skip all hooks (use sparingly!)
git commit --no-verify -m "emergency fix"
# Skip all Lefthook hooks (LEFTHOOK=0 disables every hook, not just one)
LEFTHOOK=0 git commit -m "message"
# Skip a single command, e.g. just the build step
LEFTHOOK_EXCLUDE=build git commit -m "message"Only skip hooks for emergencies. CI will still catch issues.
Troubleshooting
Hooks Not Running
# Reinstall hooks
bunx lefthook installFormat Conflicts
If formatting creates unexpected changes:
# Format entire codebase
bun run format
# Stage formatted files
git add .Commitlint Failing
Check your message follows the format:
# Good
git commit -m "feat: add user profile page"
# Bad (missing type)
git commit -m "add user profile page"
# Bad (wrong type)
git commit -m "added: user profile page"