npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@anhnth24/harnessctl

v0.5.2

Published

Standalone enforcement overlay for Claude Code hooks — audit-grade JSONL, deterministic gates, profile-based policy

Readme

harnessctl v0.3.2

Standalone enforcement overlay for Claude Code hooks. Nudges, warns, or blocks on context, spec, and test violations — without forking or modifying Claude Code itself.


What's new in v0.3.2

  1. Per-event audit JSONL with passthrough fields. Every gate decision is captured in append-only JSONL with gate, pattern_id, language, framework, snippet. Auditable, greppable, queryable. Redaction is the gate's responsibility; the logger never auto-redacts.

  2. Config-as-policy. .harness/config.json lets teams cap gate severity, disable per-gate, or skip paths — without forking the harness. Profiles (strict/balanced/lenient) ship preset policies.

  3. Doctor visibility. harnessctl doctor shows effective gate config with source attribution (user-config | profile:<name> | gate-default) so teams see exactly why a gate behaves as it does.

  4. Three opinionated gates demonstrate the spine: hardcoded-path-blocker, test-meaningfulness-regex, comment-quality-heuristic. The audit platform is the point.

Migration note: New gates default to nudge in the balanced profile. Users with no .harness/config.json will see new annotations on their next session. Override any gate with .harness/config.json or set profile to lenient.

Policy-as-config example

The same gate can run at different severities across two team configs in the same monorepo:

// team-backend/.harness/config.json — strict on test quality
{ "hooks": { "test-meaningfulness-regex": { "enabled": true, "mode": "warn" } } }

// team-frontend/.harness/config.json — quieter while bootstrapping
{ "hooks": { "test-meaningfulness-regex": { "enabled": true, "mode": "nudge" } } }

Same gate, different cap, no fork. harnessctl doctor shows which team member's config is active.


What it is

harnessctl is a standalone hook overlay that plugs into Claude Code's hook system. It registers a single dispatcher (bin/harness-dispatcher.cjs) for all hook events, routes each event in-process to one or more gates, and returns a structured CC-protocol response (pass / nudge / warn / block).

Eight enforcement gates ship in v0.3.2:

