Measure & Improve
Everything in this guide condenses into one command:
npx harness-scoreThe scanner walks your repository (filesystem only — no LLM, no network, no telemetry), runs 36 deterministic checks, and reports a maturity level with the exact gaps to the next one:
harness-score v0.3.0 /work/my-app
Maturity: L2 · Guided Score: 66/108 (61%)
Context & Guides ████████████████░░░░ 80%
Skills & Commands █████████████░░░░░░░ 67%
Hooks & Guardrails ░░░░░░░░░░░░░░░░░░░░ 0%
...
To reach L3: sensors ≥ 60%; ci ≥ 50%Installing
npx harness-score # no install
npm install -g harness-score # global binary
npm install --save-dev harness-score # pinned devDependencyAlso mirrored on GitHub Packages (@paladini/harness-score) and JSR for Deno/Bun projects.
CLI reference
harness-score [path] # human report (default: current directory)
harness-score --json # full report as JSON
harness-score --md report.md # markdown report (use "-" for stdout)
harness-score --badge badge.svg # SVG pill: harness + detected level (L0–L4)
harness-score --min-level 3 # exit 1 if below L3 — the CI gate
harness-score --diff base.json # compare against a previous --json reportTracking score over time
--diff <file> compares the current scan against a baseline report saved from an earlier --json run — the level and score deltas, per-dimension movement, and exactly which checks flipped:
harness-score --json > baseline.json # save today's report
# ...later, after changes...
harness-score --diff baseline.json # see what moved Compared to baseline:
Level: L2 · Guided → L3 · Sensing (+1)
Score: 61/108 (56%) → 84/108 (78%) (+22pp)
Sensors & Feedback 20% → 90% (+70pp)
Newly passing: SNS-01, SNS-02, SNS-04, CI-01, CI-02--diff works with --json (adds current/baseline/diff to the payload) and --md (adds a "Compared to baseline" section) too — this is what the GitHub Action uses to post "harness score moved from L2 to L3" comments on pull requests.
The Cursor plugin
Install Harness Score from the Cursor Marketplace and you get:
/harness-audit— runs the scanner on the open workspace and has the agent present the report with its top remediations.- The
harness-engineeringskill — when you then say "fix it" or "improve my harness", the agent knows how to write the missing artifacts following this guide's recipes.
The analysis itself is always the deterministic CLI; the model only presents results and applies fixes you ask for.
The CI gate
Harnesses regress silently — someone deletes hooks.json in a cleanup, a rules file rots. Ratchet your level in CI:
- name: Harness gate
run: npx -y harness-score --min-level 3Or use the packaged action, which also emits the badge:
- uses: paladini/harness-score/action@main
with:
min-level: '3'
badge: 'harness-badge.svg'Show your maturity
Harness Score ships two branded SVG formats in the same visual language as the scanner's progress bars — no shields.io, no paid service, no network at render time:
| Format | Files | Shows | Best for |
|---|---|---|---|
| Badge | harness-badge.svg or badge-l0.svg … badge-l4.svg | harness · L4 | README row (112×20 pill) |
| Share card | card-l0.svg … card-l4.svg | Full banner with level name | Social posts, repo hero (860×240) |
The badge always shows only the level (L0–L4). Level names (Unharnessed, Guided, …) live on the share cards and in the scanner output.
The pill looks identical whether CI regenerates it or you pin a static file — only the wiring differs.
Badge — auto-updating (recommended)
harness-score --badge writes an SVG for whatever level the scanner detects. Wire it into CI once; the README image updates itself as your harness improves.
# .github/workflows/harness.yml
name: Harness Score
on: { push: { branches: [main] } }
permissions: { contents: write }
jobs:
harness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: paladini/harness-score/action@main
with: { badge: 'harness-badge.svg' }
- uses: JamesIves/github-pages-deploy-action@v4
with: { branch: badges, folder: ., clean: false }Reference the published file from your README — full copy-paste recipes in Embed snippets:
<img alt="Harness Score" src="https://raw.githubusercontent.com/<you>/<repo>/badges/harness-badge.svg" height="20">Set height="20" on the <img> so the pill lines up with npm/CI shields in the same row (112×20 SVG; level only — score percent stays in the CLI report).
Dogfood example (this guide's live badge on GitHub Pages):
Badge (this repo)
The matching share card for the detected level is published as harness-card.svg (currently L4 for this repository):
Badge — pin a level
Same pill, static file — pick badge-l0.svg … badge-l4.svg if you do not want CI to regenerate the image. See Embed snippets for Markdown, HTML, iframe, JSX, and more.
Share card
For a hero image or social post, use the banner card — it includes the level name (Unharnessed, Guided, …):
| Level | Badge | Share card |
|---|---|---|
| L0 · Unharnessed | badge-l0.svg | card-l0.svg |
| L1 · Documented | badge-l1.svg | card-l1.svg |
| L2 · Guided | badge-l2.svg | card-l2.svg |
| L3 · Sensing | badge-l3.svg | card-l3.svg |
| L4 · Self-correcting | badge-l4.svg | card-l4.svg |
All badge levels (112×20)
Share card example (860×240)
Download any level from the table above — cards include the level name.
Embed snippets
Copy-paste recipes for sharing. Replace placeholders:
| Placeholder | Auto-updating badge | Pinned badge (level {N}) | Share card |
|---|---|---|---|
{BADGE_URL} | https://raw.githubusercontent.com/{owner}/{repo}/badges/harness-badge.svg | https://paladini.github.io/harness-score/maturity/badge-l{N}.svg | — |
{CARD_URL} | — | — | https://paladini.github.io/harness-score/maturity/card-l{N}.svg |
{LINK} | Your repo or https://paladini.github.io/harness-score/ | Same | Same |
{N} is 0–4. This repository's live badge (no CI on your fork needed): https://raw.githubusercontent.com/paladini/harness-score/main/docs/public/harness-badge.svg
Badge size: 112×20 — always set height="20" (or height={20}) so the pill lines up with shields.io badges in the same row.
Badge — Markdown
Image only (GitHub, GitLab, dev.to — use HTML if plain ![]() stretches):
<img alt="Harness Score L4" src="{BADGE_URL}" height="20">Linked (clickable):
[]({LINK})Reference-style:
[![Harness Score][hs-badge]][hs-link]
[hs-badge]: {BADGE_URL}
[hs-link]: {LINK}Badge — HTML
<img alt="Harness Score L4" src="{BADGE_URL}" height="20" width="112">Linked:
<a href="{LINK}">
<img alt="Harness Score L4" src="{BADGE_URL}" height="20" width="112">
</a>Badge — iframe
For CMS or wikis that only allow iframes (not <img>):
<iframe
src="{BADGE_URL}"
title="Harness Score L4"
width="112"
height="20"
style="border:0;overflow:hidden"
></iframe>Badge — SVG object / embed
<object data="{BADGE_URL}" type="image/svg+xml" width="112" height="20">
<a href="{BADGE_URL}">Harness Score L4</a>
</object><embed src="{BADGE_URL}" type="image/svg+xml" width="112" height="20" />Badge — JSX / React
<a href="{LINK}">
<img
alt="Harness Score L4"
src="{BADGE_URL}"
height={20}
width={112}
style={{ verticalAlign: 'middle' }}
/>
</a>Badge — AsciiDoc
image:{BADGE_URL}[Harness Score L4,link={LINK},height=20]Badge — BBCode (forums)
[url={LINK}][img]{BADGE_URL}[/img][/url]Badge — direct URL
Paste in chat, Notion image block, Slack, Discord, or any tool that accepts a raw image URL:
{BADGE_URL}Share card — Markdown / HTML
Banner for README hero, blog posts, or social previews ({N} = 0–4):
[]({LINK})<a href="{LINK}">
<img
alt="Harness Score L4 · Self-correcting"
src="{CARD_URL}"
width="560"
style="max-width:100%;height:auto;border-radius:8px"
/>
</a>Share card — iframe
<iframe
src="{CARD_URL}"
title="Harness Score L4 · Self-correcting"
width="560"
height="157"
style="border:0;max-width:100%"
></iframe>Share card — direct URL
{CARD_URL}Worked example (pinned L3 badge)
<a href="https://paladini.github.io/harness-score/">
<img alt="Harness Score L3" src="https://paladini.github.io/harness-score/maturity/badge-l3.svg" height="20">
</a><iframe
src="https://paladini.github.io/harness-score/maturity/badge-l3.svg"
title="Harness Score L3"
width="112"
height="20"
style="border:0"
></iframe>shields.io fan? Your Action can also write a small JSON file and point a shields endpoint at it (
{ "schemaVersion": 1, "label": "harness", "message": "L3", "color": "brightgreen" }). The brand SVGs above are self-contained and need no third party.
The check catalog
Every check the scanner runs, with its remediation recipe. Check IDs are stable; the CLI links each failure to its entry here.
Context & Guides (20 pts)
CTX-01 · Agent context file present — 4 pts
An AGENTS.md (or CLAUDE.md) exists at the repository root. Fix: create AGENTS.md answering: what is this project, how do I build and test it, what conventions hold, what must I never touch. Recipe in chapter 3.
CTX-02 · Context file is substantive — 3 pts
≥20 meaningful lines and ≥2 headings — a stub scores nothing. Fix: cover layout, build & test commands, conventions, and no-go zones. Commands over descriptions; point to rules instead of pasting them.
CTX-03 · Cursor rules directory in use — 4 pts
At least one .mdc file under .cursor/rules/. Fix: start with one short always-on rule holding your non-negotiables, then add glob-scoped rules per area (api.mdc, testing.mdc).
CTX-04 · Rules have valid frontmatter — 3 pts
Every rule declares description, alwaysApply, or globs. Fix: add the frontmatter block; without it Cursor can't decide when the rule applies.
CTX-05 · Rules are scoped — 2 pts
Not every rule is alwaysApply: true. Fix: scope rules with globs: so they load only when relevant — every always-on rule taxes every request's context.
CTX-06 · No bloated rules — 2 pts
No single rule exceeds 500 lines. Fix: split by concern, or move procedural content into a skill.
CTX-07 · README present — 1 pt
Fix: add a README.md; it's the first orientation document for humans and a fallback for agents.
CTX-08 · No legacy .cursorrules — 1 pt
The deprecated single-file format is absent. Fix: migrate .cursorrules content into scoped .cursor/rules/*.mdc.
Skills & Commands (17 pts)
SKL-01 · At least one skill — 4 pts
A SKILL.md under .cursor/skills/<name>/ (or .agents/skills/). Fix: package your most repeated procedure (deploy, release, migration) as a skill — chapter 3.
SKL-02 · Skills declare name and description — 3 pts
Frontmatter with name: and description: on every skill. Fix: the agent decides whether to load a skill from these two fields alone; without them the skill is invisible.
SKL-03 · Slash commands defined — 3 pts
Markdown files under .cursor/commands/. Fix: encode workflows you trigger deliberately (/review, /release) as command files.
SKL-04 · Skill descriptions are trigger-worthy — 2 pts
Descriptions ≥40 characters. Fix: write descriptions as trigger conditions — "Use when the user asks to deploy or release; covers tagging, pipeline, rollback, smoke tests."
AGT-01 · Custom subagent defined — 3 pts
A markdown file under .cursor/agents/. Fix: package a purpose-built subagent for a job the primary agent should delegate (planning, review, release) — see Subagents in chapter 2.
AGT-02 · Subagents declare name and description — 2 pts
Frontmatter with name: and description: on every subagent definition. Fix: the parent agent decides whether to delegate from these two fields alone; without them the subagent is never invoked.
Hooks & Guardrails (14 pts)
HKS-01 · hooks.json present and valid — 4 pts
.cursor/hooks.json exists and parses as JSON. Fix: create it with {"version": 1, "hooks": {}} and grow from the recipes in chapter 5.
HKS-02 · Known events, version declared — 2 pts
version present; every registered event is a documented Cursor event. Fix: typo'd event names fail silently — check against the event list in chapter 2.
HKS-03 · Gate hook guards risky operations — 4 pts
A hook on beforeShellExecution, beforeMCPExecution, preToolUse, or beforeReadFile. Fix: add the destructive-command deny gate from chapter 5 — prose rules are requests; gates are facts.
HKS-04 · Feedback hook observes output — 2 pts
A hook on afterFileEdit, postToolUse, or similar. Fix: format-and-lint on edit gives the agent instant feedback inside the session.
HKS-05 · Hook scripts committed — 2 pts
Scripts referenced by hooks.json exist in the repository. Fix: commit them; a hook pointing at a missing script fails open on every machine but the author's.
Sensors & Feedback (20 pts)
SNS-01 · Test runner configured — 6 pts
A real test script/config (vitest, jest, pytest, go test, cargo test…). Fix: wire up the runner with one obvious entry point and document it in AGENTS.md — tests are how the agent verifies its own work.
SNS-02 · Linter configured — 5 pts
eslint/biome, ruff, golangci-lint, rubocop, or equivalent. Fix: every convention expressible as a lint rule stops needing prose.
SNS-03 · Type checking in place — 4 pts
tsconfig (ideally strict: true), mypy/pyright, or a statically typed language. Fix: the type checker is the only sensor that reviews every agent edit for free — chapter 4.
SNS-04 · Formatter configured — 3 pts
prettier/biome, black/ruff-format, gofmt/rustfmt. Fix: formatting noise in diffs hides real mistakes from review.
SNS-05 · Test files exist — 2 pts
At least one actual test file in the tree. Fix: a configured runner with zero tests is a green light nobody earned.
CI Feedback (14 pts)
CI-01 · CI pipeline configured — 4 pts
GitHub Actions workflow (or GitLab/CircleCI/Jenkins equivalent). Fix: add .github/workflows/ci.yml running your sensors on every push.
CI-02 · CI runs the tests — 4 pts
Fix: no agent-authored change should be mergeable without the suite firing.
CI-03 · CI runs lint/typecheck — 3 pts
Fix: cheap computational sensors belong on every push — keep quality left.
CI-04 · Pre-commit checks installed — 3 pts
husky/lint-staged, pre-commit, or lefthook. Fix: the earliest feedback a commit can get; catches what on-edit hooks missed before it enters history.
Hygiene & Safety (23 pts)
HYG-01 · .gitignore present — 2 pts
Fix: agents commit what they see; make build output and local state invisible.
HYG-02 · .gitignore covers env files — 3 pts
A .env pattern in .gitignore. Fix: add .env and .env.* (allow !.env.example) — the cheapest guardrail in existence.
HYG-03 · No unprotected .env files — 4 pts
No real env files in the tree unless gitignored (templates are fine). Fix: move secrets out; keep .env.example documenting required variables.
HYG-04 · MCP config free of credentials — 4 pts
No credential signatures in .cursor/mcp.json. Fix: use ${ENV_VAR} interpolation — an inlined key in mcp.json is a secret published to every clone.
HYG-05 · License present — 2 pts
Fix: add a LICENSE; required for open-source use and the Cursor Marketplace.
HYG-06 · No secrets in harness files — 2 pts
AGENTS.md, rules, and hooks config are clean of token signatures. Fix: these files are loaded into model context every session — a key there is exfiltrated by design.
HYG-07 · Lockfile committed — 3 pts
package-lock.json, uv.lock, Cargo.lock, go.sum, or equivalent. Fix: reproducible installs mean your sensors test the same dependency tree everywhere.
HYG-08 · MCP config uses env interpolation for credentials — 3 pts
.cursor/mcp.json is valid, and any credential-shaped field (token, key, secret, password…) uses ${ENV_VAR} interpolation rather than a literal. The positive complement to HYG-04 — a repo with no MCP setup earns nothing here, same as any other bonus check. Fix: reference secrets as "${VAR_NAME}" and document required variables in .env.example.
A worked improvement plan
Starting from a typical L0 product repo, one focused session per level:
- → L1 (an afternoon). Write
AGENTS.md(CTX-01/02). Include build/test commands even if the sensors are weak — the agent will use them. - → L2 (a day). Three scoped rules + one skill for your most repeated procedure (CTX-03…06, SKL-01/02). Fix hygiene: gitignore, env files, license (HYG-01…05).
- → L3 (the real work, if sensors are missing). Test runner + linter + strict types +
ci.ymlrunning all three (SNS-*, CI-01…03). If you already have them, this level is free. - → L4 (a morning). The two hooks from chapter 5 — one gate, one formatter — committed with their scripts (HKS-*), pre-commit (CI-04), then
--min-level 4in CI so it never regresses.