Mechanical Enforcement
Rules a reviewer would otherwise have to remember belong in a linter. This skill is the curated catalogue of rules, the linters that enforce them, and the rationale for each, so a new project is hardened without re-deriving the set.
A content skill, not a tool: rules and snippets. Wiring them into git hooks is the hk skill's job.
Principles
- Mechanical over social. If a rule relies on a reviewer remembering it, it will drift. Encode it in a linter, a type, or a test - never in a convention.
- Types first, lint second, tests third. Prefer
strictTypeScript / Pydantic / clippy to a custom lint rule. Reach for a lint rule when the type system can't express it. Reach for a test only when neither can. - Architectural boundaries are linter rules. Layers (domain <- infra, UI <- schemas) are enforced with
no-restricted-importsor a graph check when the rule is transitive, never trusted to vigilance. - Auto-fix where possible, gate where not. Formatters and whitespace fixers run with
fix = trueand re-stage. Correctness rules gate the commit. - Own the rule list where the rules are the point, take a preset where they are not.
@commitlint/config-conventionalfor commits andnext/core-web-vitalsfor Next are presets worth inheriting; the lint set that backs an idiom is a config this catalogue owns, because a preset that silences one of those rules does it silently. Override either only with a comment saying why. - The why lives with the rule. Every non-obvious override has an inline comment saying what would break if it were removed.
- The diagnostic is part of the gate. A rule that says what is banned but not what to use instead costs a correction round - for a reviewer and for an agent. Where the linter takes a custom message, name the replacement and where it lives.
When to use this skill
- New project → pick from the table below, read that stack's reference, copy its drop-ins, wire with
hk. - Existing project → audit against the stack reference, add what is missing, ratchet what lands red.
- A bug just happened → "what rule would have caught this mechanically?" (Adding a new rule).
- Unfamiliar stack → the picks table.
Picks by stack
The table names the defaults. The Read column is where each pick is justified, its traps listed and its hk tier map kept - read it before configuring anything.
| Stack | Format | Lint | Type-check | Read |
|---|---|---|---|---|
| TypeScript / JS | oxfmt | oxlint (+ type-aware via tsgolint); knip; a ts-morph architecture test; ESLint only behind a TS 6 alias | tsc -p (TS 7) | references/typescript.md (Picks) |
| Python | ruff format | ruff; import-linter; ast-grep; deptry; vulture; complexipy | basedpyright recommended | references/python.md (Picks) |
| Rust | rustfmt | clippy -D warnings; cargo-deny; cargo-machete | cargo check | references/rust.md (Picks) |
| Nix | nixfmt | deadnix + statix | nix eval of every host's .drvPath | references/nix.md (Picks) |
| Shell: sh, bash, zsh, PowerShell | shfmt per dialect; Invoke-Formatter | ShellCheck per dialect; zsh -n; PSScriptAnalyzer | - | references/shell-quality.md (Picks) |
| Go | gofmt / gofumpt | golangci-lint | go vet | references/other-stacks.md (Go) |
| SQL; Postgres migrations | sqruff | sqruff; squawk | - | references/other-stacks.md |
| CSS / SCSS | Biome or oxfmt | stylelint | - | references/other-stacks.md |
| HTML | - | html-validate | - | references/web-delivery.md (HTML conformance) |
| Markdown; YAML; TOML | rumdl; oxfmt; taplo | rumdl; yamllint; taplo | - | references/other-stacks.md |
| Commit messages | - | commitlint | - | Commit messages, below |
| Comment & doc content | - | Vale (existence styles); windbag is a watch | - | references/comment-hygiene.md |
| Secrets; typos; GitHub Actions | - | gitleaks; typos; zizmor + actionlint | - | Secrets & CI hardening, below |
| API / event contracts | - | buf breaking; oasdiff; graphql-inspector; cargo-semver-checks; api-extractor | - | references/contract-gates.md; references/architecture-boundaries.md (Boundary contracts) |
| Code duplication | - | jscpd | - | references/complexity.md (Duplication) |
| Custom rules / SAST | - | Opengrep; ast-grep | - | references/architecture-boundaries.md (Greppable invariants) |
Framework single-file components (.astro / .vue / .svelte) have no row: the Rust JS linters misread their templates - see references/typescript.md (Framework single-file components).
Locale spell-checker caveat. A locale-rewriting spell hook (typos en-gb, aspell) auto-"fixes" US-spelled external identifiers inside string literals - CLI flags (--flavor), protocol names (authorization), CSS keywords (color, center), schema.org types, API fields - and silently breaks the build or the wire. Allow-list the class proactively (a fixed word list rots as the dictionary drifts) and re-check string literals after any auto-fixed commit. Wiring: hk [default.extend-words], hk/references/builtins-by-language.md.
Rules catalogue
Rules are organised by concern, not by linter: what it prevents, how to encode it, known exceptions. The routing table at the end says which reference owns which concern; only the two every repo has stay inline.
Secrets & CI hardening
| Rule | Encode with | Prevents |
|---|---|---|
| No committed secrets | gitleaks pre-commit step | Token leaks |
| Every environment variable the app reads is declared | A committed .env.schema (varlock) with @defaultSensitive=true, and varlock load && varlock audit in the check command | An undeclared or drifted variable: load fails the gate when the environment does not satisfy the schema, audit fails it when code reads a name the schema does not declare |
| One policy value spelled across many configs stays in agreement | Custom checker: one expected constant, one (file, regex, unit) row per config, normalise units, fail on drift; glob the step on exactly those files | Silent policy forks - a quarantine hand-encoded in nine files across four time units, where editing one quietly weakens the rest |
| Every dependency build script has a recorded decision | Custom checker reading installed manifests against the allowlist (pnpm: allowBuilds in pnpm-workspace.yaml); glob on package.json, lockfile, workspace YAML. Ready-made: the hk skill's assets/pnpm-build-scripts-check.mjs | A CI-only ERR_PNPM_IGNORED_BUILDS that a global ignoreScripts hides locally. Assert statically; the reproduction command is itself a de-protection |
No --no-verify, and no other hook-manager kill switch | Project AGENTS.md for the human lane; for the agent lane a pre-execution tool gate that returns a deny or ask - Claude Code PreToolUse, pi tool_call, Codex PreToolUse, opencode tool.execute.before. references/agent-tool-gates.md | Bypassing the whole gate. Preventable for an agent's shell tool, never for a human terminal; the flag is one of eight spellings a correct policy enumerates |
| Workflows are correct | actionlint (hk builtin) | Expression type errors, a broken needs: graph, unknown runner labels; it shells out to an installed ShellCheck for run: blocks rather than embedding one |
| Workflows are pinned and safe | zizmor (gate on exit ≥ 11; SARIF or --format=github) | unpinned-uses, dangerous-triggers (pull_request_target / workflow_run), template-injection into run:, excessive-permissions, impostor commits, typosquatted actions, dependabot-cooldown shorter than 7 days, dependabot-execution |
| SHA-pins stay fresh, not stale | Dependabot package-ecosystem: github-actions with cooldown: { default-days: 7 } | Pinned actions rotting unpatched, and a freshly-compromised release auto-bumping before the community catches it - the Actions arm of the release-age quarantine |
Run zizmor and actionlint together - minimal overlap, both static. agent-ci executes a workflow locally in the real runner image: a dynamic complement, not a linter. Dependency supply-chain posture (quarantine keys and units, install-script blocking, osv-scanner, provenance, the Actions-cooldown bug) is the supply-chain-hardening skill's; this skill keeps the linter-shaped controls above and the config-drift-checker pattern.
Commit messages
// commitlint.config.js
export default { extends: ["@commitlint/config-conventional"] };
Wire via hk's commit-msg hook (see references/hk-steps.pkl). Nothing else to configure.
Composition with the hk skill
This skill gives you what to enforce; the hk skill gives you how to wire it. The generic ladder - each stack reference's Picks section maps its tools onto it:
tier 1 (format/fix) → trailing-whitespace, newlines, typos, the stack formatter (fix = true, re-staged)
tier 2 (lint/gate) → the stack linter incl. boundary and purity rules, gitleaks, yamllint, check-merge-conflict, zizmor --offline + actionlint (glob .github/workflows/*.{yml,yaml}, action.yml)
tier 3 (typecheck) → the stack type checker
tier 4 (deps/dead code/test) → dependency and dead-code gates, the test runner scoped to changed paths by glob
CI / pre-push → baseline-diff contract gates (buf breaking / oasdiff / cargo-semver-checks / api-extractor), squawk on migrations/**/*.sql, publish gates, audits
commit-msg → commitlint
Baseline-diff gates run at pre-push / CI, not pre-commit: they need the base ref. Use fix = true + stash = "git" on pre-commit so tier 1 auto-fixes and re-stages. Worked example: references/hk-steps.pkl.
Adding a new rule
When a bug escapes to review or production, the retro question is: what rule would have caught this mechanically?
- Identify the smallest AST pattern, import, or type flag that expresses the rule.
- Pick the linter that already owns that concern (the picks table).
- Add it, with an inline comment explaining the failure mode it prevents. Where the rule takes a custom message, make the message name the replacement and where it lives, not just the ban.
- Record it with the same rationale in the matching
references/stack file (inline above only for a cross-stack concern), and as a drop-in if it is a new type of rule.
Ratcheting a gate onto non-conforming code
A hard threshold (complexity cap, coverage floor, a new no-restricted-* or graph rule) fails the whole build the day it lands on a codebase that already violates it - so it gets reverted, or slackened to a ceiling that governs nothing. Ratchet instead (Ford/Parsons): record today's violations as a committed baseline, fail only new ones, shrink the baseline deliberately, and never refresh it in CI. The vehicle per tool and the traps in each: references/ratcheting.md.
Complexity gates are the archetype. If the number you want produces more than a couple of dozen violations, it is a refactoring backlog, not a gate - carry it as CI reporting until the backlog shrinks. A number chosen to make today's worst file pass is not a gate.
References
| When the task involves… | Read |
|---|---|
| A TypeScript / JS project: strictness, type check, lint families, formatting, hygiene, dead code | references/typescript.md; drop-ins typescript-strict-app.jsonc, typescript-strict-lib.jsonc, typescript-oxlintrc.jsonc, typescript-oxfmtrc.jsonc, typescript-ast-grep.yml, knip.jsonc |
| TypeScript test gates: vitest keys, test lints, runtime backstops, bun test | references/typescript-testing.md; drop-ins typescript-vitest.config.ts, typescript-vitest-universal-setup.ts, typescript-vitest-setup.ts |
| TypeScript dependencies and publishing: lockfile, licences, build, publint, attw, the consumer smoke | references/typescript-publishing.md; drop-in typescript-publish-gates.sh |
| TypeScript security: prototype pollution, ReDoS, the parse boundary, runtime flags, sinks | references/typescript-security.md; drop-in typescript-ast-grep.yml |
| A Python project | references/python.md; drop-ins python-ruff.toml, python-typecheck.toml, python-vulture.toml, python-import-linter.toml, python-purity.toml, python-ast-grep.yml, python-pytest.toml, python-deptry.toml |
| A Rust workspace | references/rust.md; drop-ins clippy-thresholds.toml, rust-workspace-lints.toml, cargo-deny.toml |
| Nix | references/nix.md |
| Shell: sh, bash, zsh, PowerShell | references/shell-quality.md |
| Go, SQL, Postgres migrations, CSS / SCSS, Markdown, YAML, TOML | references/other-stacks.md; Go complexity drop-in golangci-complexity.yml |
| Layer and graph boundaries, greppable invariants, purity, contract gates (any stack) | references/architecture-boundaries.md; drop-ins typescript-arch-test.ts, dependency-cruiser.cjs, eslint-boundaries.mjs, purity-boundaries.mjs; command patterns in references/contract-gates.md |
| Complexity and duplication: what to gate on, the numbers, off-by-default traps, jscpd | references/complexity.md |
| Gating the agent's own tool calls: per-harness hook contracts, ask versus deny, the VCS bypass surface, testing a guard | references/agent-tool-gates.md |
| Comments and docs that narrate a change or hedge instead of stating the rule | references/comment-hygiene.md |
| Adopting a gate on a codebase that already violates it | references/ratcheting.md |
| Web delivery: runtime a11y, HTML conformance, structured data, Open Graph, broken links | references/web-delivery.md |
| Wiring the tiers into hk | references/hk-steps.pkl; commitlint.config.js; the optional Biome preset biome-ultracite.jsonc |
External: hk (git hook manager), Ultracite (an optional React/Next preset over the oxc stack, with the traps references/typescript.md lists).