| Gate | Hook event | What it checks | Default (balanced) | |---|---|---|---| | context-completeness | UserPromptSubmit | Prompt has ≥2 context signals (file path, identifier, plan ref, git ref, quoted error) | warn | | spec-discipline | PreToolUse (Edit/Write) | Recent messages reference docs/ or a plans/*.md before editing src/ | warn | | test-first | PreToolUse (Edit/Write) | A test file exists for the source file being edited | warn | | plan-adherence | PreToolUse (Edit/Write) | File is declared in the active plan phase | warn | | edit-loop | PreToolUse (Edit/Write) | Detects repeated edits to same file within session window | warn | | hardcoded-path-blocker | PreToolUse (Edit/Write) | Absolute user-specific paths written into source | warn | | test-meaningfulness-regex | PreToolUse (Edit/Write) | Weak assertions in Jest/pytest test files | nudge | | comment-quality-heuristic | PreToolUse (Edit/Write) | TODO/FIXME/HACK/XXX inline comments | nudge |

All gates fail open: a crash or timeout logs an error audit line and passes through silently. Error counts surface at session start and via harnessctl stats --errors.


Why (vs CK alone)

ClaudeKit provides slash-command workflows and skill orchestration. harnessctl adds per-event enforcement at the hook layer — before CC acts on a prompt or edit. It reads CK metadata from the filesystem only; no CK import, no fork, no coupling.

Use both together: CK for planning and orchestration, harnessctl for discipline.


Requirements

  • Node.js >= 18 (uses node:test, fs.copyFileSync, crypto)
  • Claude Code >= 1.x (smoke-tested on Claude Code as of 2026-05-10; see §Limitations #4)
  • Platforms: Windows, macOS, Linux

Install

# 1. Clone or copy this repo into any directory on your PATH or project tooling dir
git clone <repo-url> harnessctl
cd harnessctl

# 2. Run init from your project root (the directory with .claude/settings.json)
node /path/to/harnessctl/bin/harnessctl.cjs init --profile=balanced

# 3. Restart Claude Code — hooks are now active

init is idempotent: re-running with the same profile skips existing config. Use --force to overwrite. Use --dry-run to preview without writing.

Profiles: strict | balanced (default) | lenient


Profiles

Three preset profiles ship in lib/profiles/. They control the mode for each gate:

| Profile | context-completeness | spec-discipline | test-first | plan-adherence | edit-loop | hardcoded-path-blocker | test-meaningfulness-regex | comment-quality-heuristic | |---|---|---|---|---|---|---|---|---| | strict | block | block | block | block | block | block | warn | nudge | | balanced | warn | warn | warn | warn | warn | warn | nudge | nudge | | lenient | off | off | off | disabled | warn | off | off | off |

Modes:

  • block — CC denies the tool use or adds a correction notice; the user must fix and retry
  • warn — writes a warning to stderr; CC continues normally
  • off — gate is skipped entirely (bypass logged to audit)

To customize per-gate, edit .harness/config.json directly after init:

{
  "schema_version": 1,
  "profile": "balanced",
  "hooks": {
    "context-completeness":      { "enabled": true, "mode": "warn" },
    "spec-discipline":           { "enabled": true, "mode": "block" },
    "test-first":                { "enabled": true, "mode": "warn" },
    "hardcoded-path-blocker":    { "enabled": true, "mode": "warn",  "skip_paths": ["docs/", ".github/"] },
    "test-meaningfulness-regex": { "enabled": true, "mode": "nudge" },
    "comment-quality-heuristic": { "enabled": true, "mode": "off" }
  },
  "overrides": {}
}

skip_paths uses prefix match only in v0.3.2 (e.g. "docs/" matches any path starting with docs/). Suffix (.snap), exact-filename (package-lock.json), and internal-slash prefix (.github/workflows) are also supported. Glob characters (*, ?) are not supported and emit a warning at load time.


Hooks reference

context-completeness (UserPromptSubmit)

Scores the submitted prompt for context signals:

| Signal | Example | |---|---| | file_path | src/parser.ts, README.md | | code_identifier | parseToken(), user_id | | plan_reference | plans/my-feature/plan.md | | git_ref | abc1234, feature/auth | | quoted_error | `TypeError: cannot read properties` (≥20 chars) |

Decision matrix:

signals >= 2  → pass
signals == 1  → nudge (inject additionalContext hint)
signals == 0  → warn (balanced) | block (strict)
same prompt submitted >2 times → escalate to block regardless of mode

Opt-out: include [no-context] anywhere in the prompt.

Example nudge message:

Vague prompt detected. Add: a file path, an identifier, or a plan reference.


spec-discipline (PreToolUse: Edit, Write, MultiEdit)

Fires when editing src/** or top-level .ts/.cjs/.js files (excluding test files). Scans the last 5 user messages for a reference to docs/ or plans/*.md.

Example warn message:

No spec reference found before editing src/parser.ts. Add a docs/ or plans/*.md reference, or include [no-spec] to opt out.

Opt-out: include [no-spec] in a recent message.


test-first (PreToolUse: Edit, Write, MultiEdit)

Fires when editing src/** non-test files. Checks the filesystem for a sibling *.test.* or *.spec.* file matching the target basename.

Example warn message:

Create test for parser first

Opt-out: include [no-test] in a recent message (checked via transcript_path).


plan-adherence (PreToolUse: Edit, Write, MultiEdit)

Fires when editing any file while an active plan (status: in-progress) exists in plans/. Checks whether the target file is declared in the active phase's create | modify | delete file list. Files declared under read are NOT gated (read-only access is unrestricted).

Gate behavior:

| Condition | Decision | |-----------|----------| | No plan.json found / no in-progress plan | pass (fail-open) | | No in-progress phase in the active plan | pass | | File is in the active phase file set | pass | | File NOT in set, balanced profile | nudge (warn in user turn) | | File NOT in set, strict profile | block (deny tool use) | | Gate disabled (lenient profile) | pass |

Profile defaults:

| Profile | plan-adherence | |---------|----------------| | strict | block | | balanced | warn | | lenient | disabled |

Opt-out: edit phase markdown to add the file, re-run harnessctl plan-init, and commit. Ad-hoc bypass: not implemented in v0.3 — use mode: "warn" in .harness/config.json temporarily.

Path normalization: handles Windows backslash vs forward slash. Compares case-sensitively (no lowercase conversion — would break WSL case-sensitive mounts).

See docs/plan-json-schema.md for full schema and author workflow.


plan-init command (v0.3)

node bin/harnessctl.cjs plan-init <plan-dir> [--check] [--force]

Derives plan.json from all phase-*.md files in <plan-dir> by parsing the ## Related Code Files section in each phase file.

Usage:

# Generate (or regenerate) plan.json
harnessctl plan-init plans/my-feature

# Dry-run: check if plan.json is in sync (exit 1 if stale — use in CI)
harnessctl plan-init plans/my-feature --check

Idempotent: re-running on unchanged markdown produces byte-identical plan.json.

Ambiguous paths: bullet lines with no path-like characters (/, \, or file extension) emit a warning to stderr and are skipped. Fix the markdown and re-run.

Phase markdown format (in each phase-XX-name.md):

## Related Code Files

### Create
- lib/new-file.cjs

### Modify
- lib/existing.cjs

### Delete
- lib/old-file.cjs

### Read
- docs/reference.md

See docs/plan-json-schema.md for full schema documentation.


replay command (v0.3)

node bin/harnessctl.cjs replay <session_id> [--format=json] [--out=<path>] [--project=<slug>]

Reads a Claude Code session transcript and cross-references the .harness/audit/ log to produce a structured JSON file — useful for post-mortems, compliance reviews, and debugging.

Options:

| Flag | Default | Description | |---|---|---| | --format=json | json | Output format. Only json supported in v0.3. | | --out=<path> | ./replay-{session_id}.json | Output file path. | | --project=<slug> | derived from CWD | CC project slug (folder name under ~/.claude/projects/). |

Format deferral: --format=html and --format=md are reserved for v0.3.5 (HTML 3-panel renderer and Markdown narrative). Using them in v0.3 prints a deferral message and exits 1.

JSON output schema (v0.3-stable)

{
  "schema_version": 1,
  "session_id": "abc123...",
  "transcript_path": "/Users/you/.claude/projects/my-project/abc123.jsonl",
  "generated_at": "2026-05-10T12:00:00.000Z",
  "turns": [
    {
      "ts": "2026-05-10T12:00:00.000Z",
      "user_prompt": "Please edit src/index.ts to add a logger.",
      "tool_calls": [
        { "tool": "Edit", "tool_input": { "file_path": "src/index.ts", "..." : "..." } }
      ],
      "decisions": [
        { "gate": "spec-discipline", "decision": "nudge", "audit_ts": "2026-05-10T12:00:01.000Z" },
        { "gate": "plan-adherence", "decision": "pass",  "audit_ts": "2026-05-10T12:00:01.100Z" }
      ]
    }
  ]
}

Decision matching: each turn's decisions array contains audit lines whose ts falls within ±5 seconds of the turn's timestamp and share the same session_id. If no audit lines match a turn, decisions is [] and audit_match: null is set on the turn. Pre-harness sessions (no audit log) will have decisions: [] for all turns — v0.3 does not re-run gates on historical transcripts. In-process fallback is deferred to v0.3.5.

Security note: replay output may contain the full text of user prompts. Treat output files as sensitive — equivalent to your audit log.

Post-mortem workflow

# 1. Find session_id from last session
harnessctl stats --json | jq '.sessions[-1].session_id'

# 2. Generate replay JSON
harnessctl replay <session_id> --out ./postmortem.json

# 3. Inspect with jq
jq '.turns[] | select(.decisions | length > 0)' postmortem.json

# 4. HTML rendering (v0.3.5)
# harnessctl replay <session_id> --format=html --out ./postmortem.html

v0.3.5 HTML preview note

v0.3.5 will add --format=html (3-panel single-file renderer: timeline | turn detail | audit diff) and --format=md (Markdown narrative). The v0.3 JSON output is the stable data layer both renderers will consume. Pin tooling against schema_version: 1.


/harness toggle

Inside a Claude Code session, use the slash command to toggle gates on/off for the current session (does not persist across restarts):

/harness off context-completeness   # disable for this session only
/harness on  spec-discipline        # re-enable
/harness status                     # show current toggles

Implemented via hooks/harness-toggle.cjs. Toggle state is stored in .harness/state/{session_id}.json and isolated per session.


stats command

node bin/harnessctl.cjs stats [--since=7d] [--errors] [--perf] [--hot-files] [--acceptance]

| Flag | Description | |---|---| | --since=7d | Time window: 7d, 30d, all (default: 7d) | | --errors | Show fail-open event count and details | | --perf | Show p50/p99 latency per gate | | --hot-files | Top 10 most-edited file paths (from audit) | | --acceptance | Block acceptance rate (blocks not bypassed / total) |

All metrics are derived from .harness/audit/{date}.jsonl — no separate counter storage.

tune command (v0.4 — BETA)

Analyzes recent audit JSONL and suggests .harness/config.json deltas based on acceptance/bypass signals. Print-only — never writes config.

node bin/harnessctl.cjs tune --experimental-thresholds [--since=30d] [--min-fires=50] [--json]

BETA — uncalibrated thresholds. Default thresholds (bypass>=80%, accept>=90%, fires<5) are unvalidated guesses. The command requires --experimental-thresholds to run. Treat output as a starting point for review, not as automatic policy.

| Flag | Description | |---|---| | --experimental-thresholds | Required. Opt-in to BETA thresholds | | --since=30d | Window: 7d, 30d, all (default 30d) | | --min-fires=50 | Suppress suggestions for gates with fewer fires (default 50) | | --json | Machine-readable JSON output |

Suggestion rules (v1)

| Condition | Suggestion | |---|---| | error_rate >= 5% AND fires >= 10 | Flag for investigation (no mode change) | | fires < 5 | Flag "below noise floor — review value" | | bypass_rate >= 80% AND blocks ≥ 10 AND fires >= min-fires | Downgrade block→warn, warn→nudge, nudge→off | | accept_rate >= 90% AND mode is nudge/warn AND fires >= min-fires | Upgrade nudge→warn, warn→block |

Bypass detection: block decision followed by a pass on the same target_path, same session_id, within 5 minutes. Disabled-gate bypass events (reason: mode_off | user_toggled_off) are filtered out — they are NOT policy bypasses.

Read-only and offline. The command never writes config, never shells out, never echoes audit snippet content.

Plugin SDK (v0.4.1 — experimental)

Custom team gates without forking. Drop a .cjs file under .harness/gates/, opt it in via .harness/config.json, done. Full reference: docs/plugin-sdk.md.

Experimental. Contract schemaVersion: 1 may change before v0.5 freezes it. Plugins are full-trust code — the opt-in flag is the trust boundary, not a sandbox.

Quick start (reference plugin)

cp examples/plugins/no-console-in-api.cjs .harness/gates/

Edit .harness/config.json:

{
  "plugins": {
    "totalBudgetMs": 100,
    "gates": {
      "no-console-in-api": { "enabled": true, "mode": "warn", "schemaVersion": 1 }
    }
  }
}

Run harn doctor to verify it loaded. The plugin warns when Edit/Write/MultiEdit introduces console.log( under src/api/.

Author your own

// .harness/gates/<name>.cjs
module.exports = {
  name:          'no-console-in-api',
  event:         'PreToolUse',
  defaultMode:   'warn',
  schemaVersion: 1,
  async run(payload, ctx) {
    // ctx = { sessionId, hookName, mode, payload, pluginConfig, pluginName }
    // Intentionally narrowed: NO root, NO full config, NO state.
    return { decision: 'pass' };
  },
};

Runtime guarantees

  • Plugin name collisions with built-ins are rejected (no silent replace).
  • Async timeouts and throws are isolated (Promise.race 50ms + try/catch).
  • Sync while(true){} will hang CC — documented limitation; v0.5 adds worker_threads.
  • Per-event plugins.totalBudgetMs cap short-circuits late plugins on chain explosion.
  • harn init --force preserves the plugins section across re-runs.

Uninstall

node bin/harnessctl.cjs uninstall

Removes harness hook entries from .claude/settings.json. The .harness/ directory and all audit/state data are preserved. Re-run init to re-enable.


FAQ

Q: Does harnessctl modify Claude Code itself? No. It registers hooks via .claude/settings.json. Claude Code invokes the dispatcher as a subprocess on each hook event. No CC source is modified.

Q: Will it break my existing hooks? No. installHooks merges non-destructively — existing hook entries for other commands are preserved. Harness entries are identified by their command string and never duplicated.

Q: What happens if the dispatcher crashes? It exits 0 (fail-open). The crash is recorded in .harness/audit/ with decision:error. At the next SessionStart, harnessctl injects a 1-line warning with the 24h error count. Run harnessctl stats --errors for details.

Q: Can I use a custom profile? Not via a CLI flag. Edit .harness/config.json directly after init. The profile field is informational only once you've customized gate modes.

Q: Does it work offline / without internet? Yes. All gates are heuristic-only in v0.2 — no LLM calls, no network I/O.

Q: How do I report bugs or false positives? File an issue or edit .harness/config.json to set the noisy gate to "mode": "warn". Run harnessctl stats --acceptance after a week to measure false-positive impact.


§Limitations

All known limitations of v0.2, enumerated explicitly:

  1. Heuristic false positivescontext-completeness and spec-discipline are regex-based heuristics; expect ~10-20% false-positive rate. Default balanced profile uses warn mode (not block) to mitigate user friction.

  2. Fail-open philosophy — Gates that crash or time out pass through silently. The audit records a decision:error line. Fail-open count is surfaced via harnessctl stats --errors and injected as a 1-line warning at SessionStart if errors occurred in the last 24h. Use this to detect gate regressions early.

  3. No-LLM caveat — All signals in v0.2 are heuristic (regex + filesystem). An LLM-based classifier for vague-prompt detection is deferred to v0.3. Expect higher FP/FN rates on ambiguous prompts compared to a semantic approach.

  4. Claude Code version drift — harnessctl was smoke-tested against Claude Code as of 2026-05-10. Newer CC versions may add hook payload fields not handled here (e.g., new source enum values for SessionStart). Unknown source values default to startup behavior; other new fields are ignored. Fail-open applies if the dispatcher crashes on unexpected input.

  5. Single-instance state isolation — Each CC session has its own state file (.harness/state/{session_id}.json). Two terminal windows in the same repo do not share toggles. Running /harness off context-completeness in session A leaves session B unaffected. This is intentional — per-session isolation prevents cross-session interference.

  6. Windows path quirks — Long paths, Unicode filenames, and locked files can cause fs operations to fail. The dispatcher tolerates these via fail-open. The settings-writer uses copyFileSync + unlinkSync (not renameSync) to avoid EPERM errors when overwriting existing files on Windows. The mtime-cache invalidation in test-discovery.cjs may behave differently on NTFS if two file writes occur within the same filesystem timestamp granularity window.

  7. Plan-adherence (v0.3) — The plan-adherence gate is active as of v0.3. It requires plan.json (generated by harnessctl plan-init) to function. Without a plan.json or an in-progress plan, the gate passes through silently (fail-open). FP target is <5% over 1-week dogfood.

  8. Edit-loop not enforced — The edit-loop counter gate was replaced by passive audit

    • harnessctl stats --hot-files. Files with high edit counts surface via --hot-files for manual review. No active block is issued for repeated edits to the same file in v0.2.

Standalone, no CK fork

harnessctl is an independent tool. It does not fork, patch, or import ClaudeKit. It reads CK skill metadata from the filesystem only (e.g., ~/.claude/skills/*/SKILL.md) for informational purposes. Zero claudekit or @ck/ imports across the entire codebase.

