@anhnth24/harnessctl
v0.5.2
Published
Standalone enforcement overlay for Claude Code hooks — audit-grade JSONL, deterministic gates, profile-based policy
Maintainers
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
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.Config-as-policy.
.harness/config.jsonlets teams cap gate severity, disable per-gate, or skip paths — without forking the harness. Profiles (strict/balanced/lenient) ship preset policies.Doctor visibility.
harnessctl doctorshows effective gate config with source attribution (user-config|profile:<name>|gate-default) so teams see exactly why a gate behaves as it does.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
nudgein thebalancedprofile. Users with no.harness/config.jsonwill see new annotations on their next session. Override any gate with.harness/config.jsonor set profile tolenient.
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 activeinit 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 retrywarn— writes a warning to stderr; CC continues normallyoff— 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 modeOpt-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
parserfirst
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 --checkIdempotent: 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.mdSee 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.htmlv0.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 togglesImplemented 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-thresholdsto 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: 1may 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.race50ms + try/catch). - Sync
while(true){}will hang CC — documented limitation; v0.5 adds worker_threads. - Per-event
plugins.totalBudgetMscap short-circuits late plugins on chain explosion. harn init --forcepreserves thepluginssection across re-runs.
Uninstall
node bin/harnessctl.cjs uninstallRemoves 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:
Heuristic false positives —
context-completenessandspec-disciplineare regex-based heuristics; expect ~10-20% false-positive rate. Defaultbalancedprofile useswarnmode (not block) to mitigate user friction.Fail-open philosophy — Gates that crash or time out pass through silently. The audit records a
decision:errorline. Fail-open count is surfaced viaharnessctl stats --errorsand injected as a 1-line warning atSessionStartif errors occurred in the last 24h. Use this to detect gate regressions early.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.
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
sourceenum values forSessionStart). Unknownsourcevalues default to startup behavior; other new fields are ignored. Fail-open applies if the dispatcher crashes on unexpected input.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-completenessin session A leaves session B unaffected. This is intentional — per-session isolation prevents cross-session interference.Windows path quirks — Long paths, Unicode filenames, and locked files can cause
fsoperations to fail. The dispatcher tolerates these via fail-open. The settings-writer usescopyFileSync + unlinkSync(notrenameSync) to avoidEPERMerrors when overwriting existing files on Windows. The mtime-cache invalidation intest-discovery.cjsmay behave differently on NTFS if two file writes occur within the same filesystem timestamp granularity window.Plan-adherence (v0.3) — The plan-adherence gate is active as of v0.3. It requires
plan.json(generated byharnessctl plan-init) to function. Without aplan.jsonor an in-progress plan, the gate passes through silently (fail-open). FP target is <5% over 1-week dogfood.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-filesfor 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 doctorRuns 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:
_harnessmarker 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)
settings.json write race vs concurrent CC — The atomic writer uses
writeFileSync(tmp) → copyFileSync(tmp, target) → unlinkSync(tmp)(notrenameSync, which throws EPERM on Windows when the target exists). This is best-effort, not a hard lock. If CC is writingsettings.jsonconcurrently, a race window exists. Mitigation: runharnessctl initonly when Claude Code is not active. Auto-heal (--fix) is deferred to v0.3.5 until a file-lock acquisition story exists.Doctor print-only in v0.3 —
harnessctl doctor --fixis deferred to v0.3.5. On detected drift, runharnessctl initto re-merge. Doctor only reports; it never writes tosettings.jsonin 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
- On each edit-type tool invocation, the gate reads audit JSONL for the target file within
the configured
window_minuteswindow (capped at 1000 events / scan for performance). - If
edit_count > threshold, it also scans the same window for anyBashtool event whosecommandfield matches a test-runner pattern (vitest,jest,pytest,cargo test,go test,mocha,playwright test,node --test). - Decision:
edit_count <= threshold→ passedit_count > thresholdAND test event found → passedit_count > thresholdAND 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
