verify-bootstrap
Set up a repo's machine gates (format, lint, type check, security, dependency audit, tests), or audit whether existing gates have gone stale. Use when a repo has no automated gate (lint を入れて), or its
Files2
View on GitHubSKILL.mdskills/verify-bootstrap/SKILL.md
GitHubverify-bootstrap — set up machine gates in a repo
Humans cannot read code as fast as AI writes it. So judging structural correctness is moved to machines. For each repo, this skill sets up the best checking tools available at that moment.
What this skill does not carry (the core of the design)
It carries no list of tool names. It carries no language-detection table either. Tools go stale (flake8 + black + isort → ruff happened within two years). A fixed table rots, and the rotten table gets handed out to repos.
It carries only how to investigate, and a contract:
- a procedure that enumerates what the repo actually contains (no static language list)
- what to ask search-first for each category
- the contract of the generated
.claude/verify.sh(the only thing a pre-commit hook or CI needs to know) - the discipline of strictness and staleness detection
As a result, the same procedure works for languages and tools that did not exist when this skill was written.
Modes
| Mode | Trigger | What to do |
|---|---|---|
| bootstrap | No .claude/verify.sh |
Run Steps 1–6 |
| audit | It already exists (--audit / "audit" / 「棚卸し」) |
Re-run Step 1, re-derive the selections in .claude/verify.md with search-first, and propose the differences |
Step 1 — Enumerate what the repo actually contains (detection, no static table)
Do not guess. Measure.
# Extension distribution of tracked files (top) — excludes generated and dependency directories
git ls-files | sed -n 's/.*\.\([A-Za-z0-9_]*\)$/\1/p' | sort | uniq -c | sort -rn | head -20
# Which build/dependency manifests exist
git ls-files | grep -iE '(^|/)(pyproject\.toml|package\.json|Cargo\.toml|go\.mod|Gemfile|pom\.xml|build\.gradle.*|[^/]*\.xcodeproj/.*|Package\.swift|mix\.exs|composer\.json|Makefile|justfile)$'
# Traces of existing gates (prevents installing twice)
git ls-files | grep -iE '(pre-commit-config|lefthook|\.github/workflows/|trunk\.yaml|\.golangci|\.eslintrc|ruff\.toml)'
The manifest grep above is a discovery aid, not an authority. The first command's extension distribution is the source of truth; when an ecosystem shows up that the grep does not list, pass it straight to Step 2 (do not edit the table and add it to this skill — carrying no table is this skill's design).
Include prose, configuration, and schemas. Code is not the only target of a gate (Japanese prose in Markdown, YAML, CI definitions, and shell scripts can all be checked by machine).
Step 2 — Research the current best tool for each category
For each detected ecosystem, fill in the 6 categories. When a category cannot be filled, write "none" explicitly (an empty cell and "this language has nothing for it" are different things).
| category | Question |
|---|---|
| format | Normalize formatting. Remove diff noise so that only changes in meaning are left to review |
| lint | Structural errors, complexity, dead code |
| type check | Contracts expressible as types. For an untyped language: "is there a way to introduce type annotations incrementally?" |
| security | Dangerous constructs and leaked secrets (if a pre-commit hook or CI already runs a secret scan, check for duplication) |
| dependency | Known vulnerabilities, unused dependencies, licenses |
| test | Test runner and coverage measurement |
Do not rely on defaults for budget rules (rules that need a threshold: complexity, function/file length, bundle size, etc.).
Major linters usually ship budget rules OFF by default (because the thresholds are opinionated) —
under "maximum strict" operation they structurally slip through (a 2026-08-28 measurement found zero coverage:
no budget rule in 104 lint/build configs across 82 repos). Ask about them explicitly when filling the lint category. When the stack's standard toolchain
has budgets on by default (e.g. SwiftLint's cyclomatic_complexity / file_length), confirm them and
record one line in verify.md. Do not set thresholds globally — distributions differ several-fold
between repos (same measurement).
Measure the distribution over the whole existing corpus first, then place the threshold where only current outliers turn red
(the exemption-boundary principle of the skill review-to-lint. A threshold that turns the gate red on day one is a design error).
The lint / type check selection policy is LLM-first (the reader and the editor are both LLMs; humans do not read the code. Rationale: AKC ADR-0025 (LLM-first artifact readability)). Build the select set on the same 4 axes regardless of language:
- Bug-class detection (undefined names, unused code, known dangerous patterns, type contradictions) — the core of an uncorrelated verifier. Highest priority
- Canonical form (formatter, import order) — for diff stability, not for the eye. When another session touches the code, no spurious diff appears = golden files and reviews stay quiet
- Budget (complexity, file length) — an edit-reliability budget for the next editor, who works within a limited context (thresholds from the distribution measurement above)
- Explicit type enforcement at boundaries — types are a machine-verifiable specification and the frozen edge for regeneration and edits by other sessions.
Production side only (test functions are not consumed boundaries, so they are excluded).
Example implementation in Python: ANN001-003 + ANN201 + ANN401, excluding
tests/**
Do not select human-aesthetics rules (docstring style, naming conventions, comment formatting). They are not something to remove later; they are never put in. A rule whose only justification is "hard for humans to read" has no LLM-first justification.
For each category, call the search-first skill (do not call WebSearch directly — search-first comes before
adding a dependency or writing your own utility). Every question must include the point in time, "as of today's date",
and "has there been a migration away from the established alternative?".
Do not answer with the standard tool you remember — that is exactly what this skill exists to prevent.
When the report comes back, confirm the following before adopting:
- Was the last release within 12 months? (do not bring an abandoned project into a new repo)
- Can it run as a single binary, or on demand with an exact version pin? (e.g.
uvx <tool>@<x.y.z>/npx <pkg>@<x.y.z>/brew. Does it run without polluting the repo's runtime environment?) - Can the configuration be carried in? (can strictness be declared, and exceptions allowlisted?)
- Does the same thing run in CI and locally? (if they diverge, the local gate becomes a formality)
Step 3 — Install at maximum strictness
The default is maximum strict. AI does not complain about strictness, so the balance that used to be loosened on account of "human patience" tips toward correctness.
- Do not leave warnings as warnings — make them errors
- Forbid type escape hatches (
anyequivalents, comments that suppress type checking) - Pin versions (like
ruff==0.16.0. Fixes the supply chain. Bumps are manual) - Exceptions go in the config file's allowlist with a reason, not as inline suppression comments (inline suppressions lose their reasons and multiply without limit)
- When a repo-specific exception is needed, first move the stack toward the standard — only what remains after that goes into the allowlist with a reason ("only this repo is special" is a sign of insufficient design)
A new repo is maximum strict from day one (there is zero debt to drain, so no ratchet is needed). Use a ratchet only when retrofitting an existing repo — install every rule as warn, drain the existing violations completely, then raise them to error. Blocking from day one grows "workaround etiquette".
Budget-rule thresholds are likewise one-way — when exceeded, do not raise the threshold; prune (remove dead code and duplication, split). Put the comment "prune, don't raise — record changes in verify.md with a dated reason" on the threshold's config line. The spot that an agent touching the config sees the moment the gate turns red is where this operating rule gets delivered.
Step 4 — Generate .claude/verify.sh (the contract with pre-commit hooks and CI)
The repo's single entrypoint. A caller (a pre-commit hook, CI) looks only at this file's existence and its exit code — so the caller needs no change whatever language the repo is written in.
Contract (mandatory):
| Item | Specification |
|---|---|
| Path | <repo root>/.claude/verify.sh (executable) |
| Arguments | --staged = fast check at the commit boundary (staged files only). No argument = full check of the whole repo |
| Environment | If $VERIFY_REPO_ROOT is set, use it as the repo root (a caller that approves the content before running it passes it) |
| exit code | 0 = PASS / 1 = FAIL (blocks the commit) / 2 = cannot check (tool missing, etc.; fail-soft) |
| Output | On FAIL, detection lines a human and an LLM can read and act on. Output on PASS is advisory (things that do not block the commit but should be passed on — a ratchet awaiting promotion, a notice that a gate is asleep); the calling hook hands it to the model. With nothing to say, no output (do not add noise to a silent PASS). stdout and stderr are not distinguished — the caller captures both with 2>&1, so progress lines and deprecation warnings written to stderr on success also become advisory. Silence what should stay silent |
| Run time | --staged within a few seconds. Anything slower goes only in the no-argument mode |
The fast / slow split (if it is not kept, bypass etiquette grows):
--staged: format check, lint, security scan (things that complete per file)- no argument: build, type check, test, dependency audit (whole repo, minutes)
Tool resolution: use it if it is on PATH → otherwise run it on demand with an exact version pin (e.g. uvx <tool>@<x.y.z> / npx <pkg>@<x.y.z>, the same pin as Step 3) → if that is not possible either, exit 2 fail-soft. Fail-soft is not silent — print to stdout which gate is
asleep (a sleeping gate is more dangerous than a missing one).
Implementation traps (both were hit in practice):
Determine the repo root in the order
$VERIFY_REPO_ROOT→ the script's own location — if it starts from cwd, then when called through a hook it checks a different repo and passes silently: fail-open. A caller that approves content may run the verified bytes from a temporary file (to avoid TOCTOU), soBASH_SOURCEmay not point into the repo. Supporting both is mandatory:if [[ -n "${VERIFY_REPO_ROOT:-}" ]]; then ROOT="$VERIFY_REPO_ROOT" else ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P) || exit 2 fiConfirm the file is tracked — many repos have a
.gitignorethat excludes.claude/*. If it stays excluded, the gate disappears from CI and other clones. Check withgit check-ignore, and add!.claude/verify.sh/!.claude/verify.mdif needed
Generating the file does not by itself make it run at the commit boundary. Whatever runs verify.sh without a
permission prompt must run it only after a human has read the content and approved it, with the approval bound to
a content hash. A file that merely exists — for example in an external repo that was only cloned — must not run.
In the author's harness, a pre-commit hook (verify-precommit.sh) enforces this with
python3 ~/.claude/scripts/hooks/verify_allow.py approve <repo>, and passes PASS-time output to the model as advisory.
Because approval is bound to the content hash, a changed verify.sh needs approval again. In the author's harness, show the human verify_allow.py diff <repo> (the change since the approved version), then run approve through Bash; a hook asks the human to pass it at a permission prompt that carries the same diff. This is the trust boundary for repo-local code that runs without a permission prompt.
Run the generated verify.sh once on the spot to confirm it works. Then
inject a violation temporarily and prove each category fires (broken formatting, an unused import,
eval, etc.). A check that only passes is not evidence, because a sleeping gate also passes.
When the proof is done, always delete the injected probes.
Step 5 — Record the selections (make staleness visible)
In <repo root>/.claude/verify.md, leave the following for each category:
## type check
tool: pyright ==1.1.x
selected: 2026-07-31
reason: faster than mypy and strict by default. ty / pyrefly are pre-1.0 as of 2026-07
re-research triggers: 12 months elapsed / an alternative reaches 1.0 / the current tool's last release is more than 12 months old
Without this record, audit mode cannot decide "what to re-derive".
The selection date and the re-research triggers are mandatory; one line of reason is enough. For budget rules, also record
the threshold and the as-of date of the measured distribution (e.g. C901=15 — p99=14, measured 2026-08-28. It is the baseline
compared against the re-measured distribution at audit time).
Step 6 — Wire the same thing into CI
With only a local gate, a bypassed commit slips through. If CI has a job that runs .claude/verify.sh
with no argument, local and CI cannot diverge structurally
(call the same entrypoint from both. Writing a separate command sequence for CI always drifts).
In a repo where the implementing agent runs in a cloud session, CI is the only verify judge — the reviewer does not
re-run it locally but reads the conclusion of the run on the branch tip. Trust that conclusion only when the diff does not
touch .claude/verify.sh, .github/ or .claude/settings.json: CI runs the branch's own copies, so send such a diff back. A repo without CI
cannot be handed to cloud sessions, so for a repo with a remote on github.com, include this Step in bootstrap.
The template is references/ci-verify.yml. Fixed parts: triggers on push to main and claude/** /
actions pinned by commit SHA (resolve from the tag with gh api repos/<o>/<r>/git/ref/tags/<tag>) /
permissions: contents: read / persist-credentials: false / a secret scan job over the whole history
(a local pre-commit secret scan sees only staged additions). Parts to fill in: installing the tools verify.sh
resolves from PATH (the selection record in .claude/verify.md is the source of truth) and the language setup.
Tests that fail on a Linux runner (macOS-only launchd / BSD stat) declare themselves with skipif, and a test
that hits SystemExit because of a production-side platform guard gets fixed — to keep "red in CI = red
locally".
audit mode
- Re-run Step 1 (has the stack changed? — if a language was added, categories open up)
- For each category in
.claude/verify.md, re-derive with search-first only those that match a re-research trigger (re-deriving everything costs every time) - Present the differences: current → candidate, switching cost, reason for staying
- The user decides. Do not switch automatically — a tool change produces a repo-wide diff, and the cost of reverting exceeds the cost of introducing
Related
- Entry point for Phase 0: skill
search-first(tool selection always goes through it) - The overall implementation flow that includes the gate: your implementation workflow (plan → test → review → verify)