@duyquangnvx/harness
Shared repo configuration: Biome, tsconfig, lefthook and Ruff presets and a lint-on-edit hook for Claude Code. A repo extends the presets and keeps its own overrides in its own files; the package's harness command sets it up, checks it and upgrades it. A repo that publishes a package releases it with @duyquangnvx/release.
| File | What it is | A repo uses it as |
|---|---|---|
biome.preset.json | Formatter and the mechanical rules: any, !, enum, console, nested ternaries, complexity, file length, exhaustive switches | "extends": ["@duyquangnvx/harness/biome"] |
tsconfig.strict.json | strict plus the checks it leaves out | "extends": ["@duyquangnvx/harness/tsconfig.strict.json"] |
lefthook.yml | Pre-commit: Biome on staged files, then tsc --noEmit, both from node_modules/.bin under any package manager | extends: [node_modules/@duyquangnvx/harness/lefthook.yml] |
ruff.toml | Python line length and rule selection | extend = "node_modules/@duyquangnvx/harness/ruff.toml" |
lint-on-edit.mjs | Claude Code PostToolUse hook: formats and lints each edited file, exit 2 hands what is left back to Claude. Runs Biome only when it is installed in the repo, Ruff only when a Ruff config exists | the command below |
The Biome preset is flat, because a file Biome extends cannot extend another. Biome takes the specifier without .json.
The harness command
| Command | What it does |
|---|---|
init | Sets the repo up: below |
doctor [--json] | Says, changing nothing, what the git repo it runs in and this machine lack or have behind, each with the command or line that fixes it. The repo: the package against the latest on npmjs, Biome at the version the package pins, each config extending its preset, the lint hook, lefthook's git hook, an AGENTS.md and a CLAUDE.md importing it, and what the old copy-everything harness left. The machine: the rules link at ~/.claude/rules/harness, the duyquangnvx plugin, and Matt's skills at the tag this package names |
context [--json] | Says, changing nothing, what Claude Code loads at launch in the folder it runs in, with a rough token count each: instruction files and their imports, rules (those with paths: as on demand), and skill and subagent descriptions from ~/.claude, .claude and enabled plugins. It names the lines two loaded files both hold and any file loaded from above the repo root (ADR 0010) |
upgrade | Adds @duyquangnvx/harness@latest with the repo's package manager, sets Biome to the version it pins and prints what changed. A Python-only repo runs npx @duyquangnvx/harness@latest upgrade, which copies the hook and Ruff config again; it refuses when the package running is behind the latest on npmjs |
Run it as npx @duyquangnvx/harness <command> before the package is installed, and through the package manager after (pnpm exec harness doctor). init and upgrade refuse to write over uncommitted changes. harness <command> --help has the details.
| Exit | Meaning |
|---|---|
0 | nothing to fix |
1 | a usage error |
2 | something to fix in the repo or on the machine, which the output names |
70 | a bug |
Set up a repo
npx @duyquangnvx/harness init
In a repo with package.json and a lockfile, init adds the package and the tools its presets run to devDependencies with the package manager the lockfile names, makes lefthook install the prepare script when there is none, denies lefthook's install script as below, writes the configs below when they are missing, and adds the hook to .claude/settings.json, pointing a hook that runs the old copy .claude/hooks/lint-on-edit.mjs at the package instead. A repo with no TypeScript (no tsconfig.json, no typescript package and no .ts file git sees) gets neither typescript nor tsconfig.json, and doctor does not ask for a tsconfig.json. It never rewrites a file the repo already has: it prints the line that file needs and exits 2.
In every repo, Python-only too, init writes an AGENTS.md holding ## Project status (the greenfield default) and placeholder ## Commands, ## Coding standards and ## Agent skills sections, each only a comment saying what belongs there, and a CLAUDE.md holding only @AGENTS.md, so Claude Code and other agents read one file. A CLAUDE.md with content of its own gets how to move that into AGENTS.md. doctor checks that AGENTS.md exists, never what is in it: it holds only what an agent cannot find on its own, so commands that package.json or the README already give stay out.
By hand, the same setup is:
pnpm add -D @duyquangnvx/harness lefthook typescript
pnpm add -D --save-exact @biomejs/[email protected]
biome.json, adding the repo's own exceptions next to extends:
{
"$schema": "https://biomejs.dev/schemas/2.6.2/schema.json",
"extends": ["@duyquangnvx/harness/biome"]
}
tsconfig.json, in a repo with TypeScript: add "extends": ["@duyquangnvx/harness/tsconfig.strict.json"].
lefthook.yml. Jobs merge by name, and where both files set a key the package's value wins, so a repo adds its own jobs, or keys such as root, beside the package's:
extends:
- node_modules/@duyquangnvx/harness/lefthook.yml
The jobs run the tools from the repo's node_modules/.bin, so the preset works under npm and pnpm alike and never fetches a tool that is missing. pnpm exec fails in an npm repo, and npx adds about half a second a job and resolves a missing name on the registry.
.claude/settings.json; before the first install the package is missing, and the hook then exits 0:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "f=\"$CLAUDE_PROJECT_DIR/node_modules/@duyquangnvx/harness/lint-on-edit.mjs\"; [ -f \"$f\" ] || exit 0; node \"$f\""
}
]
}
]
}
}
A repo with Python next to package.json: ruff.toml holding the extend line above. A Python-only repo, one without package.json, has no node_modules: there init copies the hook to .claude/hooks/lint-on-edit.mjs and this config to .claude/harness.ruff.toml, adds node "$CLAUDE_PROJECT_DIR/.claude/hooks/lint-on-edit.mjs" as the hook in .claude/settings.json, and writes a ruff.toml holding extend = ".claude/harness.ruff.toml" when the repo has no Ruff config; a ruff.toml, .ruff.toml or pyproject.toml [tool.ruff] of its own gets the line printed instead.
Git hooks under npm 12 and pnpm 11
Both block lefthook's own postinstall, which is what used to install the git hooks. Measured with npm 12.2.0, pnpm 11.8.0 and lefthook 2.2.2:
- npm 12 installs the package but leaves
.git/hooksempty, warning that the script is "not covered by allowScripts". - pnpm 11 fails the install (
ERR_PNPM_IGNORED_BUILDS, exit 1) until the build is allowed or denied. - The lefthook binary comes from a platform optional dependency and runs without the
postinstall.
So the repo installs the hooks itself and denies lefthook's script, which works under both:
package.json:"scripts": { "prepare": "lefthook install" }, and under npm"allowScripts": { "lefthook": false }.- pnpm:
pnpm-workspace.yamlholdingallowBuilds:withlefthook: false.
lefthook install, and lefthook run, which syncs the hooks first, write to the repo's shared .git/hooks: an install in any worktree installs the hooks for all of them.
A pnpm repo also lists '@duyquangnvx/harness' under minimumReleaseAgeExclude in pnpm-workspace.yaml, so upgrade installs a release the day it ships instead of a day later.
Release this package
It releases from this folder with @duyquangnvx/release. The tests run against the built dist/: pnpm test builds first.