pr-to-lint-rule — Claude Code skill: what it does, how to install
This skill turns recurring review guidance into a custom lint rule and tests whether the checker matches the rule’s actual intent.
npx skills add zernie/vigiles --skill pr-to-lint-rule -a claude-codeWhat pr-to-lint-rule does
The skill accepts a prose requirement or a custom rule (⚙) entry from a vigiles audit report. It inspects the repository to identify the main language, linter, test framework, and local rule conventions. Existing rules come first; synthesis is reserved for cases that need custom AST analysis, a fix, or options unavailable in the current tooling.
It writes the checker together with a separate intent test. That test covers safe lookalikes such as strings, comments, examples, and compliant calls, as well as alternate forms of the violation. A trust gate runs both sets: precision and recall must reach 1.0 before the rule is kept. If the checker leaks, the skill abstains and returns the requirement as prose. It runs only when explicitly invoked, so installing vigiles does not start synthesis.
What it helps with
- turn recurring review guidance into a lint rule
- reuse an existing rule before writing custom code
- test a checker against adversarial examples
- catch false positives in strings and comments
- detect missed aliases and alternate violations
Example prompts for the agent
- Turn this review rule into a lint check: do not import directly from
antd; use the design-system barrel. - Create a custom rule for replacing
console.logwith our logger, then test its intent independently. - Convert the requirement that API calls use
withRetryinto a checker after reading the repository.
How to install pr-to-lint-rule
1Skills CLI (Vercel)
npx skills add zernie/vigiles --skill pr-to-lint-rule -a claude-code2Manually: put the files in place
~/.claude/skills/pr-to-lint-rule/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/pr-to-lint-rule/
1Skills CLI (Vercel)
npx skills add zernie/vigiles --skill pr-to-lint-rule -a codex2Manually: put the files in place
.agents/skills/pr-to-lint-rule/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add zernie/vigiles --skill pr-to-lint-rule -a cursor2Manually: put the files in place
.cursor/skills/pr-to-lint-rule/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install zernie/vigiles pr-to-lint-rule2Manually: put the files in place
.github/skills/pr-to-lint-rule/SKILL.md1Skills CLI (Vercel)
npx skills add zernie/vigiles --skill pr-to-lint-rule -a gemini-cli2Platform CLI
gemini skills install https://github.com/zernie/vigiles --path .claude/skills/pr-to-lint-ruleManually: put the files in place
.devin/skills/pr-to-lint-rule/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add zernie/vigiles --skill pr-to-lint-rule -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about pr-to-lint-rule
How do I install pr-to-lint-rule in Claude Code?
Run npx skills add zernie/vigiles --skill pr-to-lint-rule -a claude-code, then use ~/.claude/skills/pr-to-lint-rule/SKILL.md.
What does pr-to-lint-rule do?
It looks for an existing linter rule first, then builds a custom checker and a separate test for the rule’s intent when synthesis is needed. The checker must pass the trust gate before it is kept.
What happens when the repository has several possible languages or linters?
The skill asks you to choose before generating a rule when the repository does not provide a confident single choice.
Source text
SKILL.md · MIT
---
name: pr-to-lint-rule
description: Synthesize a recurring code-review rule into a custom lint rule — gated by an independent soundness test that abstains rather than ship a checker it can't prove sound
disable-model-invocation: true
argument-hint: <a prose rule, or a "custom rule (⚙)" line from a vigiles audit>
---
Turn a prose rule that **no off-the-shelf linter rule matches** into a custom
lint rule — the opt-in **synthesis** step. This is the hand-off target of the
**custom rule (⚙)** lane in a `vigiles audit` rule map: audit maps a rule there
when it looks enforceable but nothing off-the-shelf fits. You invoke this skill
explicitly; nothing here runs on its own, and installing vigiles never starts
synthesizing anything.
The point of this skill is **not** "write a checker." A model can write a
plausible-looking checker in seconds. The point is the discipline that makes the
result **trustworthy**: synthesize the rule **and** an independent test that
encodes the rule's real _intent_, run the checker against adversarial cases it
didn't author, and **abstain** — hand it back as prose — if it leaks. A checker
that matches a rule's _surface_ but not its _intent_ gives false confidence
(the measured failure mode: most naively-synthesized checkers silently leak).
Shipping a green check nobody should trust is worse than shipping nothing.
## Arguments
`$ARGUMENTS` — a prose rule to enforce. Either a **custom rule (⚙)** line copied
from a `vigiles audit` report, or free text. Examples:
- "we keep telling people not to import directly from antd — use our design-system barrel"
- "people forget to use our custom logger instead of console.log"
- "wrap outbound API calls in our `withRetry` helper"
- "route handlers must go through the `withAuth` wrapper"
## The pipeline
```
prose rule
→ detect language + linter (ask if ambiguous)
→ REUSE first: does an off-the-shelf rule already fit? → use it, STOP (no synthesis)
→ clarify intent + read the codebase (what exactly is the violation? the fix?)
→ SYNTHESIZE: the rule + an INDEPENDENT intent-encoding test (adversarial cases)
→ TRUST GATE: run the checker on that test; precision AND recall must be 1.0
pass → KEPT (safe to enforce; wire it in)
leak → ABSTAIN (hand back as prose; never ship a checker it can't prove sound)
```
## Instructions
### Step 1 — Detect the language and toolchain
Read the repo to find the **primary language**, the **linter in use** (ESLint,
Ruff, Pylint, Clippy, RuboCop, Stylelint…), the **test framework**, and any
**existing custom rules** (to match conventions). If you cannot confidently pick
one — polyglot repo, no linter config, several candidates — **ask the user**
before generating anything.
### Step 2 — REUSE before you synthesize (ADOPT > REUSE > SYNTHESIZE)
Synthesis is the **last** resort, not the first. Before writing any rule:
1. **Check existing plugins / built-in rules.** Read the linter reference doc
below (the plugin table + `no-restricted-*` families) — most one-off
prohibitions are a config line, not custom code.
2. **Try a built-in construct rule.** ESLint `no-restricted-syntax` /
`no-restricted-imports`, Ruff/Pylint selects, RuboCop cops, etc.
3. **Only synthesize** when the rule needs AST analysis, a fix, or options that
no existing rule provides.
If an off-the-shelf rule fits, **use it and stop** — enable it (one config line)
and jump to Step 6. That is the `strengthen` outcome, and it's the better one;
don't synthesize a rule when a maintained one exists.
| Language | Linter | Reference doc |
| --------------------- | --------- | ------------------------------------------ |
| JavaScript/TypeScript | ESLint | `../../../skills/linter-docs/eslint.md` |
| Python | Ruff | `../../../skills/linter-docs/ruff.md` |
| Python | Pylint | `../../../skills/linter-docs/pylint.md` |
| Ruby | RuboCop | `../../../skills/linter-docs/rubocop.md` |
| Rust | Clippy | `../../../skills/linter-docs/clippy.md` |
| CSS | Stylelint | `../../../skills/linter-docs/stylelint.md` |
For **Go**, generate a `golang.org/x/tools/go/analysis` analyzer with
`analysistest` tests. For any other language, use the most idiomatic linting
approach with test cases and integration notes.
### Step 3 — Nail the intent, then read the codebase
Synthesis fails when the checker enforces what's _easy to match_ instead of what
the rule _means_. Before writing anything, pin down:
- **The exact violation.** "No hardcoded secrets" is not "a variable named
`password`" — it's `apiKey`, `token`, an env-less literal too. "Wrap API calls
in retry" needs the real signature of the calls and the wrapper.
- **The fix.** What does the compliant form look like, concretely, in _this_
codebase? Read the real files — the helper's import path, its call shape.
- **The clean lookalikes.** What _looks_ like a violation but isn't? (the pattern
inside a string, a comment, a JSDoc `@example`; an aliased/namespaced form.)
If any of this is ambiguous — and for anything like "wrap API calls in retry" it
usually is — **ask the user** rather than guessing. A rule built on a guessed
intent will fail the gate anyway; asking is cheaper.
### Step 4 — Synthesize the rule AND an independent intent test
Write two artifacts:
1. **The rule** — prefer AST/keyword analysis over text/substring scanning
(text-scan checkers false-positive on the pattern appearing in prose; a
name-based checker false-negatives on synonyms). Follow the reference doc's
anatomy, fix/suggest, and registration guidance.
2. **An independent test that encodes the INTENT, not the checker's shortcut.**
This is the trust anchor — write it to the rule's _meaning_, then seed it with
the adversarial cases that break naive synthesis:
- **valid (must NOT flag):** the pattern in a string literal, a line comment,
a JSDoc/docstring `@example`; a camelCase identifier that merely _contains_
the keyword (`consoleLog`); a compliant call that _looks_ close.
- **invalid (MUST flag):** every real form of the violation — synonyms
(`apiKey`/`token`, not just `password`), the aliased/namespaced form
(`window.console.log`), the wrapped-differently case.
Do not make the test agree with a shortcut the checker took. If the rule is
"no hardcoded secret," the test asserts `apiKey` is a violation even if your
first checker only catches `password` — so the gate can catch the leak.
### Step 5 — The trust gate: precision AND recall must be 1.0
Run the synthesized checker against the Step-4 test (ESLint `RuleTester`, pytest,
`analysistest`, whatever fits the linter).
- **Pass** (every invalid flagged, every valid clean → precision = recall = 1.0):
the rule is **KEPT** — safe to enforce. Continue to Step 6.
- **Leak** (any false positive or false negative): the rule is **ABSTAINED**.
Do **not** wire it in. Report honestly _why_ it leaked (e.g. "matches the
pattern in a comment," "misses `apiKey`") and either (a) hand the rule back as
prose, or (b) if you can fix the checker, do so and re-run the gate — but never
ship an un-passed checker.
The reference gate lives in `rule-enforcer/gate.js` (two-stage: self-test + a blind
adversarial gold set), the canonical adversarial cases in `rule-enforcer/gold/gold.json`,
and a runnable end-to-end demo (`cd rule-enforcer && npm run demo` — watch it keep the
sound rules and abstain the two leaky ones). Read them for the case shapes and the
abstain discipline, then mirror it here; don't reinvent the gate.
### Step 6 — Wire a KEPT rule into the instruction file
Only for a rule that passed the gate (or an off-the-shelf rule reused in Step 2).
**v2 specs** (repo has `CLAUDE.md.spec.ts`): add an `enforce()` line and
recompile —
```typescript
"<rule-id>": enforce("<linter>/<rule-name>", "<why>"),
```
then `npx vigiles compile`.
**v1** (hand-written `CLAUDE.md`): append —
```markdown
### <Rule title — imperative, concise>
**Enforced by:** `<linter>/<rule-name>`
**Why:** <one sentence — the architectural reason>
```
### Step 7 — Present the outcome
Show the user, honestly:
- **Kept rules:** the generated files, the gate evidence (the adversarial cases
it passed), integration steps, the spec/`CLAUDE.md` block, and how to verify
(run the linter, watch it catch a real violation).
- **Abstained rules:** which rule, _why_ it leaked, and that it stays prose — a
refused checker is a correct outcome, not a failure to hide.
Then ask before writing any files or editing the spec/`CLAUDE.md`. Nothing is
written for the user automatically.
© zernie · MIT · zernie/vigiles