# HarnessLink 17.5.97 release notes

12 changes, released Oct 27, 2026.

### Breaking Changes

- **HarnessLink is now proprietary software.** Its own code is licensed under the HarnessLink End-User Licence in `LICENSE` (use with a HarnessLink account; no reverse engineering except where the law allows it, no redistribution, no circumventing limits, policies or licence and integrity checks, no building a competing service), and the Terms of Service say the same in plain language. Code inherited from oh-my-pi and pi stays under the MIT licence with its copyright notices (`LICENSES/MIT-oh-my-pi.txt`), as do the LiteLLM-derived model prices (`LICENSES/MIT-litellm.txt`); `NOTICE.md` lists exactly which files that covers. `hnl --license` prints all of it, the release tarball is never built without these files, the installers point to them, and the package `license` fields read `SEE LICENSE IN ../../LICENSE` (`packages/utils`, wholly upstream-derived, stays `MIT`).
- **HarnessLink installs as one compiled binary (ADR-77).** Each release builds a minified `hnl` executable per platform (macOS, Linux incl. musl, Windows; arm64 and x64) instead of a source tree: it installs to `~/.hnl/bin/hnl` with the verified release manifest beside it, `hnl` on PATH links to it, and the background service runs from it. The installer and the automatic updater move an installer-made source install (`~/.hnl-src`, `~/.superpi-src`) onto the binary and remove the old tree (a git checkout is never touched); every reinstall keeps the previous binary as `hnl.previous`. There is no `bun install` at install or update time, and no new source tarball is cut — the last one stays served only as the bridge pre-binary updaters update through.

### Added