A commercial preset pack (SOC2/ISO compliance profiles) is reserved for a future SKU. The current v0.2 release is internal-only. The audit schema (v:1) and profile JSON format are kept stable and open to preserve that optionality.


doctor command (v0.3)

node bin/harnessctl.cjs doctor

Runs 6 health checks and prints one line per check (✓ green / ✗ red / ! yellow). Print-only: never modifies settings.json. Exit 0 if all green; exit 1 if any red.

| # | Check | Detects | |---|---|---| | 1 | Dispatcher binary exists | Missing bin/harness-dispatcher.cjs | | 2 | .harness/audit/ writable | Permissions preventing audit writes | | 3 | .harness/config.json schema valid | Invalid or missing config | | 4 | Profile JSONs parseable | Corrupt profile JSON files | | 5 | settings.json harness entries present | Entries wiped by CC init/update or other tool | | 6 | Harness regex matchers compile | Invalid regex in _harness-marked entries |

On red drift, prints: Run 'harnessctl init' to re-merge harness entries.

--fix auto-heal is deferred to v0.3.5 (requires file-lock story). Passing --fix prints a deferral message and exits 1.

Drift detection at SessionStart

Every SessionStart event runs a fast-path drift check (<50ms p99). If harness entries are missing or corrupted, injects a one-line warning into CC's additionalContext:

⚠ harness drift detected: run 'harnessctl doctor' for details.

Warn-once per session: state flag at .harness/session-state/{session_id}.json { drift_warned: true } suppresses repeat warnings within the same CC session.


_harness marker convention (v0.3)

Every harness-injected hook entry in .claude/settings.json carries a _harness field:

{
  "command": "node \"/path/to/bin/harness-dispatcher.cjs\"",
  "_harness": "0.3.0"
}

The _harness field stores the harness semver that wrote the entry. It serves as the identity marker for drift detection — doctor identifies "missing" (no entry), "corrupted" (entry present but marker stripped), and "foreign" (marker on non-dispatcher command).

CC compatibility: Empirically confirmed on CC v2.1.133 — CC passes through unknown JSON keys verbatim. The marker survives claude config list, claude -p, and normal CC ops.


_order convention (v0.3, documentation only)

Teams may add _order: N to hook entries to communicate intended execution order across multiple tools sharing the same event. Recommended ranges:

| Range | Purpose | |---|---| | 0–29 | System / harness hooks (harness uses this range) | | 30–69 | User / team hooks | | 70–99 | Cleanup / post-processing hooks |

_order is not enforced by harnessctl code. CC respects array insertion order natively. This is a README convention for human readability and multi-tool coordination.


Min CC version (v0.3)

