Add a Code Quality Hook
SkillFiles & storageGuides your agent through adding a new linter to a repo's pre-commit and CI code quality pipeline.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Add a Code Quality Hook skill
About this capability
Recipe for wiring a new linter into Opik's unified 🐙 Code Quality pipeline (pre-commit + CI). Use when adding a pre-commit-driven linter/formatter to the repo — enumerates every file that must change (`.pre-commit-config.yaml`, `scripts/precommit-hook-descriptions.tsv`, `scripts/precommit-detect-hoo
What this skill tells your AI
The instructions your AI receives, as published by comet-ml/opik in .agents/skills/add-code-quality-hook/SKILL.md and read by ahel’s review.
Opik runs all linters through one pipeline: pre-commit locally, and the 🐙 Code Quality workflow (.github/workflows/code_quality.yml) in CI. CI does not run pre-commit wholesale — it derives, per PR, the set of hooks that actually have work using scripts/precommit-detect-hooks.py, then runs one CI job per matched linter. That design makes adding a linter a small, fixed set of edits — but each edit is load-bearing, and skipping one produces a silent gap (the hook runs nowhere, or renders blank in the summary, or provisions the wrong runtime) rather than a loud failure. This skill is the checklist.
The whole recipe is a generalization of two real PRs: actionlint (live in .pre-commit-config.yaml today — grep it as you read) and hadolint (OPIK-6673, PR #7352 — the first Docker-image hook, toolchain: none). Read the actionlint hook alongside this doc; it is the canonical, verifiable example.
The wiring files
Each linter touches these four files. Do all four.
1. .pre-commit-config.yaml — add the hook
Add the upstream hook (repo / rev / id). Pin rev to a tag or SHA — never a floating ref.
An explicit files: regex is mandatory, not optional. This is the single most common miss. The CI matrix detector (precommit-detect-hooks.py) routes work to hooks by path regex, not by pre-commit's types:. Most upstream hooks (actionlint, hadolint) ship a types:-only match with no files:. If you copy them verbatim, the detector cannot route any file to your hook and CI silently never runs it — pre-commit locally still works, so the gap hides until something slips through. The detector guards against this: it raises loudly if a hook has types:/types_or: without files: (see precommit-detect-hooks.py lines ~115). So a missing files: fails the detect step rather than regressing silently — but you still must write the regex.
Write a files: regex that captures exactly the paths the linter should gate. Example (actionlint — workflows only):
- repo: https://github.com/rhysd/actionlint
rev: v1.7.12
hooks:
- id: actionlint
name: ⚙️ actionlint — github workflows
files: ^\.github/workflows/.+\.(yml|yaml)$
The name: is what the reader and the CI summary see — give it a clear, emoji-prefixed display name matching the house style of the other hooks. The description keyword you add in step 2 is matched against this name, so pick a name containing a stable, distinctive substring (e.g. actionlint, hadolint).
2. scripts/precommit-hook-descriptions.tsv — add the description row
This TSV is the single source of truth for the per-hook descriptions shown in the Code Quality timing/skipped tables (the CI summary comment). Format: <keyword>\t<description>. The keyword is matched as a substring of the hook display name (name: from step 1).
Miss this and the hook renders with a blank description in the CI summary. Add a row:
hadolint Lint Dockerfiles
Order matters — most-specific first. Matching is first-substring-wins top-to-bottom, so a more specific keyword must precede any that it contains (the file already does this: ruff-format before ruff). If your keyword is a substring of an existing one, place it above that line.
3. scripts/precommit-detect-hooks.py — toolchain and content-type maps
Two maps in this file may need an entry. Most new hooks need neither (default is toolchain: none, no content-type narrowing) — but decide deliberately:
-
TOOLCHAIN_BY_ID— add your hook id here only if the leg's CI job must provision a heavy runtime:java(shells out to mvn),node-fe/node-ts(shells out to npm). Pre-commit's own hooks self-provision in isolated envs and need nothing; alanguage: golanghook (actionlint) self-builds; a Docker-image hook (hadolint) runs the image — all of these arenone.code_quality.ymlbranches its setup steps onmatrix.leg.toolchain, so a wrong value means a job either wastes minutes provisioning an unused runtime or lacks the runtime it needs. -
TYPED_IDS— add your hook id here only if it carries an upstreamtypes:that narrows itsfiles:match to a content suffix, so the detector doesn't emit a leg that would no-op at runtime. Value is the tuple of suffixes the hook actually acts on (e.g.(".py", ".pyi")). Symptom of a missing entry: the CI timing comment reports fewer ran rows than emitted legs ("detect over-emitted a leg"). If yourfiles:regex is already suffix-precise (like actionlint's\.(yml|yaml)$), you don't needTYPED_IDS.
Rule of thumb by hook type:
| Hook type | TOOLCHAIN_BY_ID | TYPED_IDS |
|---|---|---|
| Docker-image linter (hadolint) | none (omit) | usually omit — make files: suffix-precise |
language: golang/self-built (actionlint) | none (omit) | omit if files: is suffix-precise |
| Python tool (ruff, mypy) | none (omit) | add suffixes if files: is a broad dir regex |
| Shells out to mvn | java | as needed |
| Shells out to npm (FE/TS) | node-fe / node-ts | as needed |
4. CONTRIBUTING.md — "how to run locally" note
Add a short section alongside the existing GitHub Actions workflows (actionlint) note: what the linter checks, that it runs in the unified 🐙 Code Quality workflow and locally via pre-commit, and that make hooks enables it. If the hook needs a local dependency (a Docker daemon for hadolint, etc.), say so; if pre-commit self-provisions it (actionlint builds from source), say that instead.
Retroactive step — fix the existing violations
Adding a linter to a repo that has never run it will surface pre-existing violations. The gate must be green on day one. Policy, in order of preference:
- Fix the violation in place — this is the default. Most findings are real and worth fixing.
- Suppress inline, with a reason, next to the code — only when a fix would be genuinely undesirable. The canonical case: exact-pinning rolling-channel OS packages (e.g.
apt-get install foo=1.2.3), which rots as mirrors move — the honest answer is an inline# hadolint ignore=DL3008with a one-line why. Inline suppression is scoped to that one line and visible in review. - Never a global config-file ignore. A repo-wide ignore (a
.hadolint.yamlignored:list, an eslint config-level disable) fails open on every future file — it silently exempts code no one has reviewed. Inline-with-reason keeps the gate strict on everything new.
See the OPIK-6673 discussion for a worked case where some rules genuinely couldn't be honestly fixed and inline-with-reason was the right call.
Verification loop
Before opening the PR, confirm the wiring end-to-end — don't trust that the four edits compose:
-
Detect emits a leg for a target file. Run the detector against a file the hook should gate and confirm your hook id appears in
legswith the righttoolchain:python3 scripts/precommit-detect-hooks.py .pre-commit-config.yaml path/to/target.fileIf it lands in
skippedinstead, yourfiles:regex doesn't match. If it errors abouttypes:withoutfiles:, add thefiles:regex (step 1). -
Hook passes clean. Run it on the current tree and confirm green (this is also the retroactive check):
pre-commit run <hook-id> --all-files -
Hook fails on a new violation. Introduce a deliberate violation in a target file and confirm the hook catches it, then revert. A hook that can't fail isn't gating anything.
-
Description resolves. Confirm the summary won't render blank:
echo "<your hook display name>" | python3 scripts/precommit-hook-desc.pyExpect
<name>\t<description>— an empty second column means the keyword row (step 2) is missing or mis-ordered.
Checklist
-
.pre-commit-config.yaml: hook added with pinnedrevand an explicitfiles:regex -
scripts/precommit-hook-descriptions.tsv: keyword→description row, most-specific-first ordering -
scripts/precommit-detect-hooks.py:TOOLCHAIN_BY_IDentry iff heavy runtime needed;TYPED_IDSentry ifftypes:-narrowed andfiles:isn't suffix-precise -
CONTRIBUTING.md: local "how to run" note added - Retroactive: existing violations fixed (or inline-suppressed-with-reason); gate green on day one
- Verified: detect emits the leg, hook passes clean, hook fails on a new violation, description resolves non-blank
Signals
- GitHub stars
- 22k
- Forks
- 2k
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
add-code-quality-hook- Source
- github.com/comet-ml/opik