name: quality-stack-setup description: Use when setting up a repo's lint / static-analysis / test tooling — wiring lefthook git hooks (pre-commit auto-fixers + pre-push full verify) and a justfile that is the single source of truth both the hooks and CI call, so local checks and CI can't drift. Language-agnostic: ruff/skylos for Python, prettier/eslint/fallow for TS/JS, same pattern for any language. Installs the tooling; ongoing operation lives in quality-workflow (/quality-audit, /quality-unblock, /quality-verify-hook) and the standing suppression rule in suppression-discipline.
Quality stack setup
The stack (language-agnostic)
The point of this stack is AI-slop resistance: deterministic guardrails so generated code can't land unformatted, untyped, dead, duplicated, or untested. The tools change per language; the guardrail categories don't.
| Guardrail | Python | TS/JS | Catches the slop symptom |
|---|---|---|---|
| Format | ruff format | prettier | inconsistent style (auto-fixed) |
| Lint | ruff | eslint | error-prone patterns, unused code |
| Type-check | mypy | tsc --noEmit |
hallucinated APIs, wrong shapes |
| Static analysis | skylos | fallow | dead code, complexity, clones, secrets, AI regressions |
| Test + coverage | pytest --cov |
vitest/jest --coverage |
untested generated code |
Other languages wire the same categories (golangci-lint + gofmt, clippy + rustfmt, …).
The architecture: one definition, three callers
The checks are defined once, in a justfile. Everything else just calls those recipes, so local and CI run identical commands with the project's own pinned tools — parity is structural, not maintained.
just verify=format-check+lint+typecheck+test(+coverage). The full gate.just fix= auto-format + lint---fix. The fast pre-commit layer.
Three callers, earliest first:
- pre-commit (lefthook) — fast auto-fixers on staged files (format, lint
--fix) + the staged-file static scan (skylos/fallow). Fixes what it can; kept fast so nobody reaches for--no-verify. - pre-push (lefthook) — runs
just verify: the whole-project gate (type-check + tests + coverage can't be scoped to staged files, so they belong here, not on every commit). Catches slop before it leaves your machine, for humans and Claude alike. - CI required check (
ci.yml) — runs the samejust verifyas the unbypassable backstop. Local hooks can be skipped (--no-verify,lefthook installnot run); a required status check cannot.
Because pre-push and CI invoke the identical just verify, "passed locally → passes in CI" holds by construction. Prove the hooks actually fire (a hook that doesn't fail on bad input is not a hook) with /quality-verify-hook.
Why lefthook + just, not the pre-commit framework: the pre-commit framework installs tools into its own pinned envs, which can drift from the versions CI uses — the exact "passed locally, failed in CI" failure we're killing.
just-calls-uvx/pnpmuses the project's pinned tools everywhere. And whole-project checks (typecheck, full test suite) don't fit pre-commit's per-file model. lefthook (fast, parallel, language-agnostic, single binary) dispatches stages and calls the sharedjustrecipes — the right tool for this shape.
Why
Real working setup (now-playing repo). skylos's agent pre-commit mode is built for AI-assisted development — it scans staged files for AI-introduced regressions (security, secrets, prompt-injection, dependency hallucinations); fallow does the equivalent for TS/JS. The original gap that motivated all this: a Phase-3 cleanup regressed on a gh pr merge because the local hook never fired on that path. The fix that actually closes it is the CI required check running just verify — the merge can't complete until it's green — backed by lefthook catching it earlier on commit/push.
How to apply
Install the stack
- Copy
${CLAUDE_PLUGIN_ROOT}/templates/justfileand${CLAUDE_PLUGIN_ROOT}/templates/lefthook.ymlto the repo root. - Adjust both to the project's languages: the recipes auto-detect Python (
pyproject.toml) and TS/JS (package.json); delete the language commands you don't use, fix thekioskTS dir inlefthook.yml, and pointtestat your runner. The tools need their config present —ruff/mypyreadpyproject.toml,eslint/tscneed their configs,prettieris configless by default.- Subdir / monorepo layout (no root manifest): the justfile's
[ -f pyproject.toml ]guards check the repo root. If code lives in subdirs (each with its own manifest) and there's no root one, the guards are false andjust verifysilently passes without checking anything — the no-op gate this stack exists to prevent. Edit the recipes tocdinto each subdir (e.g.cd packages/api && uv run pytest), then run/quality-verify-hook, which stages a known-bad fixture and will catch a recipe that no-ops instead of failing.
- Subdir / monorepo layout (no root manifest): the justfile's
- Install the tools once and enable the hooks:
Add these to the project README so contributors do the same.brew install just lefthook # or: cargo install just; npm i -D lefthook lefthook install # wires .git/hooks → lefthook - Make sure
ci.yml(from the branch-promotion-model templates) is present and callsjust verify— that's the unbypassable backstop and the parity anchor. Mark itscijob a required status check (seegithub-repo-setup). - Document the rules in the project
CLAUDE.md:Quality stack:
lefthookruns auto-fixers on commit andjust verifyon push; CI runs the samejust verify. Don't bypass with--no-verify. Suppressions require an inline rationale — see thesuppression-disciplineskill.
Existing / legacy repo: gate new, fix old incrementally
A repo with history almost always has a large backlog of findings (slay-the-spire had ~2160). If the hooks blocked on the whole backlog, every commit would fail and the team would reach for --no-verify — defeating the stack. So on an existing repo the rule is gate new, report old: the scanners fail only on findings you introduce; the inherited backlog is reported but never blocks. This is a property of how you invoke the scanners, so wire them new-only:
- fallow (TS/JS dead-code / complexity / duplication) →
npx fallow audit --changed-since HEAD— scopes the gate to the diff; inherited findings are excluded. In CI use--changed-since <base-sha>against the PR base. - skylos (broad static analysis on staged TS/JS/C#/JSON — not just Python: code quality, dead code, complexity, clones, security/danger, secrets, dependencies/SCA, AI-introduced regressions) →
uvx skylos agent pre-commit .— theagent pre-commitmode is conservative by design and only surfaces new, high-confidence issues, so the backlog's low-signal noise stays quiet. (The heavier full audit,skylos --quality --danger --secrets --sca, runs in/quality-audit, not the hook.)
Then the backlog is a separate, non-blocking workstream — this is the plan for a legacy repo:
- Add the gates first (this PR) — stops the bleeding; no new slop can land.
- Snapshot the backlog with
/quality-auditso it's visible and tracked. - Burn it down incrementally — fix inherited findings in their own PRs over time (not a flag-day); each fix shrinks the next audit's number.
Tuning that made it usable on slay-the-spire: pick skylos's agent pre-commit mode (not full audit) so backlog false-positives stay quiet; scope fallow with .fallowrc.json (ignore tests / dist / build-output / vendored / docs); pin fallow as a devDep for reproducible, fast runs; and document both — with the tuning knobs (skylos --conf, .fallowrc.json) and a /quality-audit pointer — in the project CLAUDE.md. Local prereq: committers need uv (for uvx skylos) alongside lefthook/just; fallow auto-installs via npm.
Verify the new-vs-old behavior empirically before declaring done: a clean commit must pass (scanners report the backlog but exit 0), and a deliberately-bad new change must block (exit 1). The pre-commit/pre-push gates can still be skipped with --no-verify; to make them unbypassable on the PR diff, add a CI job running fallow audit --changed-since <base> + skylos --diff-base <base>.
After installing — verify and hand off
- Run
/quality-verify-hook(quality-workflow) immediately. It stages a known-bad fixture and asserts the hook fails — the only way to know a silently-misconfigured gate isn't waving everything through. - Run
/quality-audit(quality-workflow) for a baseline snapshot — on an existing repo this surfaces the debt the gates will start enforcing. - For the standing rule on suppressions (every
# skylos: ignore/# noqa/// eslint-disablecarries a rationale), seesuppression-discipline; when a hook blocks a commit,/quality-unblocktriages it.
Templates referenced
${CLAUDE_PLUGIN_ROOT}/templates/justfile— the single source of truth for checks (verify,fix, and sub-recipes).${CLAUDE_PLUGIN_ROOT}/templates/lefthook.yml— git-hook dispatcher (pre-commit fixers + pre-pushjust verify).${CLAUDE_PLUGIN_ROOT}/templates/github-workflows/ci.yml— CI backstop that runs the samejust verify.${CLAUDE_PLUGIN_ROOT}/templates/fallowrc.example.json— fallow config for the TS scan.
Related
quality-workflow— operates the installed stack:/quality-verify-hook(prove the hooks fire),/quality-audit(health snapshots),/quality-unblock(triage a failing hook),suppression-discipline(the standing rule).