Minimum tested: Claude Code v2.1.133 (smoke-tested 2026-05-10).

Empirical findings on v2.1.133:

  • _harness marker survives all normal CC ops (confirmed via researcher-260510-1755)
  • Hook entries accumulate within arrays (no replacement by CC update)
  • Cross-scope hooks (user + project) fire sequentially, in-order

Newer CC versions may add hook payload fields not handled here. Unknown fields are ignored; unknown source values on SessionStart default to startup behavior. Fail-open applies on dispatcher crash.

Doctor v0.3.5 will add CC version mismatch detection on SessionStart.


Known limitations (v0.3 additions)

  1. settings.json write race vs concurrent CC — The atomic writer uses writeFileSync(tmp) → copyFileSync(tmp, target) → unlinkSync(tmp) (not renameSync, which throws EPERM on Windows when the target exists). This is best-effort, not a hard lock. If CC is writing settings.json concurrently, a race window exists. Mitigation: run harnessctl init only when Claude Code is not active. Auto-heal (--fix) is deferred to v0.3.5 until a file-lock acquisition story exists.

  2. Doctor print-only in v0.3harnessctl doctor --fix is deferred to v0.3.5. On detected drift, run harnessctl init to re-merge. Doctor only reports; it never writes to settings.json in v0.3.


