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 across any AI tool, and reports a maturity level with the exact gaps to the next one:
harness-score v1.0.0 /work/my-app
Maturity: L2 · Guided Score: 66/108 (61%)
Detected: Cursor, Claude Code
Context & Guides ████████████████░░░░ 80%
Skills & Commands █████████████░░░░░░░ 67%
Hooks & Guardrails ░░░░░░░░░░░░░░░░░░░░ 0%
...
To reach L3: sensors ≥ 60%; ci ≥ 50%Multi-tool: The scanner recognizes harness artifacts from Cursor, Claude Code, Windsurf, Cline, Continue, and other tools via OR semantics — if you configure any one tool, Harness Score counts it. Learn more in Multi-Harness Support.
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.
Using it as a library
The CLI is a thin wrapper around a fully-typed programmatic API — useful for a custom dashboard, a bot, or any tool that wants the raw Report instead of parsing terminal output:
import { score } from 'harness-score';
const report = score('/path/to/repo');
console.log(report.level.name, report.score.percent, report.dimensions);
// With global scopes: score(path, { scopeFlags: ['user'] })
console.log(report.effective.level.index);Report, Check, CheckResult, DimensionScore, LevelInfo, ScoreSnapshot, and every other shape ship as TypeScript declarations — resolved via an explicit "types" field, so editors and tsc pick them up with no extra configuration. Lower-level building blocks are exported too, for anything score() doesn't cover directly:
import { score, computeDiff, renderMarkdown } from 'harness-score';
const report = score('/path/to/repo');
const markdown = renderMarkdown(report); // same renderer the CLI's --md usesScan configuration
By default the scanner measures repository maturity only — the harness that travels with the code and reproduces in CI. Optionally include user-level or shared harness trees for an effective score (what the agent likely sees on a developer laptop).
{
"scopes": { "user": false, "system": false },
"extraRoots": [{ "id": "team-shared", "path": "../shared-harness" }],
"gate": "maturity"
}Save as .harness-score.json at the scan root, or pass --config <file>. Full key reference: Metrics & Codes — configuration.
harness-score --scope user # repo + ~/.cursor, ~/.claude, …
harness-score --scope user,system
harness-score --gate effective --min-level 2 # gate on effective scoreThe terminal report shows Maturity (repo) and Effective (when they differ). CI should keep gate: maturity unless you intentionally run on self-hosted runners with a populated user harness.
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 (uses gate mode)
harness-score --diff base.json # compare maturity against a previous --json report
harness-score --config .harness-score.json
harness-score --scope user # include user-level harness in effective score
harness-score --gate maturity # or effective — which score --min-level usesTracking 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 its plugin directory in this repo (the Cursor Marketplace listing is submitted and pending review — this link will move there once it's live) 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@main
with:
min-level: '3'
badge: 'harness-badge.svg'
# include-user-harness: 'true' # optional: effective score overlay
# gate: 'maturity' # default — official repo maturityShow 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@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 / GEMINI.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 · Scoped rules in use — 4 pts
At least one scoped rule file for any supported tool (e.g. .cursor/rules/*.mdc, .windsurf/rules/*.md, .clinerules/*.md, .continue/rules/*.md, .github/instructions/*.instructions.md, .agents/rules/*). Nested context files in subdirectories (AGENTS.md, CLAUDE.md, GEMINI.md anywhere below the root) also count — they are directory-scoped rules in tools like Claude Code and Codex. Fix: start with one short always-on rule holding your non-negotiables, then add path-scoped rules per area (or nested context files per subtree).
CTX-04 · Rules have valid frontmatter — 3 pts
Every rule declares activation metadata (description, globs/trigger/paths/applyTo, or alwaysApply). Rules a tool auto-loads without metadata — .continue/rules/* and nested context files — pass by construction. Fix: add the frontmatter block; without it the agent can't decide when the rule applies.
CTX-05 · Rules are scoped — 2 pts
Not every rule is blanket always-on. Nested context files count as scoped — they apply only to their subtree. Fix: scope rules to paths (globs:, trigger: glob, paths:, applyTo:) 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 (or modern scoped rules also exist). Fix: migrate .cursorrules content into scoped rules for your tool.
Skills & Commands (17 pts)
SKL-01 · At least one skill — 4 pts
A SKILL.md under .cursor/skills/<name>/, .claude/skills/<name>/, or .agents/skills/<name>/. 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 · Explicit workflows/commands defined — 3 pts
Command or workflow files (.cursor/commands/, .windsurf/workflows/, .claude/commands/, .continue/prompts/, .zed/commands/, .agents/workflows/). Fix: encode workflows you trigger deliberately (/review, /release) as command/workflow 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 subagent file under .cursor/agents/, .claude/agents/, or .opencode/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 configuration present and valid — 4 pts
.cursor/hooks.json or .claude/settings.json (hooks key) exists and parses as JSON. Fix: create hooks config and grow from the recipes in chapter 5.
HKS-02 · Known events, version declared — 2 pts
Version/metadata present; every registered event is documented for your tool (Cursor lifecycle events, or Claude Code PreToolUse/PostToolUse). 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 gate hook registered (Cursor: beforeShellExecution, beforeMCPExecution, preToolUse, or beforeReadFile; Claude Code: PreToolUse). 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 feedback hook registered (Cursor: afterFileEdit, postToolUse, …; Claude Code: PostToolUse). Fix: format-and-lint on edit gives the agent instant feedback inside the session.
HKS-05 · Hook scripts committed — 2 pts
Scripts referenced by the hooks config 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 MCP config (.cursor/mcp.json, .mcp.json, .agents/mcp_config.json). Fix: use ${ENV_VAR} interpolation — an inlined key in MCP config is a secret published to every clone.
HYG-05 · License present — 2 pts
Fix: add a LICENSE; required for open-source use and plugin marketplaces.
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
An MCP config file 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.