Skip to content

Releases: LanNguyenSi/harness

v0.41.0

Choose a tag to compare

@github-actions github-actions released this 16 Jul 02:10
8de6841

Security

  • The understanding-gate approval marker (and its branch-protection twin) is now HMAC-signed, closing the "existence is enough" filesystem-marker forgery hole — the persisted-report approval path is a separate, still-unsigned residual, see below (task f9485cc7, M10 / Feature idea #7, promised in the [0.32.0] entry's "cryptographic marker signing... is a tracked follow-up" note). The marker's integrity used to rest entirely on an UNENFORCED invariant — "no configured MCP exposes a filesystem-write primitive" — checked nowhere; one future MCP tool with local file-write would silently reopen it, since its tool name wouldn't match the Edit|Write|Bash blocker matcher, and the marker's contract was "existence is enough" (a bare filesystem-write forged it). harness approve understanding now writes an HMAC-SHA256 signature over (markerId, approvedAt, approvedBy, reportContentHash)reportContentHash is the sha256 of the persisted Understanding Report bound to this approval; this is groundwork only, NOT yet enforced at gate-check time (the live cross-check against the currently-selected report is the C1 staleness follow-up, task fa423e9b) — using an operator-side key lazily generated (crypto.randomBytes(32), mode 0600) at <generatedDir>/.approval-signing.key. checkApprovalMarker (shared by the session marker, task-scoped markers, and checkBranchProtectionMarker) now REJECTS a marker with a missing or invalid signature — matched:false, same as no marker, but with a distinct forged/unsigned marker rejected diagnostic surfaced through both the Claude and Codex PreToolUse hooks and the branch-protection hook, so an operator/auditor can tell an active forgery attempt (or a pre-signing legacy marker) apart from the routine "never approved" case. Binding markerId into the signature also means the "copy a marker onto a new session id" admin trick this doc used to describe no longer works — a copied marker fails verification. Back-compat is strict, no migration window: a marker written before this shipped has no signature field and is rejected exactly like a forgery; re-run harness approve understanding (and/or harness approve branch-protection) once after upgrading a machine with a live approval.
    • What is NOT closed by this task (read before assuming more than it delivers). (1) The persisted-report approval path (checkPersistedReport, consulted by both PreToolUse hooks with EQUAL authority immediately after a forged marker is correctly rejected) is not signed at all and, under this task's own threat model (a future write primitive the Edit|Write|Bash matcher doesn't cover), is an equal-or-easier forgery target than the marker used to be: no signing key to read, no sessionId match required (the gate-read path's tolerant fallback is "any", unrestricted by age), just a JSON file with approvalStatus: "approved" dropped into the reports dir. Tracked as follow-up task 7402301d-14bb-4b03-8d88-a2bb8bad8084. (2) The solution-acceptance pack's verdict marker remains unsigned (tracked separately, [0.32.0] note). (3) An agent with BOTH arbitrary local file-read (true today, by design — the blocker never gates Read) AND a write primitive the Edit|Write|Bash matcher doesn't cover can still forge a validly-signed marker: read the key, compute the HMAC, write the result through the uncovered path — signing does not close that combined case, only the narrower, previously totally-undefended write-only-forgery case, and it makes any attempt materially more expensive and forensically visible. Given the agent and operator share one OS user account, this is deliberate defense-in-depth, not a hard authorization boundary; full write-up in src/runtime/approval-signing.ts and docs/policy-packs/understanding-before-execution.md "Marker signing". (4) An interim MCP-catalog write-capability warning at init/smoke time was considered but did not fall out naturally from this change and is left as a candidate follow-up.
    • New tests: tests/runtime/approval-signing.test.ts (key generation/persistence/corruption-recovery/rotation, sign/verify round-trip, tamper + wrong-markerId + missing-field rejections), forgery + tamper-mutation regressions in tests/policy-packs/runtime.test.ts, tests/io/read-regular-file-unreadable.test.ts, tests/cli/pack-hook-pre-tool-use.test.ts, tests/cli/pack-hook-codex-pre-tool-use.test.ts, and tests/cli/pack-hook-branch-protection.test.ts.

Added

  • Warn-only OKF staleness watch on every PR (.github/workflows/okf-staleness.yml, PR #350): runs okf-kit check (exact pin 0.3.1) against docs/okf, surfaces STALE/structural findings as per-doc annotations plus a full job summary, never blocks a merge; tool errors fail red so a broken check cannot look green. Canonical pattern for the five OKF bundle repos; never mark it required in branch protection.
  • harness pack reseed <name> pulls a pack's shipped config.ux (and config.producers) into an already-installed manifest (task 68b9ad9c). Before this, a deny-message wording fix in the init templates (e.g. the heredoc submission form, agent-tasks/e48e3b45) only reached manifests generated by a fresh harness init AFTER the fix shipped: harness apply only ever projects the manifest OUT to settings.json, nothing propagated a template fix back INTO an operator's existing policy_packs[].config.ux, so an already-installed manifest kept teaching stale wording indefinitely, even after the CLI itself was upgraded. harness doctor now also warns when an enabled pack's config.ux / config.producers textually diverges from the CLI's shipped default for that pack — compared against the pack's OWN configured mode (understanding-before-execution's required: line varies by mode), not a hardcoded one, so switching modes is never mistaken for drift. Both the warning and the reseed write share one canonical source per pack (defaultUx() / defaultProducers() in src/policy-packs/builtin/understanding-before-execution.ts and src/policy-packs/builtin/branch-protection.ts, wired through the new resolveBuiltinDefaultConfig in src/policy-packs/registry.ts), so the check and the fix can never independently drift on what "the shipped template" means; the init Custom composer (src/cli/init/composer.ts) now reads from the same source instead of carrying its own copy of the literal text. reseed is deliberately explicit-only — never invoked by apply, doctor, or any automatic path — and only ever touches config.ux / config.producers, leaving every other key on the pack entry (mode, approval_lifecycle, permission_profile, min_version, ...) untouched, so an operator's own deliberate ux customisation is never silently clobbered by an upgrade (the same reasoning harness adopt exists for in the opposite direction). --dry-run prints the diff without writing; a pack whose config.ux already matches the shipped default is a no-op. solution-acceptance has no registered shipped default (ships enabled: false with no config: block in any template) and is reported clearly as such rather than silently reseeded with nothing. A new parity test, tests/cli/init-templates-ux-parity.test.ts, pins FULL_TEMPLATE / SOLO_TEMPLATE / TEAM_TEMPLATE (src/cli/init/templates.ts, src/cli/init/profiles.ts) and the Custom composer's config.ux / config.producers against defaultUx() / defaultProducers(), so a future wording fix landed in a template WITHOUT updating those functions (which would make reseed silently pull operators back to the stale wording — the same bug class one layer removed) fails the build instead of shipping unnoticed; matching "KEEP IN SYNC" comments were added at each template's ux: / producers: block. New tests: tests/policy-packs/ux-compare.test.ts, tests/policy-packs/ux-drift-check.test.ts, tests/policy-packs/builtin-default-config.test.ts, tests/cli/pack-reseed.test.ts, tests/cli/init-templates-ux-parity.test.ts, plus new doctor coverage in tests/cli/doctor.test.ts. Docs: docs/policy-packs/understanding-before-execution.md ("Refreshing config.ux after a harness upgrade"), docs/policy-packs/branch-protection.md, docs/CLI.md. check:duplication's MAX_CLONES pin raised 82→86 with a recorded justification (scripts/check-duplication.mjs): the new cli/pack/reseed.ts necessarily clones the validate/lock/diff/write shape already repeated undeduped across add/remove's CLI and pack add/pack remove.
  • Codex sessions now get the active-claim tracker and the stay-in-scope reminder too (task cf4cdc93, closes docs/okf/codex-adapter-parity-gaps.md gap #3, the last hook-parity gap between the two runtimes). Before this, harness pack hook track-active-claim (writes/clears harness.generated/active-claim on task_start/task_finish/task_abandon so harness approve understanding can auto-resolve --task) and harness pack hook stay-in-scope (soft reminder + audit row on a review-derived follow-up task) were Claude-only: a Codex session could honor an existing task-scoped marker but could never produce the active-claim file itself, and got no stay-in-scope reminder at all.
    • No new Codex-specific CLI verb needed, unlike the other Codex hooks. Both hook bodies (src/cli/pack/hook-track-active-claim.ts, src/cli/pack/hook-stay-in-scope.ts) needed no session-id resolution and no shell-command extraction the way codex-post-tool-use/codex-pre-tool-use need. harness apply --runtime codex now emits two more [[hooks.PostToolUse]] groups running the SAME commands (harness pack hook track-active-claim / harness pack hook stay-in-scope) as the Claude branch — the Codex adapter now contributes 6 hooks total, matching the Claude branch's hook roster.
    • Same two-layer alias fix task a1348c89 established, applied to these two hooks. The existing Claude matchers (`TRACK_ACT...
Read more

v0.40.0

Choose a tag to compare

@github-actions github-actions released this 10 Jul 11:55
7bfbca1

Headline: harness approve understanding can finally persist the Understanding Report it approves. Attach the report as a quoted heredoc on the command's stdin and the same run parses it, persists it session-bound, and flips it to approved, and the report: ⚠ skipped line that every approval printed is gone, and the audit trail is no longer structurally empty. The gate's escape matcher was rebuilt around a character whitelist after review found a shell-parse divergence that let a backslash-escaped redirect smuggle commands past it. Also: init --template full makes the runtime-reality hook discoverable, CI enforces coverage thresholds, the test suite runs on macOS, and the curated OKF knowledge bundle ships. Re-run npm i -g @lannguyensi/harness to upgrade.

Added

  • Report capture on harness approve understanding (task 61fd36db, PR #337). The command now accepts the Understanding Report as a quoted heredoc on stdin, parses it with the canonical @lannguyensi/understanding-gate parseReport (new dependency, ^0.4.5), persists it session-bound with approvalStatus: "pending" into the reports dir, and lets the existing selection → validation → flip path approve it in the same run. Why this channel and not the Stop hook: the Stop-hook producer fires at END of turn, i.e. after a same-turn approve has already looked for a report, and current Claude Code builds no longer reliably persist mid-turn assistant text to the transcript JSONL, so no transcript-based capture (Stop-hook fallback, tier-6 session-id fallback, PreToolUse harvest) can work. The approval command is the only reliable carrier, and as a bonus the operator reads the full report inside the permission prompt before approving. Unparseable stdin degrades loudly (a stdin: ⚠ line plus a parse-error log in the standalone hook's format, so findLatestParseError surfaces the reason) but never blocks the approval itself; a parsed-but-hollow grill_me report still refuses the marker through the existing validation short-circuit; incomplete piped input (timeout, stream error, size cap) is refused rather than captured, so a truncated report can never be persisted and approved as if it were whole. The deny message, the init profiles/templates/composer ux.run texts, and docs/okf/understanding-gate-lockout-recovery.md all teach the heredoc form.
  • The runtime-reality PreToolUse hook is discoverable from harness init --template full (task 9f10267e, PRs #333, #334). grep runtime-reality src/cli/init/ used to return zero hits, so operators only found the hook through docs/runtime-reality-hook.md. FULL_TEMPLATE now carries a fully commented-out hooks[] entry with placeholder env values and a docs pointer: enable-in-place, but not an active default, because the hook is host-coupled and without RUNTIME_REALITY_KEYWORD, an expectations file, and RUNTIME_REALITY_PROBE_CMD it degrades to a silent allow that looks like protection. A parity test asserts parseManifest(FULL_TEMPLATE).hooks contains no active runtime-reality entry and that the commented block still exists; mutation-verified (uncommenting the block turns the guard red).
  • Curated OKF knowledge bundle for harness (PR #336): eight cross-file concept docs under docs/okf/ capturing semantics no single source file states: gate fail-posture matrix, evidence-ledger trust boundary, version-sensitive producer wiring, whole-manifest validation scope, understanding-gate lockout recovery, pause-vs-gate-disable kill switches, Codex adapter parity gaps, and debug verb selection. Every claim verified against sources at f3c1727; okf-kit check --strict clean.

Changed

  • The understanding-gate escape matcher moved to src/cli/pack/approve-escape.ts (PR #337) and is now shared by the hook rather than defined inside it. Beyond the bare harness approve … line it accepts exactly one extra shape: a clean command part plus one single-quoted heredoc, terminated by the first line exactly equal to the delimiter (mirroring shell semantics), with nothing but whitespace after it. Single-line behavior is byte-for-byte unchanged.
  • CI enforces the coverage thresholds it declares (PR #331). The Test step runs npm run test:cov instead of npm test, so the 90/90/90/75 lines/functions/statements/branches thresholds in vitest.config.ts gate merges instead of silently rotting. Master clears the gate (statements 91.69%, branches 82.57%, functions 93.46%, lines 93.41%); verified the gate trips by locally raising the lines threshold to 100.

Fixed

  • SECURITY: a backslash-escaped redirect could smuggle commands past the understanding gate (PR #337, found by review before release). harness approve understanding \<<'UR' was accepted as a report heredoc by the escape matcher, but bash reads \< as a literal < followed by a file redirect: no heredoc exists and the "body" lines execute as ordinary shell commands. Reproduced live in bash. The matcher would then emit permissionDecision: "ask" on a command visually near-identical to the legitimate one, and under a Bash(harness approve understanding:*) allowlist it would have auto-approved the smuggled command with no prompt at all. The heredoc command part is now held to a character whitelist (/^[A-Za-z0-9_\s,./=:@~-]*$/) instead of a metacharacter blacklist, so no quoting or escaping trick can change how the shell tokenizes the line. Mutation-verified: removing the whitelist fails exactly the three new exploit tests. Negative tests cover \<<, foo\<<, \\<<, quote-obscured intros, <(id), >(cmd), unquoted/double-quoted/<<- delimiters, second redirects, trailing commands, early terminators, unterminated bodies, and CR-in-head.
  • addLedgerFact could lose the tail of a failing MCP server's stderr (task 5839b59e, PR #329). stderr was captured in the child's exit handler, but Node does not guarantee the stderr data event fires before exit, so the surfaced reason could collapse to (no stderr). Capture on close (fires only after all stdio pipes drain), mirroring ledger-client.ts's queryLedgerByTag. Also adds a subprocess E2E case asserting the real block/deny envelope from hook-pre-tool-use.ts, which was previously only exercised on the allow path.
  • The test suite runs on macOS (PR #335): four Darwin-only failure clusters (14 tests), with Linux CI staying green. Tests hardcoding /bin/true (absent on macOS) use /usr/bin/true; locateGitContext / repoRelativePath resolve paths to their physical form before relativizing against git's physical work-tree root, which fixes real symlinked-path usage (macOS /var/private/var), not just the tests.
  • Two fixed-window test flakes replaced with readiness handshakes (PRs #330, #332). The smoke SIGKILL-escalation test spawns its trap child directly and waits for the trap to be installed before handing it to runSmoke, so the runner's timeout window only starts once SIGTERM is guaranteed to be swallowed. The io lock concurrency test polls for worker A's A:lock-acquired marker instead of assuming a fixed 50ms head start, which under CPU contention let worker B win the first-acquire race and flip the [A,B] assertion.

v0.39.0

Choose a tag to compare

@github-actions github-actions released this 02 Jul 20:41
3684b58

Headline: the 2026-07-01 deep-review remediation ships — enforcement no longer degrades under load, the grounding: section finally does something, and apply --merge stops eating operator edits. The PreToolUse gate pools one grounding-mcp session per intercept (a hook timeout no longer silently fail-opens enforcement), grounding.evidence_ledger.path now actually configures the grounding-mcp entry, apply --merge preserves operator-added mcpServers with provenance, the Codex understanding-gate reaches approval-lifecycle parity with Claude Code, diff --since stops reporting override layers as phantom history, and CI gains two architecture fitness gates plus new validate lints (git-ignored OW knob, block policies without declared producers). Re-run npm i -g @lannguyensi/harness to upgrade.

Added

  • Two architecture fitness functions now run in CI (task 19e293c6, harness-review-2026-07-01). npm run check:boundaries (dependency-cruiser 18.0.0, .dependency-cruiser.cjs) pins the layering schema → policies → runtime → policy-packs → cli and fails on any new reverse import; running it for the first time surfaced four pre-existing shared-util edges (policies/duration, policies/extract, runtime/ledger-record, runtime/expand-home imported from below), which are grandfathered per target file and documented in the config with a pointer to the structural-concentration follow-up (f86b2425). npm run check:duplication (jscpd 5.0.11 via scripts/check-duplication.mjs) pins the clone count in src/ at the post-extraction baseline of 82: a percentage threshold cannot see a single new ~16-line copy, an absolute count can — raising the pin requires an in-PR justification. Negative-control verified: a deliberate reverse import fails check:boundaries (exit 1), and the pre-extraction tree (85 clones) fails check:duplication.
  • The grounding: manifest section is wired: grounding.evidence_ledger.path now configures the grounding-mcp entry (task 129e1b94, harness-review-2026-07-01). apply projects the path as the EVIDENCE_LEDGER_DB env on the enabled grounding-mcp entry (the exact variable its ledger-bridge reads), ~-expanded to an absolute path; an operator-declared env override wins. adopt mirrors the same synthesis so the apply→adopt round-trip stays drift-free, and doctor gains a Grounding section that checks the EFFECTIVE ledger path for writability and warns when an override diverges from the manifest value. session.*, retention_days, and policies_source stay RESERVED with explicit status comments (no consumer exists yet).
  • Trust-model tripwire for custom block policies, plus a validate warning on undeclared producers (task 43b107f2, harness-review-2026-07-01). writing-custom-policies.md gains a fourth "Read this first" tripwire spelling out that whoever can write the ledger can open a requires:/ledger_tag gate: block policies are advisory against the very agent they gate, and an enforcing gate needs a non-agent evidence actor. New checkPolicySelfAttestation in validate warns when a block policy declares no producers: at all (require_approval excluded with a recorded rationale); the recipes and templates now model the pattern, closing the agent-writable risk-override gap on the FULL_TEMPLATE's gate-prod-destructive. Engine hardening is tracked as follow-up task 078ec604.
  • harness validate / harness doctor now warn when the solution-acceptance pack is enabled but the OW knob path is git-ignored (task 24f6ceb9, ow-review-2026-07-01). The orchestrator-workflow arm reads repo state (.ai/solution-acceptance.json plus run completeness under .ai/runs/); a repo that ignores .ai/ wholesale cannot commit its enforcement posture, so fresh clones and git worktrees silently skip the OW arm under the default auto knob. The shared check (checkSolutionAcceptanceKnobIgnored) probes git check-ignore against the process cwd, stays silent outside a git repository and in doctor --shallow runs (no spawns), and lands in the same Policy Packs section as the producer deadlock checks. Negative control: a repo that ignores only .ai/runs/ produces no warning.

Changed

  • parseConfigUx exists once (task 19e293c6). The four byte-identical copies in hook-pre-tool-use, hook-codex-pre-tool-use, hook-branch-protection, and hook-solution-acceptance (the CHANGELOG had flagged the third copy for extraction; the fourth landed anyway because no scan existed) are now one shared helper in src/cli/pack/hook-bootstrap.ts, parameterized by the hook's stderr label. The per-hook warning output is byte-identical to the pre-extraction strings, pinned by a test per label.
  • The harness repo itself now commits its OW enforcement posture (task 24f6ceb9; operator-approved revision of the earlier ".ai/ fully gitignored" decision, which was about run noise). .gitignore narrows .ai/ to .ai/runs/; .ai/workflow/ (kit templates + manifest) and .ai/solution-acceptance.json (committed as "orchestratorWorkflow": "on") are now tracked, so the solution-acceptance OW arm enforces in every checkout, including fresh clones and worktrees. docs/policy-packs/solution-acceptance.md gains a "Repo state and gitignore" section documenting the convention and the honest residual (run-to-change binding is agent-grounding 067bede3).
  • The symlink-rejecting gate-marker read exists once (task f86b2425 slice 1). checkApprovalMarker (understanding-before-execution) and readVerdict (solution-acceptance) now share readRegularFileRejectingSymlink in src/io/ (5-kind result; every deny detail and the exists-but-unreadable-still-satisfies semantic byte-identical). Future defensive fixes (lstat/read race, ENOTDIR handling) have a single home.

Fixed

  • First run on a fresh machine no longer dead-ends (task 24ec07a6, harness-review-2026-07-01). Every read verb (doctor, describe, validate, list, explain, ...) previously failed with a bare manifest not found: <path> when no harness.yaml exists; the base-manifest miss now appends No harness.yaml on this machine yet: run harness init --interactive (or harness init --template solo) to create one (same message shape, same exit 66; a missing override LAYER keeps the old message, since "run init" would be wrong advice for a mid-read race). Also fixed two doc-drift items the same review flagged: docs/for-agents.md's ${REPO} row claimed "basename of cwd" (the code resolves the basename of the git worktree ROOT) and its ${BRANCH} row promised a (detached) placeholder that has never existed (detached HEAD substitutes the empty string); docs/init-interactive.md's Custom-flow pack list was missing branch-protection and now notes that solution-acceptance is deliberately not wizard-selectable.
  • A manifest with the wrong version now gets upgrade guidance instead of a bare zod literal error (task 50a94127, harness-review-2026-07-01). version: 2 produces this CLI supports manifest version 1; your manifest declares version 2. A newer manifest needs a newer CLI: re-run npm i -g @lannguyensi/harness ... on every parseManifest surface (validate, doctor, describe, ...); a manifest missing the key gets the distinct missing manifest version: add version: 1 variant instead of wrong upgrade advice. Message wording only: issue codes, paths, error type, and exit codes are unchanged. Also corrected two dishonest schema comments: risk: was still annotated "no runtime surface reads them yet" in schema/index.ts and risk.ts although risk.classifiers[] has been live since Phase 7 (classifyRisk on every PreToolUse + when.risk.* in when-eval); and the genuinely inert keys memory.scopes / memory.retention.broken_refs now carry explicit STATUS: INERT disclosure comments (validated, read by no consumer) instead of looking like live configuration.
  • apply --merge no longer clobbers operator-added mcpServers entries — and manifest-removed/disabled servers still leave the target (task 059b669c, harness-review-2026-07-01; operator decision: deep-merge over warn-only). The merge is now per server name inside mcpServers, with provenance from .last-apply: names the manifest declares come from the generated output; a server the operator hand-added directly to the target settings.json survives (kept N operator-added mcpServer(s) (...) in the apply summary); a server harness wrote on a previous apply that the current manifest no longer emits (deleted, or enabled: false) is dropped (dropped N manifest-removed mcpServer(s) (...)), so enabled: false remains an effective kill switch on --merge targets. Without provenance (no .last-apply yet, a record without a settings.json entry, or a corrupt record) unknown names are conservatively preserved. hooks stays wholesale-owned (no stable per-entry identity in the settings shape; hand-added hooks belong in the manifest via harness adopt). The merge remains idempotent, and a malformed (non-object) mcpServers on either side falls back to the old wholesale replace.
  • adopt no longer drops hook timeout or MCP min_version/version_command on the round-trip (task 059b669c). parseSettingsHooks now captures a settings hook's timeout (positive integers only) and buildHookEntry carries it into budget_ms — 1:1 with apply's projection, so adopt→apply round-trips the value losslessly for newly adopted hooks (a timeout-only hand-edit to an already-declared hook is deliberately ignored: timeout is not part of the drift key, because hooks adopt add-only and a timeout-only difference would otherwise create a duplicate entry — pinned by a test). buildMcpEntry now also carries forward min_version/version_command from the existing manifest entry on a replace-modified drift (previously only health and enabled: false, despite the comment claiming full manifest-only-field preservation; the comment now enumerates...
Read more

v0.38.0

Choose a tag to compare

@github-actions github-actions released this 27 Jun 15:08
47364e4

Headline: the Risk Gate now flags fail-closed unclassified matches across the audit trail, and validate / doctor lint risk policies that forgot to scope by environment.name. When a risk-gate policy fires only because the action was unclassified (the "unknown is not safe" rule), harness audit (table and --json) and harness explain --trace now mark the decision, so an operator reviewing a deny can tell a real critical-severity match from a fail-closed one. A new lint, shared by harness validate and harness doctor, warns when a when: block gates on risk.severity_at_least / risk.category_in / action.reversible without an environment.name scope (those clauses match every unclassified command in every environment). Re-run npm i -g @lannguyensi/harness to upgrade.

Added

  • harness validate now warns on risk-gate policies without an environment.name scope (M7). A policy with risk.severity_at_least, risk.category_in, or action.reversible in its when: block but no environment.name clause fires on every unclassified command in every environment (the "unknown is not safe" fail-close rule). validate emits a severity: "warning" diagnostic at policies[<index>] pointing at docs/risk-gate.md. The negative control (same clauses plus environment.name) produces no warning. harness doctor delegates to the same shared check (checkPolicyRiskWithoutEnvScope), so its risk-gate health section reaches parity with validate and now also warns on action.reversible-unscoped policies; doctor previously excluded action.reversible based on an incorrect assumption that its clause matched false on an unclassified action.
  • Audit record and block-time deny message now flag a fail-closed unclassified match (M7). When a risk-gate policy fires because the action was unclassified rather than a genuine classification hit, PolicyDecision.whenUnclassifiedFallback is set to true. The field is serialised into the policy_decision ledger row and surfaces on three CLI paths: harness audit (table) annotates the reason column with [unclassified-fallback]; harness audit --json includes the whenUnclassifiedFallback: true field on each decision object; harness explain <policy> --trace --json includes whenUnclassifiedFallback: true in the JSON trace projection. The non-ux block-time deny message appends (matched via the fail-closed unclassified rule, not a real risk classification) before the hint suffix so the agent-facing message names the cause. Policies with a ux: block are not altered at the agent-facing surface; the flag still rides the audit record and is visible in harness audit and explain --trace.

v0.37.0

Choose a tag to compare

@github-actions github-actions released this 25 Jun 10:36
3d5d93c

Headline: harness doctor learns the solution-acceptance deadlock checks, plus a sessionId-namespace fix to ledger-deny hints and clearer intercept / approve-understanding diagnostics. harness doctor now reports the two solution-acceptance misconfigurations that deadlock the completion-gate (parity with harness validate), ledger-gate deny hints name which sessionId namespace to record the unblocking entry under, the understanding-gate admits read-only Bash pipelines for post-task CI polls, and harness validate now hard-errors on a solution-acceptance pack with no reachable producer. Re-run npm i -g @lannguyensi/harness to upgrade.

Added

  • harness doctor now surfaces the solution-acceptance producer/dir deadlock findings (task 08ccfe87). The two misconfigurations that harness validate already catches are now also reported by harness doctor in the Policy Packs section: condition #1 (grounding-mcp absent from tools.mcp) is reported as an error, condition #2 (relative SOLUTION_VERDICT_DIR in grounding-mcp env) as a warning. checkSolutionAcceptanceProducer is the single source of truth for both checks; no logic is duplicated.

Changed

  • Ledger-gate deny "to satisfy" hints now name the sessionId namespace (task cdc60d56, discovery 2026-06-24). The hint previously showed the session id value but not which namespace it is; an entry written under the agent-tasks task UUID never satisfies a harness runtime gate, which keys off the runtime session id (the 2026-05-17 incident on PRs #174/#175: a first attempt wrote under the task UUID and was rejected, the second under the session id and passed). The format is now ... To satisfy: record an evidence-ledger entry containing \`, under this runtime session's id `` (not the agent-tasks task UUID).` Naming an identity is not a producer verb, so the deny path stays neutral on producer (agent-tasks/88ca4bb3).
  • harness validate now errors (was a warning) when the solution-acceptance policy pack is enabled but grounding-mcp is not wired under tools.mcp (task e3af6388). Without a reachable solution_evaluate producer the completion-gate can never see a verdict and deadlocks on a permanent deny, so this is a hard misconfiguration rather than a soft warning. A relative SOLUTION_VERDICT_DIR (the other producer condition) stays a warning. Operator impact: a manifest with solution-acceptance enabled but grounding-mcp absent now makes harness validate exit 1 where it previously exited 0.

Fixed

  • Surface a previously-swallowed audit-write failure in the runtime policy intercept (task 6b8e53cc). intercept() caught and silently discarded an unexpected throw from ledger.record(), leaving harness audit / explain --trace blind with no signal. The catch now writes a harness runtime intercept: audit-write failed for <policy>: <error> diagnostic to stderr while preserving the fail-open contract (the gate decision is still applied; the stdout deny-JSON is untouched). This is a defense-in-depth backstop for an unexpected record() throw; the common {ok:false} write-failure path (connection refused / spawn fail / timeout) was already surfaced one layer down at realLedgerClient.record.
  • harness approve understanding now names the cause when a manifest-load failure degrades the ledger write (task 6b8e53cc, M8). The manifest load wrapped the ledger-write path in a bare catch {}, so an unparseable / missing manifest left the operator with ledger: ok false and the generic reason manifest unreadable; skipped ledger write — no diagnosis. The captured loader error is now interpolated into the reason (manifest unreadable (<cause>); skipped ledger write); the report flip still runs (fail-open preserved).
  • The understanding-gate PreToolUse hooks now allow a read-only Bash pipeline (task 1d024fff, friction #36/#69/#72). A post-task CI poll like gh pr checks 123 | head was blocked because the read-only classifier hard-refused any |, so confirming CI right after task_finish forced a fresh Understanding Report. A new isReadOnlyBashPipeline (used only by the Claude and Codex understanding-gate hooks) admits a single-| pipeline when every stage independently classifies read-only, while still refusing ;, &, &&, ||, |&, redirection, and command substitution. A pipeline of read-only stages cannot write, so this widens nothing the gate must stop. The strict isReadOnlyBashCommand is unchanged for its other consumers (the Risk Classifier read-only floor and the solution-acceptance write-guard). Scope: this is the read-only fix only; post-task writes (e.g. gh pr comment) remain gated by design.

v0.36.0

Choose a tag to compare

@github-actions github-actions released this 16 Jun 04:15
7269ab0

Headline: a doctor cleanup mode for rogue ledgers, plus gate-admission and pause-sentinel fixes. harness doctor gains an opt-in --rm-rogue-ledgers mode, the read-only Bash classifier re-admits sort, tree, and file behind precise write-flag guards (they were over-blocked), and the runtime-reality gate now honors the pause sentinel like every other gate. Re-run npm i -g @lannguyensi/harness to upgrade.

Added

  • harness doctor --rm-rogue-ledgers opt-in cleanup mode (PR #296). Doctor can now remove rogue evidence ledgers it finds, gated behind an explicit flag so a bare doctor run stays read-only.

Security

  • Validate sessionId before joining it into the session-export transcript path (PR #294). session-export joined sessionId into the transcript path with no validation; a rejectMalformedSessionId guard (rejecting blank values, path separators, and ..) now runs at the path-construction choke point.

Fixed

  • Re-admit sort, tree, and file in the read-only Bash classifier behind precise write-flag guards (PR #292). These commands were over-blocked; they are now admitted unless invoked with a write-producing flag (for example sort -o).
  • Honor the pause sentinel in the runtime-reality gate (PR #291). The runtime-reality checker now respects the pause sentinel like every other gate instead of firing while the harness is paused.

Changed

  • Dedupe rejectMalformedSessionId to the shared runtime helper (PR #295) and extract a shared resolveApprovalSessionId (PR #293, discovery M5). Internal refactors consolidating session-id handling, with no behavior change.
  • Remove em dashes from prose per the org style rule (PR #290).

v0.35.0

Choose a tag to compare

@github-actions github-actions released this 14 Jun 07:37
f8b2905

Headline: the Tier-1 discovery follow-up. The five HIGH findings from the 2026-06-10 discovery audit are fixed: a defense-in-depth gap in the approval-marker path, the long-standing harness add whole-manifest footgun, a solution-acceptance verdict-dir mismatch, a policy-degradation footgun that apply did not catch, and an integration suite that never ran in CI. Operator action: harness apply now refuses a manifest that declares policies: without wiring grounding-mcp under tools.mcp (previously it applied and the policies silently degraded to warn-mode at runtime); wire grounding-mcp or remove the policies. Re-run npm i -g @lannguyensi/harness to upgrade.

Security

  • Validate sessionId before joining it into approval-marker paths (task 96178e12, discovery H5, PR #284). approvalMarkerPathFor joined sessionId into the .approvals/ path with no validation, unlike the task-marker twin. A new rejectMalformedSessionId guard (rejecting blank, path separators, and ..) is applied at the single path-construction choke point, and the gate read path (checkApprovalMarker) fails closed (blocks, returns no match) instead of throwing out of the hook on a malformed value.

Changed

  • harness add asset gate is scoped to the entry being added (task 57ea5f5b, discovery H1, recurring footgun). add ran runAssetChecks over the whole proposed manifest and blocked on any error, so an unrelated pre-existing problem (for example a missing required CLI from init --template full) sank an otherwise-fine hooks-only add. The gate now blocks only on asset errors introduced or newly caused by the added entry (a baseline diff against the original manifest); pre-existing unrelated errors are surfaced as a non-blocking warning pointing at harness validate. harness validate stays whole-manifest.
  • harness apply fails loud on policies without grounding-mcp (task 09120efb, discovery H3, PR #288). checkPolicyGroundingMcp ran only in validate (a warning). An operator who ran apply without validate could deploy policies that silently degrade to warn-mode at runtime. apply now runs the same check in its gate phase and fails closed with a message naming the degradation and the fix. See the operator action above.
  • CI runs the integration suite (task 6791ba98, discovery H7, PR #286). tests/integration/operator-state-isolation.test.ts was gated behind HARNESS_INTEGRATION_TESTS=1 that CI never set, so the operator-state-isolation acceptance ran nowhere. A dedicated CI step now runs npm run test:integration on push and PR.

Fixed

  • Project SOLUTION_VERDICT_DIR into the solution-acceptance hook (task d4395979, discovery H2, PR #287). The completion-gate hook (consumer) read the verdict marker the grounding-mcp server (producer) writes, but harness did not project a manifest-declared tools.mcp[grounding-mcp].env.SOLUTION_VERDICT_DIR into the hook, so a non-default override split producer and consumer onto different dirs and the gate could never see a verdict. apply now projects the override onto both solution-acceptance hook commands, mirroring the UNDERSTANDING_GATE_REPORT_DIR pattern. The validate warning is corrected: an absolute override is now handled silently, and only a relative override (which cannot be reconciled across working directories) warns.

v0.34.0

Choose a tag to compare

@github-actions github-actions released this 10 Jun 15:51
6908197

Headline: the discovery release. A live-reproduced gate-integrity bug is closed: harness approve understanding could silently bind a fresh session to a weeks-old leftover report when the producer Stop hook failed (finding C1 of the 2026-06-10 discovery audit); the tolerant fallback now rejects stale sessionId-less candidates, prints loud adoption warnings, and orders reports by creation time instead of mtime. Around it: non-TTY-safe confirmations on apply and adopt (with apply --yes), validate --json, a new harness gc retention cleanup, an uninstall that finally sees the ~/.harness/ state root and now removes migrated state it previously left behind, and docs synced to reality. Operator action: none required unless you piped confirmation prompts (echo yes | harness apply --overwrite-drift now refuses; use --yes). Re-run npm i -g @lannguyensi/harness to upgrade.

Added

  • harness gc: retention-based cleanup of gate state (task 38943a05, harness-discovery M3). Nothing ever deleted terminal understanding-gate reports, parse-error logs, or approval markers of dead sessions; the reports dir on the originating install accumulated 103 files in under a month, and stale leftovers were the raw material of the C1 stale-adoption bug. The new verb ages out artifacts older than a retention window (default 30 days, --retention-days <n>): terminal-status (approved/expired) reports by their createdAt, parse-error logs and approval markers by mtime. Pending reports are never touched regardless of age, and only the enumerated harness-owned dirs are considered; the evidence ledger and solution-acceptance verdict dirs stay producer-owned. Dry-run by default, --apply deletes; per-file deletion failures are surfaced loudly and fail the command.

  • apply --yes, non-TTY guards on confirmation prompts, validate --json (task 0f901128, harness-discovery H4 + H6). The apply --overwrite-drift and adopt confirmation prompts read stdin via readline with no TTY check, so a triggered confirmation in CI or an agent-driven shell blocked forever waiting for input that never comes. Both default prompts now refuse loudly under a non-TTY stdin and name the escape hatch; apply gains --yes (skip the overwrite-drift confirmation, as if the operator typed yes; adopt already had it). Interactive TTY behavior is unchanged. harness validate gains --json, emitting { diagnostics, errorCount, warningCount } on stdout with exit-code semantics unchanged, closing the gap with describe / doctor / list for CI and agent pipelines. (Discovery note: the H4/H6 write-up claimed --yes was missing everywhere; adopt --yes and apply --json already existed. The real gaps were apply --yes, the two TTY guards, and validate --json.) Breaking workaround: piping the confirmation (echo yes | harness apply --overwrite-drift) previously worked and now refuses, since piped stdin is exactly the non-TTY case the guard exists for; use --yes instead.

Fixed

  • uninstall was blind to the ~/.harness/ state root and left .understanding-gate/ behind (task 38943a05, harness-discovery M2). Uninstall hardcoded ~/.claude/ as the only root, so on installs migrated by migrate-home (v0.24.0) it found no manifest, lock, or harness.generated/ to remove and tore down essentially nothing but settings.json entries. It now resolves the state root through the shared resolver (~/.harness/, legacy fallback, HARNESS_HOME env; --state <path> override), inventories and removes .understanding-gate/ (reports, parse-errors, hypotheses) alongside manifest/lock/generated, and prints both roots when they differ. An explicit --home without --state keeps the historic single-directory contract. Also fixed: two raw NUL bytes embedded in template literals made the source file binary to grep/file tooling; they are now \u0000 escapes (behavior identical).

  • Docs synced to the shipped binary (task 27adde6d, harness-discovery M9 + L1). README and docs/CLI.md claimed v0.30.0 (three releases behind); CLI.md now tracks v0.33.0 and gains the verbs shipped since: approve branch-protection, pack hook solution-acceptance[-writeguard], pack hook runtime-reality, plus the previously undocumented pack hook stay-in-scope. Corrected along the way: the Hook-entrypoints table documented a top-level harness hook ... namespace that does not exist (the runtime entrypoints live under harness pack hook ...; harness add hook is the unrelated manifest mutation). New docs/policy-packs/solution-acceptance.md documents the pack end-to-end, including the SOLUTION_VERDICT_DIR / SOLUTION_VERDICT_ID env knobs that previously existed only in CHANGELOG entries; writing-custom-policies.md now names three builtin packs. The stale branch-protection.ts header comment claiming the full template does not wire the pack (it ships enabled: true) is fixed.

Security

  • approve understanding no longer adopts stale sessionId-less reports via the tolerant fallback (task adc20a8b, harness-discovery C1, friction-log #67). Live repro 2026-06-10: with the live session's report never persisted (a silent Stop-hook producer failure, tracked separately in agent-grounding), harness approve understanding adopted the only candidate on disk, a 17-day-old sessionId-less pending report, validated it, stamped the fresh sessionId onto it, and printed only "passed structural checks". Three changes close this: (1) the tolerant fallback rejects sessionId-less candidates older than 15 minutes (TOLERANT_FALLBACK_MAX_AGE_MS); the rejection is its own error path naming the file, its createdAt, and its age, and pointing at parse-errors/, since a stale-only state usually means the producer failed to persist the fresh report. (2) When a fallback adoption does happen, the CLI prints the adopted report's createdAt and age with a verify-this warning instead of flipping it silently. (3) Report listing now orders by creation time (JSON createdAt, then filename ISO prefix, mtime as last resort) instead of mtime, which the approval rewrite itself bumps, so selection stays deterministic under rewritten files. The strict sessionId match and the gate-read / expiry paths (tolerantFallback: "any", no age limit) are unchanged.

v0.33.0

Choose a tag to compare

@github-actions github-actions released this 09 Jun 05:48
5be3e0c

Headline: a security-driven release that closes two gate-bypasses. A read-only-bash classifier hole let command rm -rf / and env FOO=bar rm -rf / run as "read-only" past the hard Understanding Gate, and the branch-protection override could be self-blessed by an agent-writable ledger ACK. Both are now closed: command runners recurse-classify their nested argv, and the branch-protection override is an operator-only canonical marker (new harness approve branch-protection verb), mirroring the understanding gate. Also bundled: a vitest CVE bump, clearer gate-block messages surfaced by dogfooding, and a SOLUTION_VERDICT_ID knob for solo sessions. Operator action: none required, back-compat. Re-run npm i -g @lannguyensi/harness to upgrade.

Added

  • solution-acceptance: SOLUTION_VERDICT_ID env knob for solo / non-agent-tasks sessions (task 01435583, PR #272): the completion-gate derived the verdict id solely from the agent-tasks active-claim, so a session that never calls task_start was permanently blocked with "no active-claim". It now falls back to a SOLUTION_VERDICT_ID env var when no claim is present. Resolution order is active-claim first, then SOLUTION_VERDICT_ID, then fail-closed, so a claimed session's id stays authoritative and cannot be redirected by the env (a sessionId fallback is still intentionally absent). The env value is validated as a safe single path segment; a malformed value fails closed. Set it to the same id you pass to mcp__agent-grounding__solution_evaluate({ id }).

Changed

  • Gate-block messages are clearer about preflight ordering, mode-aware wording, and the approve escape hatch (PR #274), surfaced by dogfooding the gates. preflight-before-investigation / preflight-before-push ux.required now note that harness preflight is itself gated by the Understanding Gate, so the "Run: harness preflight" remedy no longer dead-ends when the report is not yet approved. A new understandingApprovalRequirement(mode) helper derives the understanding ux.required phrase from the mode (only strict says "a human-approved Understanding Report"; fast_confirm / grill_me stay "an approved Understanding Report"); no output change for current grill_me profiles. approveEscapeHint() appends a targeted hint when a blocked Bash command starts with harness approve but trips the (unchanged) metachar guard, telling the agent to re-run it bare; the understanding ux.run line gains the same "(bare, no pipes, chaining, or redirection)" guidance.

Security

  • Command runners env / command no longer bypass the hard gate (HIGH audit finding, PR #271). The read-only-bash classifier listed env and command in SIMPLE_READ_ONLY_BINS, so the gate treated command rm -rf /tmp/x and env FOO=bar rm -rf / as read-only and allowed them without an approved Understanding Report, a hard-gate bypass across hook-pre-tool-use, hook-codex-pre-tool-use, and hook-solution-acceptance-writeguard. Both binaries are command runners (their argv is itself a nested command); they are removed from the simple set and given a find-style special case that strips the runner's own leading flags/assignments and recurse-classifies the residual underlying command. Bare and lookup-only forms (env, env -u X, command -v node) stay read-only; env -S / --split-string fails closed since it re-parses a string into a fresh argv that defeats whitespace tokenization.

  • branch-protection override is now an operator-only canonical marker (MEDIUM audit finding #39, PR #275). The override was satisfied by any branch-protection-ack ledger entry, which an agent can self-write via mcp__agent-grounding__ledger_add, letting it bless its own protected-branch edit. The agent-writable ledger ACK is replaced with an operator-only canonical marker file under harness.generated/.approvals/branch-protection-<sessionId>, mirroring the understanding gate (writeApprovalMarker / checkApprovalMarker). A new operator verb harness approve branch-protection writes the marker and records the branch-protection-ack ledger row as a best-effort audit echo only. The blocker now reads the marker as the canonical override; the ledger tag alone no longer opens the gate.

  • vitest bumped to ^4.1.4 (CVE-2026-47429 / GHSA-5xrq-8626-4rwp, PR #273). vitest < 4.1.0 lets the UI server read and execute arbitrary files. vitest is a devDependency; the lockfile is regenerated.

v0.32.0

Choose a tag to compare

@github-actions github-actions released this 30 May 19:28
9718825

Headline: harness ships a new opt-in solution-acceptance policy pack that makes task completion EARNED from a real preflight run rather than self-attested. The producer (@lannguyensi/grounding-mcp >= 0.3.2 solution_evaluate) records a HEAD-pinned verdict from a real preflight run --json; this pack gates the task-finishing tools (agent-tasks completion verbs + git push / gh pr merge) on a ready verdict at the current HEAD, and adds an anti-forgery write-guard so the agent cannot hand-write the verdict marker. Operator action: none required, back-compat. The pack is opt-in (harness pack add solution-acceptance, or flip the disabled exemplar in the full template); it needs grounding-mcp under tools.mcp and the preflight binary on PATH, and harness validate warns if you enable it without the producer. Re-run npm i -g @lannguyensi/harness to upgrade.

Added

  • solution-acceptance builtin policy pack (task cc43c7a4, PR #269): the consumer half of the "Verifier-gated Done" gate. Two blocking: hard PreToolUse hooks:
    • completion-gate (harness pack hook solution-acceptance): denies task_finish / task_submit_pr / task_merge / pull_requests_merge and git push / gh pr merge unless a ready solution-acceptance verdict exists at the current git HEAD for the active-claim task. Fail-closed (missing / not-ready / HEAD-drift / unresolvable-HEAD / no-active-claim all block); the verdict id is the active-claim task id, never the session id.
    • write-guard (harness pack hook solution-acceptance-writeguard): the anti-forgery closure. Relocating the verdict dir is not sufficient (the understanding-gate allows all Bash post-approval), so this hook denies the agent's enumerated writes into the verdict dir (Bash redirects / tee / mv / cp / ln / interpreter writes that reference it, including glob-obscured spellings and chmod/chattr on the dir, plus Write/Edit/MultiEdit/NotebookEdit whose target lands inside it). The only legitimate writer is the producer.
    • harness is a pure consumer: it reimplements the marker read + gate decision locally (no grounding-mcp runtime dependency) and reads the producer-default verdict dir ~/.local/state/agent-grounding/solution-verdicts (SOLUTION_VERDICT_DIR override). The gate decision is ready && head === HEAD only; confidence is informational (parity with the producer's solution_gate). A golden-fixture test pins the consumer field-for-field to a real 0.3.2 marker.
    • harness validate / harness doctor warn when the pack is enabled but grounding-mcp is absent from tools.mcp (the producer would be unreachable and the gate would deadlock) or declares a non-default SOLUTION_VERDICT_DIR. Added to the full init template as a disabled, discoverable exemplar (no No-Op default).
    • Anti-forgery scope is v1-honest: it closes the enumerated-write-path residual, not arbitrary same-uid forgery. Cryptographic marker signing (which also closes glob-every-segment and interpreter runtime-path-construction spellings) is a tracked follow-up.