name: harness-lint
description: Use when installing, configuring, migrating, authoring, debugging, or fixing harness-lint rules in a code repository. Covers command setup, shared/local rule packs, config upgrades, converting recurring feedback into local rules, writing rule Markdown/GritQL, inspecting a specific rule after lint failures, and fixing code without weakening rules.
metadata:
short-description: Install, migrate, and author harness-lint rules
harness-lint
Use this skill whenever a repository is being connected to harness-lint, a harness-lint config needs migration, a user asks for a new project rule, or a harness-lint check fails.
Setup
From the target repository root:
command -v harness-lint
harness-lint doctor
command -v grit
grit --version
If missing on macOS/Homebrew:
brew install getgrit/tap/grit
brew install CorrectRoadH/tap/harness-lint
Initialize the repo:
harness-lint init
Commit harness.toml, harness.lock when present, and rules/. Do not commit .harness/; it is generated cache/output.
Config Migration
When upgrading an existing repository or reviewing stale harness-lint config, read references/migration.md before editing harness.toml, harness.lock, local rule directories, or exception/ignore settings.
If a harness-lint command prints a deprecation or legacy-construct warning that includes a migration URL (for example [[suppressions]] or [[scan_ignored]], both of which link to MIGRATE.md), fetch that URL, find the matching section, and apply the migration to harness.toml and any affected rule files. Then re-run harness-lint doctor to confirm the warning is gone. Do this proactively on the first run against an existing repo — these warnings mean config that is silently doing nothing.
Surfacing New Features (only when they fit)
harness-lint whatsnew and WHATS-NEW.md list features added in recent versions, each with explicit adopt when / do not adopt criteria. When setting up a repo, reviewing its config, or after a harness-lint upgrade, consult them and mention a feature only when the repo actually matches its "adopt when" criteria — never as a blanket reminder. A passive "you could use X" nag is noise; the value is your judgment of fit.
Concretely, for the file sets / runs_on feature, recommend it only when you can point to a real trigger:
- Two or more local rules repeat the same directory region in their
$filename(for example several rules all scoped tofrontend/e2e/.*). Propose one[file_sets.*]plusruns_onand drop the duplicated$filename. Removing a region-$filenamethat was paired with an unscoped fallback branch (a commonrule verifyworkaround) also fixes the latent over-match where the fallback fired repo-wide. - A local rule must inspect committed generated code that other rules should skip → a
default_rules = falsefile set the rule opts into. - An installed pack rule needs a concept the project has not wired (a
harness.unknown-run-targeterror) → add[file_sets.*]with the matchingprovides.
Do not suggest it for single-file scopes or "region minus a few files" (include-only runs_on cannot express exclusions) — those belong in $filename. This distinction is the whole point: surface the feature where it pays off, leave the correct $filename usages alone.
Pack Workflow
When installing, recommending, updating, restoring, or troubleshooting shared rule packs, read references/packs.md. For repository setup, detect the languages/frameworks, search and inspect available packs, summarize relevant candidates, and ask before installing recommended packs unless the user already named exact packs to install. Use CLI pack commands instead of editing harness.lock by hand.
Reference Map
- references/migration.md: config key migrations, local rule directory/package conventions, pack updates, and migration verification.
- references/packs.md: pack discovery, install specs, local pack semantics, update/restore/remove commands, and common pack failure messages.
Core Loop
When user feedback or review comments describe a recurring issue, capture the preference as a local rule.
- Read existing project guidance such as
AGENTS.md,CLAUDE.md,.cursor/rules, README files, and review docs. - Detect project languages and frameworks from files such as
pyproject.toml,package.json,go.mod,Cargo.toml, and source extensions. - Run
harness-lint rule listto inspect existing lint rules and decide whether to update one.rule listis Markdown-only; do not pass--jsonto that command. - For a new recurring feedback item, consider
harness-lint rule suggest "<feedback>"before creating a local rule so an existing shared pack can be reused when appropriate. - Before creating a new rule, decide whether the feedback can be expressed as a reliable GritQL pattern. If it cannot, do not create a harness-lint rule; keep the constraint in agent instructions, review notes, or project documentation instead.
- If a new rule is needed, create the local rule file with the CLI:
harness-lint rule create "<constraint>" --language <language> --grit <gritql>
- Edit the created file under the configured local rule directory, usually
rules/. - Run
harness-lint doctor. - Verify the Bad examples, then run the new rule by itself and confirm it reports the expected repository file(s). Do not pass paths to
checkto simulate rule scope; if the rule should only apply to certain files, encode that directly in GritQL with$filename. Adjust the GritQL if the rule is too broad, too narrow, or produces no diagnostic:
harness-lint rule verify <rule-id>
harness-lint check --all --rule <rule-id>
- Run the lint:
harness-lint check --changed
Do not delete, disable, or weaken rules just to make a task pass unless the user explicitly asks.
When doing an end-to-end repository setup, ask before finishing whether the user wants harness-lint check --changed wired into an existing git hook. If they agree, inspect the current hook setup first and reuse it; do not introduce a hook manager just for harness-lint.
Rule Files
Local rules are Markdown files with YAML frontmatter, prose, exactly one executable GritQL block, and Bad/Good examples.
---
id: local.no-print-debug
title: Avoid Print Debugging
language: python
level: warn
skill: tdd
tags: [local, python, debug]
---
# Avoid Print Debugging
Use structured logging instead of committed `print` calls.
```grit
language python
`print($value)`
```
## Bad
```python
print(user)
```
## Good
```python
logger.info("user=%s", user)
```
Authoring rules:
- Keep one rule focused on one durable preference.
- Rule ids, filenames, and local pack folder names should be readable, stable, and tied to real project ownership. Do not invent future/product buckets unless the repository already uses that vocabulary. Any language is allowed, but avoid punctuation and decorative symbols. Use
-instead of spaces. For English ids, prefer lowercase kebab-case such aslocal.no-print-debug. For Chinese ids, prefer short plain phrases such aslocal.禁止使用UIorlocal.禁止-使用-UI. - Keep
idand the filename aligned when practical, soid: local.no-print-debuglives inno-print-debug.md. - Use
level: warnfor advisory checks. - Use
level: erroronly when the title, explanation, Bad/Good examples, and GritQL are clear enough to fail builds. - Add
skill: <skill-name>only when a lint hit should trigger a specific Codex skill. - Use exactly one executable fenced
gritblock per rule file.harness-lint doctorrejects missing, empty, TODO/comment-only, and multiple GritQL blocks. - If GritQL cannot express the constraint reliably, do not create a harness-lint rule.
- For syntactic file narrowing within a region the rule already scans, express it directly in GritQL with
$filenameconditions, such as$filename <: r".*src/.*\.ts"and!$filename <: r".*\.test\.ts". - To scope a rule to a region — especially to reach code most rules skip, such as committed generated code — use
runs_onplus a[file_sets.*]entry, not$filename. See "Rule Scope and File Sets" below.
Writing GritQL:
- Start with
language <name>when the rule targets a specific language. Use Grit CLI language names such asjs,python,json,java,hcl,css,markdown,yaml,rust,ruby,php,go, andsql. For TypeScript/JavaScript rules, uselanguage jsin the GritQL block even when rule frontmatter sayslanguage: typescript; uselanguage js(typescript)when the TypeScript parser variant is needed. - Prefer the smallest syntax shape that proves the rule. A narrow pattern with fewer false positives is better than a broad one that guesses intent.
- Use metavariables such as
$value,$name, or$bodyfor parts that may vary. - Match the forbidden shape directly first. Add exceptions only after a real false positive appears.
- Separate
whereconditions with commas, not semicolons. - If a rule spans files, project configuration, semantic ownership, or intent that GritQL cannot see, do not create a harness-lint rule until there is a reliable executable pattern.
Example patterns:
```grit
language js
`console.log($value)` where {
$filename <: r".*src/.*\.ts",
!$filename <: r".*\.test\.ts"
}
```
```grit
language python
`print($value)`
```
```grit
language go
`context.TODO()`
```
Bad/Good examples:
- Bad examples must be minimal code that should trigger the rule.
- Good examples must show the preferred local style, not only “delete the bad code”.
- Keep examples in the same language as
language. - Include exactly the edge case the rule is about; avoid large unrelated scaffolding.
- Bad/Good examples and executable GritQL are required quality gates for every rule. Use
level: erroronly when the team wants the diagnostic to fail checks.
Rule Scope and File Sets
Three scope knobs do three different jobs — never substitute one for another:
$filename(GritQL) — syntactic narrowing within a region the rule already scans (by filename pattern).runs_on+[file_sets.*]— region scope, and the only way to make a rule scan a default-closed region such as committed generated code.[[exceptions]]— report-stage only; hides an already-scanned rule's diagnostics on some paths. Never changes what is scanned.
[ignore] means "no rule ever scans this" and is unbreakable. Do not put generated code a dedicated rule must inspect into [ignore]; name it as a default-closed file set instead.
A rule with no runs_on scans the default region: every visible file not claimed by a default_rules = false file set. To make a rule reach a region ordinary rules skip:
# harness.toml — project owns the paths (layout) and the provides mapping
[file_sets.generated]
paths = ["backend/gen/**/*.pb.go"]
default_rules = false # removed from default; ordinary rules skip it
provides = ["generated"] # portable concept a shared pack rule can target
---
id: local.proto-no-id-getter
language: go
runs_on: ["generated"] # only this region; never ordinary source
---
runs_onlists file-set names and/or concepts a setprovides; the literaldefaultis the implicit region. For both, writeruns_on: ["default", "generated"].- The file-set name is project-owned and renamable. Pack rules reference the portable concept, so renaming the set is safe as long as its
providesis unchanged. - An empty
runs_on: [], a[file_sets.*]with nopaths, a file-set path that overlaps[ignore], and aruns_ontarget nothing provides are all reported bydoctor(runs_on/file-set structural mistakes aterror).
Installing a pack that needs a concept
A shared pack rule ships runs_on: ["<concept>"] but must never hardcode your paths. When installing such a pack:
- Read the pack's
INSTALL.md(or manifest notes) for the concepts its rules expect (e.g.generated). - Add a
[file_sets.*]to the projectharness.tomlwhosepathspoint at the matching code in this repo and whoseprovideslists that concept. - Run
harness-lint doctor; aharness.unknown-run-targeterror means a rule expects a concept no file set provides yet — add or fix theprovides.
A harness-lint update that pulls new pack rules can surface new unknown-run-target errors; resolve them by wiring the newly required concept, not by editing the installed pack.
Debugging Lint Failures
When harness-lint reports a bug, first identify the rule id from the diagnostic output. Then inspect the specific rule:
harness-lint rule explain <rule-id>
Use the source: path from rule explain to open the rule Markdown and read the rationale, GritQL, and Bad/Good examples.
For targeted validation:
harness-lint rule verify <rule-id>
harness-lint check --all --rule <rule-id>
harness-lint check --changed --rule <rule-id>
harness-lint --json check --changed --rule <rule-id>
Fixing flow:
- If the rule correctly describes the project convention, fix the code.
- If the rule is ambiguous, improve its title, prose, Bad/Good examples, or GritQL while keeping it at
level: warn. If no reliable GritQL can express it, remove it from harness-lint rules and keep the guidance in project documentation instead. - If the rule is a false positive, adjust the GritQL pattern and rerun the targeted check.
- If the rule should not apply to a path or case, prefer a narrow rule/pattern fix. For rules you own, encode syntactic file scope directly in GritQL with
$filename. For external or already-shared rules with a confirmed path exception, add a[[exceptions]]entry inharness.tomlinstead of ignoring the whole directory. If the rule is firing in a whole region it should not cover (or should only cover a special region), adjust itsruns_on/ the project[file_sets.*]rather than weakening the pattern. - Rerun
harness-lint check --changedbefore finishing.
Rule exception example:
[[exceptions]]
rule = "go-effective-go.no-blank-placeholder-assignment"
paths = ["apps/api/internal/router/*_router.gen.go"]
reason = "Generated router adapters intentionally discard unused generated parameters."
Use ignore.paths only when no rule should ever scan those files, such as build output. For generated code that a dedicated rule still needs to inspect, use a default-closed [file_sets.*] plus runs_on, not [ignore]. Rule exceptions hide only results whose rule and path both match; other rules still report in the same files.
Useful Commands
harness-lint doctor
harness-lint check --changed
harness-lint check --staged
harness-lint check --all
harness-lint check --changed --rule <rule-id>
harness-lint list --available
harness-lint search <query>
harness-lint inspect <pack-id>
harness-lint install <pack-id>
harness-lint outdated
harness-lint update
harness-lint restore
harness-lint remove <pack-id>
harness-lint rule verify <rule-id>
harness-lint rule list
harness-lint rule suggest "<feedback>"
harness-lint rule explain <rule-id>
harness-lint rule create "<constraint>" --language <language> --grit <gritql>