- **`hnl doctor` (ADR-72).** One command that finds and fixes what keeps HarnessLink from working on this machine, and lists each fix: the service (installed, running, on this CLI's build, auto-update failures, release integrity), sign-in (account, org, linked device), capture lists, prompt-history ingest, outboxes (failed items requeued, missing capture triggers reinstalled, a sync now), session uploads, cloud parity, the org policy cache (refreshed when missing, invalid or expired), context/policy hooks and `hnl` MCP entries (reinstalled), the MCP hub and gateway (service restarted), leftovers from before the rename, and an integrity check of HarnessLink's databases (a damaged one gets a backup-and-recover command; nothing is deleted). `--check` diagnoses and changes nothing, `--only <area>`, `--json`; exit 1 while problems remain. It catches the pattern where memories keep syncing while prompt history and transcripts don't: a harness HarnessLink banks memories from but doesn't capture is added to `cloud.harnesses`, and a history/sessions/memories category switched off without a record is explained (on by default, so it was switched off on this machine) with the command to turn it back on; an interactive run asks.
- Opt-outs are dated: turning a sync category off (`hnl cloud sync --disable`, setup's Customize screen), a harness's capture off (`hnl cloud harnesses disable`, `hnl harness select`) or its context hooks off (`hnl harness context --remove`) records when in `cloud.offByChoice` in config.yml. `hnl doctor` shows those as "off by your choice (since <date>)" and never undoes them.
- The service runs `hnl doctor --check` every 30 minutes and after a failed sync pass and records `~/.hnl/doctor.json`. When something is off, `hnl` and `hnl cloud status` show one "needs attention" line with the command to run, and the next harness session starts with one line such as "HarnessLink: Prompt history sync is turned off on this machine while memories still sync; run `hnl cloud sync --enable history`." (each problem at most once a day).
- Device health on the dashboard: the service posts a summary of each check to `POST /v1/devices/self/health` (finding codes, one-line summaries and commands; never paths or prompts; control migration 0051 stores it on `devices`), and `GET /v1/devices/self/health` returns the newest history, session and memory the cloud stored from the device (tenant migration 0027 records which device uploaded each session). The cloud flags `server.history_stalled` on its own when a machine's memories still arrive but none of its prompts have for two days, unless history is off on purpose there. Devices marks such machines "Needs attention: run `hnl doctor` on this machine" with every problem and its command, and Overview shows a banner naming them.
- **Signed releases (ADR-76).** Every release now carries `/hnl/manifest.json` (version, commit, and the SHA-256 and size of each binary) and `/hnl/manifest.json.sig`, an Ed25519 signature made with a release key kept in one file outside the repo. `install.sh` (OpenSSL 3, or perl on a stock Mac), `install.ps1` and the automatic updater check the signature and every downloaded file against it, and refuse to install a release that is unsigned, signed by an unknown key, or modified, with a one-line reason. The background service checks a minute after it starts and daily that the running `hnl` is byte-for-byte the signed release (`~/.hnl/integrity.json`); `hnl doctor` reports a modified binary (`client.modified`, fix: `hnl update --force`) and sends the result with its health report. The server flags such a device as "Modified client" on your Devices page, on the owner console and, when your organisation's Devices visibility switch is on, on the org's Devices list. Nothing is blocked by the flag; operators can turn on refusal with `MODIFIED_CLIENT_POLICY = "refuse"` (docs/CLOUD_SETUP.md, "Release signing"). Releasing needs the key: `bun scripts/release-sign.ts keygen|embed|check|sign|verify`, run by `deploy:prod`; rotation embeds two public keys during the overlap.
- **Org policy installed system-wide on Team and Enterprise (ADR-74).** On a machine signed in to a Team or Enterprise organisation whose policy is on, HarnessLink also writes the policy into each harness's admin-owned settings, above anything in your own: Claude Code's `managed-settings.d/50-harnesslink.json` (`SessionStart`, `UserPromptSubmit` and `PreToolUse` hooks on the absolute `hnl` launcher, plus `permissions.deny`/`ask` for unconditional explicit tool and command rules), Codex's `requirements.toml` (the same hooks through a wrapper in `hooks.managed_dir`, `[features] hooks = true`, `[rules] prefix_rules`) and Cursor's enterprise `hooks.json` (the four policy hooks, fail-closed), plus a root-owned `enrollment.json` with the org and device ids. No token, signature or instruction text goes in them. `hnl cloud setup` installs them after sign-in with one administrator prompt; `hnl policy managed [status|install|remove]` shows, installs or removes them. The service keeps them current, elevating only when a file must change and at most once a day (macOS password dialog; on Linux and Windows the change waits for `hnl policy managed install`); a waiting change shows in `hnl cloud status` and the doctor. Leaving the organisation or a plan change removes them the same way; signing out does not. A file at one of these paths that HarnessLink did not write is left alone. When both the managed and your own hooks fire for one event, context is added once and a denial is recorded once. Each device reports its state (`PUT /v1/devices/self/managed-settings`, control migration 0052: `devices.managed_settings`, `managed_settings_at`), and Policies › Coverage lists it per device; `GET /v1/org/devices` returns `managed_settings`. The signed policy bundle now carries the org's paid plan (`org.plan`), and its ETag changes with it. Windows elevation is written but not yet exercised on Windows.
- **Org policy changes apply in seconds, and open sessions are told (ADR-73).** The background service holds a WebSocket to the new `GET /v1/policy/stream` (one hibernating Durable Object per organisation on the server); every policy save on the dashboard, every platform-owner override and every organisation suspension or restore announces the organisation's new version on it, and the service fetches the signed bundle at once, so the next tool call is decided by the new rules (about 10 ms from save to cached on a local server, under 5 s required). The announcement carries only the organisation id and version, never policy, and changes nothing by itself: only a bundle that verifies is cached. The service reconnects with backoff, fetches again whenever the stream comes back, and still checks every minute, so a machine that can't hold the stream open gets changes within a minute. Every open Claude Code, Codex, omp and pi session gets one "HarnessLink policy updated (vN)" block on its next prompt: changed instructions in full, withdrawn ones by name and a line per rule or limit change, nothing when the change doesn't reach that session; what each session was last shown is kept in `~/.hnl/agent/policy-seen/`. Org instructions at session start and in the update now say they come from your organisation through HarnessLink and take precedence over project and user instructions. pi's managed extension now adds the org's instructions at session start too.

### Fixed

- Editing local files can no longer switch org policy off. The policy now stays bound to the sign-in in `~/.hnl/cloud.token` even when `HNL_CLOUD_TOKEN` is set, or the file's `expires_at` is edited (both used to read as "not signed in", so no policy was enforced); a `cloud.token` that can't be read or parsed blocks tool and model calls until `hnl cloud login`. On a machine with a managed enrollment (Team and Enterprise managed settings), only a bundle of that organisation verifies, and signing out keeps the cached policy in force instead of turning it off. A `cloud.suspended` file that exists but can't be read now counts as suspended, like a damaged one.
- Every plan gate now answers the same way. Organisation invites, teams, shared artifacts, the agent inventory, organisation sharing switches and the organisation digest used to refuse Free and Pro with a bare `feature_gated`, so the dashboard could not show which plan unlocks them; they now name the feature (`shared_org`) and the plan (`team`). Sharing a project's memory checks `shared_memory`, and `GET /v1/org/spend` answers 403 `feature_gated` (`member_spend`, `team`) below Team instead of `visibility_off`. A new contract test calls every gated feature's endpoints on each plan below it and on its plan, asserts every plan limit at its boundary, and fails when a feature is added to the catalog without a row.
- Free's 30-day history lock could be bypassed from the CLI: `hnl cloud pull-sessions` and history pulls fetched prompts and transcripts older than the window (and from other machines), which `hnl resume` could then open. The CLI read planes (`GET /v1/sessions/index`, `GET /v1/sessions/index/:id`, `GET /v1/sync/history/changes`) now apply the same lock as the dashboard: locked sessions are left out and counted, opening one answers 403 `history_locked`, and locked prompts are not sent. Upgrading makes them available again.

Update HarnessLink and every installed harness to its current release:

hnl update

New machine? Install with:

curl -fsSL https://harnesslink.sh/install.sh | bash

[Newer: 17.5.98](https://harnesslink.sh/releases#v17.5.98) · [Older: 17.5.96](https://harnesslink.sh/releases/17.5.96) · [All releases](https://harnesslink.sh/releases) · [Atom feed](https://harnesslink.sh/releases.atom)