Edit-loop gate (v0.3 M2)

The edit-loop gate fires on PreToolUse for Edit, Write, and MultiEdit tools. It counts how many times the same file has been edited within a rolling window — purely by reading the append-only audit JSONL. No state files are written; the gate is race-free.

Behavior

  1. On each edit-type tool invocation, the gate reads audit JSONL for the target file within the configured window_minutes window (capped at 1000 events / scan for performance).
  2. If edit_count > threshold, it also scans the same window for any Bash tool event whose command field matches a test-runner pattern (vitest, jest, pytest, cargo test, go test, mocha, playwright test, node --test).
  3. Decision:
    • edit_count <= thresholdpass
    • edit_count > threshold AND test event found → pass
    • edit_count > threshold AND no test event → nudge (warn mode) or block (block mode)

MultiEdit semantics

One MultiEdit invocation = one audit event, regardless of how many edits it contains. Threshold math applies to invocation count, not individual edit count within a MultiEdit batch.

Profile defaults

| Profile | mode | threshold | window_minutes | |----------|-------|-----------|----------------| | strict | block | 3 | 30 | | balanced | warn | 5 | 60 | | lenient | warn | 10 | 120 |

Custom config

{
  "hooks": {
    "edit-loop": {
      "enabled": true,
      "mode": "warn",
      "threshold": 5,
      "window_minutes": 60
    }
  }
}

stats --hot-files and --perf

harnessctl stats --hot-files [--since=1h|30m|7d|all]
harnessctl stats --perf [--since=7d]

--hot-files aggregates target_path from audit log; prints top 10 by edit count.

--perf prints p50/p99 latency per gate from duration_ms field (recorded with perf_hooks.performance.now() precision).

The --since flag accepts:

  • Nd — N days (e.g. 7d, 30d)
  • Nh — N hours (e.g. 1h, 6h)
  • Nm — N minutes (e.g. 30m, 90m)
  • all — no cutoff