openlatch-client 0.6.19

OpenLatch runtime enforcement node — the capture-and-enforce adapter that evaluates every covered action against a coding agent's Autonomy Zone before it runs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# Command reference

`openlatch --help` is always current. This page adds what `--help` cannot: what each command guarantees, and how to read what it prints.

## Commands

| Command | Notes |
| ------- | ----- |
| `openlatch init` (alias `setup`) | Complete, idempotent reinstall. Reclaims the ports first — identifying the holder **by the port**, not by `daemon.pid` — then proves the daemon it started is the one serving (PID, version, `/health`). |
| `openlatch status` | Dashboard plus the twelve-section verdict, and the License line — state, hosts, fair use, contract end; `events are queued` while the platform is refusing on licensing grounds |
| `openlatch start [--foreground] [--port N] [--model-relay-port N]` | Refuses when a daemon already runs (`OL-1501`, exit 5) |
| `openlatch stop` / `restart` | `stop` also removes the agent's model relay wiring, and says so |
| `openlatch doctor` | Full diagnosis with causes and remedies |
| `openlatch doctor --fix` | Heals what it can, backs up to `.bak` siblings + `~/.openlatch/fix-journal.json`. Never re-enables what you switched off. Reinstalls the relay's certificate authority into your user trust store when it is missing, and rotates it once its window opens — never silently, and never by regenerating it under a running session. |
| `openlatch doctor --restore` | Reverses the last `--fix`. Surgical merge preserves your non-OpenLatch hooks. |
| `openlatch doctor --rescue [--since 24h] [--yes]` | Redacted diagnostic ZIP for support |
| `openlatch system <subsystem>` | The per-subsystem verbs, grouped: `aip`, `auth`, `hooks`, `model-relay`, `proxy`, `supervision`, `telemetry`, `inventory`. Root stays the whole-host commands; `doctor` names the exact `system` command in every remedy. |
| `openlatch system aip refresh` (alias `update`) | Makes the running daemon poll the policy bundle immediately and waits up to 15 seconds for that attempt. Reports changed, unchanged (304), pending, refused or offline plus the last-known-good resident bundle facts. A pending result exits 7; a refusal, offline daemon or unavailable poller exits 1. |
| `openlatch system model-relay status` | Full classification, not a port probe. A hooks-healthy agent whose request plane is not captured reads *enforced, not measured*. An untrusted or expiring relay certificate authority gets its own remedy (`openlatch doctor --fix`), never the generic reachability one. |
| `openlatch system model-relay {enable,disable} [-y] [--no-restart]` | Writes `[model_relay] enabled`, then offers the restart that applies it |
| `openlatch system hooks status` | The Hooks section in detail |
| `openlatch system supervision {install,uninstall,enable,disable,status}` | OS-native supervisor (launchd / systemd --user / Task Scheduler), user-scope only |
| `openlatch system inventory {list,log,rescan,status,inspect,projects,ack}` | Configuration-plane monitoring |
| `openlatch system proxy {status,discover,set,clear,test}` | The egress route. See [egress.md]egress.md#commands. |
| `openlatch system auth {login,logout,status,switch}` · `openlatch system telemetry {…}` · `openlatch update` · `openlatch logs` · `openlatch uninstall [--purge]` · `openlatch docs` | |

Noun-verb aliases: `system hooks install` = `init`, `daemon start` = `start`, and so on — identical arguments, pinned by test.

### Internal commands (driven by the platform)

One `system` leaf is spoken by another process, not typed by a person, and is hidden from `openlatch system --help` for that reason.

| Command | What it does |
| ------- | ------------ |
| `openlatch system evaluate [--describe]` | Pure NDJSON evaluation from supplied inputs; no daemon or configuration required. See [the evaluation protocol]evaluate-protocol.md for frames, state ownership and its command-specific exit codes. |

`openlatch evaluate` at the root is a deprecated alias of `system evaluate` — identical arguments and exit codes, pinned by test — kept for one release so a platform pinned to the previous client keeps working while it moves its argv. It is removed in the release after this one.

Global flags: `--json`, `--verbose`/`-v`, `--debug`, `--quiet`/`-q`, `--no-color`. Every diagnostic's JSON carries the same `sections[]` the human rendering shows.

### `init` flags

`init` additionally takes the egress flags — highest precedence, applied per key:

| Flag | Effect |
| ---- | ------ |
| `--proxy <url>` | The proxy to use. **Never put a credential in it**: command-line arguments are readable by every process on the host (`/proc/<pid>/cmdline`, Win32 `CommandLine`), so a URL carrying `user:pass@` is rejected with `OL-1226`. Use `OPENLATCH_PROXY` or the prompt — both store the password in the OS credential store. |
| `--no-proxy <list>` · `--ca-bundle <path>` · `--proxy-mode <auto\|manual\|direct>` · `--proxy-auth <auto\|none\|basic\|negotiate>` · `--proxy-spn <spn>` | Persisted to `[proxy]`. |
| `--org <ORG>` | The organization this machine joins — its slug, its name or its id. Falls back to `OPENLATCH_ORG`. See [Choosing the organization]#choosing-the-organization. |
| `--yes` | Never prompt, even on a terminal. An install that would have to ask a question fails instead, naming what it needed — the only way to script an install from an interactive shell. |
| `--dry-run` | Report what would happen to the egress route and exit. Writes no file and creates no directory, not even `~/.openlatch`. With `--json`: `proxy: { action: "keep"\|"discover"\|"persist", source? }`. |

### `init` output

`init` speaks in six stages and ends with one result card. Engineering detail — ports,
PIDs, paths, hook event names — goes to `~/.openlatch/logs/init-YYYY-MM-DD.log` and is
shown only with `--verbose`.

```
┌  Setting up this machine
│
◇  Checked this machine · found Claude Code
│
◇  Signed in · Acme Corp
│
◇  Usage data · sharing usage data
│
◇  Connected Claude Code · actions now go through OpenLatch
│
◇  Started OpenLatch · running · starts at login
│
◇  Final checks · all checks passed
│
└  Done.

  ╭──────────────────────────────────────────────────────────────────────╮
  │                                                                      │
  │ ✓  OpenLatch is live on this machine                                 │
  │                                                                      │
  │    Organization   Acme Corp                                          │
  │    Agent          Claude Code                                        │
  │                   actions controlled · model calls observed          │
  │    Policies       in sync                                            │
  │    Runs           in the background · starts at login                │
  │                                                                      │
  │    Next           Keep using Claude Code as usual.                   │
  │                   See what they do at https://app.openlatch.ai       │
  │                                                                      │
  ╰──────────────────────────────────────────────────────────────────────╯
  Check anytime openlatch status   ·   Full diagnostics openlatch doctor
```

The card is the customer verdict, and the exit status follows it:

| Card | When | Exit |
| ---- | ---- | :--: |
| ✓ **OpenLatch is live on this machine** | nothing failed and nothing needs you. A known coverage limit (an agent whose model calls are not relayed) is a coverage line, not a task | 0 |
| ! **AIP bundle awaiting delivery; no AIP is enforced yet** | the daemon waited up to 10 s for its first verified bundle and remains running and retrying; plain output is `result=pending`, JSON is `verdict: "pending"` | 7 |
| ! **OpenLatch is running — one thing needs you** | a warning you have to act on, with its remedy | 7 |
| ! **No AI agent to connect on this machine yet** | no supported agent found. Not an error: OpenLatch is installed, install an agent and run `init` again | 3 |
| ✗ **OpenLatch couldn't finish setting up** | a stage failed, or a check failed at the end. The log path is printed for support | 1, or the error's own code |

`openlatch doctor` is unchanged: the same host can be `DEGRADED` (exit 7) under `doctor`
and live (exit 0) under `init` when the only warnings are known limits.

**No terminal** (MDM, CI, a pipe): no animation and no prompt. One line per finished
stage, the card without its border, and a last line for scripts to grep:

```
Setting up this machine
[1/6] Checked this machine - found Claude Code
[2/6] Signed in - Acme Corp
[3/6] Usage data - off (no terminal to ask)
[4/6] Connected Claude Code - actions now go through OpenLatch
[5/6] Started OpenLatch - running - starts at login
[6/6] Final checks - all checks passed
Done.
…
result=live org="Acme Corp" agents=claude-code exit=0
```

`result=` is `live`, `pending`, `attention` or `not_live` — the same vocabulary as the `verdict` key
of `init --json`. No agent is `result=attention reason=no-agent exit=3`; a failure adds
`code=` and `log=`. `--json` output is otherwise unchanged; its `exit_code` is the
process's own exit status. `--quiet` prints nothing but errors.

`--foreground` closes the rail with `└  Running in the foreground · Ctrl+C to stop` and
hands the terminal to the daemon; there is no card. When the install script has put
`openlatch` somewhere the calling shell will not find until it restarts, it sets
`OPENLATCH_INSTALL_PATH_HINT` and the card ends with *Open a new terminal to use the
openlatch command*.

## Choosing the organization

A browser sign-in (`init`, `system auth login`) never mints a key before an organization is chosen.
The browser hands the terminal a single-use code, bound to this sign-in by PKCE and a `state` value;
the CLI lists your organizations with it, and only then is a key minted, in the one chosen.

| Situation | What happens |
| --------- | ------------ |
| `--org <ORG>` or `OPENLATCH_ORG` names one | Used with no question. `--org` wins over the variable. Matched on the id, then the slug, then the name — exact, any case, never a prefix |
| You belong to one organization | Chosen for you; the rail prints `Organization  <name>` |
| Several, on a terminal | A picker. **Nothing is marked when it opens**: ↑ ↓ or a digit marks a row, Enter answers. On a plain line prompt, type the row number |
| Several, nobody to ask (`--yes`, `--json`, `--quiet`, no terminal) | `system auth login` fails with `OL-1608` and lists them — never a default. A headless `init` with no `OPENLATCH_API_KEY` stops earlier with `OL-1600`: it never starts a browser sign-in, so it never learns your organizations; set the key, or run `init` on a terminal |
| `OPENLATCH_API_KEY` set | The key's own organization is used. A `--org` or `OPENLATCH_ORG` naming another fails with `OL-1612` before anything is written; naming the key's own is accepted silently |

**An enrolled machine never changes organization through `init` or `login`** — not after a logout,
not after a revoked key. A machine is enrolled once its first policy bundle pinned its organization;
only `openlatch uninstall --purge` clears that pin. Naming or picking another organization there fails
with `OL-1614`, before any key is minted or anything is touched, and names the command that does it:

### `openlatch system auth switch --org <ORG>`

Moves this machine to another of your organizations. `--org` is required: there is no picker, and
`OPENLATCH_ORG` is not read. It refuses with `OL-1616` when the key comes from `OPENLATCH_API_KEY`
(change the variable instead), and with `OL-1600` on a machine that was never enrolled.
`--no-browser` prints the sign-in link instead of opening it, as for `auth login`.

1. A browser sign-in, then the organization is resolved and a key is minted in it. Naming the
   organization the machine is already in prints `Already in <name>` and changes nothing.
2. A running daemon sends what it has queued with the **old** key (up to 10 s), then pauses uploads.
3. The new key is stored; the daemon applies the switch — events captured under the old organization
   that are still queued are dropped, never sent with the new key — and the old key is revoked.

Until the new organization's policy bundle is accepted, **the old organization's bundle keeps
enforcing**: a switch never leaves the machine without a bundle. With the daemon stopped, the switch
is applied at its next start, before anything is uploaded.

A failure before the new key is stored changes nothing — the old key, its organization and the
daemon's state are as they were. A failed revoke of the old key does not undo the switch: the output
says `old key not revoked — revoke it in the console`.

```
✓ Moved LUC-LAPTOP from OpenLatch to SuperAdmin
• 3 queued events dropped
```

`--json`: `{ switched, from: {id, name}, to: {id, name}, events_sent, events_dropped, old_key_revoked }`;
`events_dropped` is `null` when the daemon was not running, since the count is known only when it
applies the switch.

### `openlatch system auth logout`

Revokes the stored key and deletes it from **both** the OS keychain and the encrypted credentials file.
A key in `OPENLATCH_API_KEY` is the operator's: it is never revoked or reported. `--json` adds
`credentials_cleared: ["keychain" | "file", …]`.

### Sign-in errors

| Code | Exit | Meaning |
| ---- | :--: | ------- |
| `OL-1608` | 1 | Several organizations, nothing naming one, nobody to ask |
| `OL-1609` | 1 | The picker was left unanswered |
| `OL-1610` | **3** | No organization matches the value given |
| `OL-1611` | 1 | The value matches the name of several organizations — use the slug |
| `OL-1612` | 1 | `OPENLATCH_API_KEY` belongs to another organization |
| `OL-1613` | 1 | An organization was named and the platform could not be reached to confirm it |
| `OL-1614` | 1 | This machine is enrolled in another organization — use `switch` |
| `OL-1615` | 1 | The daemon did not accept a switch; nothing changed |
| `OL-1616` | 1 | `switch` with a key from `OPENLATCH_API_KEY` |
| `OL-1617` | 1 | The sign-in expired or was already used — run the command again |
| `OL-1618` | 1 | Your account belongs to no available organization |
| `OL-1619` | 1 | The organization is suspended, deleted, or its licence has ended |
| `OL-1620` | 1 | Too many sign-ins in progress — wait five minutes |

No message ever contains a key, a sign-in code or a PKCE verifier.

## `openlatch update`

| Invocation | On a terminal | No terminal (MDM, CI, a pipe) |
| ---------- | ------------- | ----------------------------- |
| `openlatch update` | checks, then asks **Update now** / **Not now** | checks, reports the release, and prints the `--yes` hint. Nothing is installed |
| `openlatch update --yes` (`-y`) | checks, then installs without asking | the same |
| `openlatch update --check` | checks only | checks only |

`--apply` still works as a hidden alias of `--yes`, so existing scripts keep running.
`--force-cargo` bypasses the refusal to update a `cargo install` binary (`OL-1505`).

The run uses the same rail as `init`. There are six stages when a background service
is running: **Check → Download → Verify → Install → Restart → Confirm**. The daemon
applies the update, restarts into the new version, and the run ends only once the new
version answers. With no service running, the CLI installs in-process in four stages
(Check → Download → Verify → Install) and the card says to run `openlatch start`. If
the new daemon does not stay up, a **Rolled back** stage shows the old version
restored.

```
┌  Updating OpenLatch
│
◇  Checked for updates · 0.5.7 → 0.5.8
│
◇  Update to 0.5.8 · yes
│
◇  Downloaded · 8.4 MB in 2.9s
│
◇  Verified · signed by OpenLatch
│
◇  Installed · 0.5.7 kept as a backup
│
◇  Restarted the background service · back in 5.4s
│
◇  Confirmed · running 0.5.8
│
└  Done.
```

A result card follows, closed by one line on automatic updates. When they are off, that
line is a warning naming the setting that turns them back on (see
[configuration](configuration.md)). Engineering detail goes to
`~/.openlatch/logs/update-YYYY-MM-DD.log`, and a failed run prints that path.

**Modes.** All human output goes to stderr. stdout stays empty unless `--json` is set.

| Mode | When | What it draws |
| ---- | ---- | ------------- |
| Live | a terminal that interprets VT sequences | the rail with spinners, a download meter, and the bordered card |
| Append | a terminal that refused VT (an old conhost) | the same rail, drawn as ASCII lines that are never redrawn |
| Plain | stderr is not a terminal | one `[n/N]` line per stage, a one-sentence summary for any outcome that is not green, and a last line for scripts to grep |

```
Updating OpenLatch
[1/6] Checked for updates - 0.5.7 -> 0.5.8
[2/6] Downloading 8.4 MB...
[2/6] Downloaded - 8.4 MB in 2.9s
[3/6] Verifying the signature - failed
      The signature doesn't match OpenLatch's release key (OL-1504)
Still running 0.5.7 - nothing was changed. The signature doesn't match (OL-1504).
Automatic updates: on - last checked 4h ago
result=failed step=verify code=OL-1504 version=0.5.7 log=… exit=1
```

The `result=` token is one of `up_to_date`, `available`, `declined`, `check_failed`,
`updated`, `failed` or `rolled_back`. A failure adds `step=` (`download`, `verify`,
`install`, `restart` or `confirm`), `code=` and `log=`. An outcome that leaves something
for you to run adds `hint=`: `openlatch update --yes`, `openlatch start` or
`openlatch restart`.

`--json` skips the rail and prints the same JSON documents as before. A check prints
`{current, latest, severity}`, and `latest` is `null` when you are up to date. An apply
prints `{from, to, applied, restart_required}`, or `{current, idempotent: true}`. Errors
go to stderr as `{error: {code, message, …}}`. `--quiet` prints nothing and exits with
the same code as `--json`.

**Exit codes**

| Code | Rail (terminal or Plain) | `--json` / `--quiet` |
| :--: | ------------------------ | -------------------- |
| 0 | Up to date, or updated with the new version running | a check, whatever it found; an apply that succeeded or was already up to date |
| 7 | Declined; available but not installed (`--check`, or no terminal to ask); the check could not reach the registry; updated but no background service is running; or the service is still on the old version | only when an apply finds the service still on the old version |
| 1 | A stage failed: `OL-1504` (check, download, verify or swap), `OL-1216` (the new version cannot read this machine's policies), `OL-1506` (the daemon stopped reporting progress); or the new version did not come back or was rolled back (`OL-1502`) | an apply that failed |
| 5 | Refused: a `cargo install` binary (`OL-1505`) | the same |
| 6 | The daemon is up, but its admin endpoint could not be used: missing or wrong token, or the RPC failed (`OL-1506`) | the same |

Exit 7 follows the health convention in [Exit codes](#exit-codes): the run itself worked,
but something still needs you. So `openlatch update --yes && deploy` does not go ahead on
a host where the new version is not yet the one enforcing.

## Reading a health report

Every command that reports health — `status`, `doctor`, `system model-relay status`, `system hooks status`, `system proxy test` — renders the same twelve sections from one model, so a state means the same thing wherever you see it.

### States

| State | Mark | Means |
| ----- | :--: | ----- |
| `Ok` | `✓` | Enabled **and proven working**. The only green. |
| `Off` | `⚠` | Switched off deliberately. Always carries a code and a remedy. |
| `Degraded` / `Pending` | `⚠` | Working, but not at full capability, or still verifying |
| `Unknown` | `⚠` | Blocked by another section, which it names. One outage, one cross. |
| `Failed` | `✗` | Enabled and not working |
| `NotApplicable` | `·` | Absent from the build, an exercised privacy opt-out, or persistence inside a sandbox |

### Sections

Twelve sections, always all reported, under three groups — the questions a reader asks in the order they ask them:

| Group | Sections | Question |
| ----- | -------- | -------- |
| **Runtime** | Environment · Daemon · Persistence · Update | is it running, and will it keep running? |
| **Coverage** | Hooks · Model relay · Autonomy Zone AIP · Inventory · Integrity | is it seeing everything, and enforcing? |
| **Platform** | Connection · Cloud · Telemetry | is any of it leaving this machine? |

`Autonomy Zone AIP` sits under Coverage, not Platform: the bundle arrives from the cloud but the evaluation is local and authoritative — it is about what this host enforces, not about what leaves it. Its stable JSON key remains `policy`.

For a resident schema 2 bundle, `doctor` and `status` report the number of
distinct AIPs represented by artifacts this client successfully loaded. This is
a bundle-resident evaluation count, not the total number authored on the
platform; exception artifacts and skipped artifacts are excluded. Legacy
schema 1 bundles and schema 2 artifacts without a nonempty canonical
`policy_id` omit the count rather than inferring identity from a display ID.

`Connection` sits before `Cloud` because it is the hop `Cloud` travels over. Splitting them is what lets the report say *which* hop broke: a dead proxy and a dead platform are two different problems, and one section could only ever render both as the same failure.

A green section is one line. Anything else lists what needs attention, with its cause and its remedy. `-v` unfolds every check.

```
Runtime
  Environment  ✓    3 passed, 1 not applicable
  Daemon       ✓    2 checks passed
  Persistence  ⚠    Disabled at your request (user_opt_out)   [OL-1513]
                    The daemon will not restart after a logout, a reboot, or a crash.
                    found in : [supervision] disabled_reason = "user_opt_out"
                    to fix : Run `openlatch system supervision enable` to re-arm it.
  Update       ✓    Daemon is serving the installed version (0.1.18)

Platform
  Connection   ✓    via HTTP proxy proxy.corp:8080 (set by: env)
  Cloud        ✓    Connected: http://localhost:5173 (0 event(s) forwarded)
  Telemetry    ·    Disabled by user (opt-out honoured)
```

### More than one agent

On a host running a single agent the report above is the whole story. On a host
running several, "Hooks is failing" stops being an answer — hooks can be
enforcing for Claude Code and dead for Codex CLI, and the section rollup is
worst-wins, so it reads `✗` either way.

So every row that belongs to one agent names it, and `doctor` and `status` both
close with a matrix: one column per detected agent, one row per section that has
anything to say per agent.

```
Coverage
  Hooks        ✓  claude-code  Enforced
               ✗  codex-cli    Monitored — installed and capturing, enforcing nothing   [OL-1461]
  Model Relay  ✓  claude-code  Agent wired to http://127.0.0.1:7600

Agents
               claude-code  codex-cli
  Environment  ✓            ✓
  Hooks        ✓            ✗
  Model Relay  ✓            —
```

A cell is the worst state among that agent's checks in that section. `—` means
that agent files no check there at all — absence, not a state, and deliberately
not `NotApplicable`. Host-wide findings — the relay listener, the daemon — carry
no agent and stay in the section list where they were.

The section rollup is unchanged and stays worst-wins: if one agent is broken,
something *is* broken. The matrix answers *which*.

`doctor --json` carries the same thing as a top-level `agents` object, keyed by
agent, each entry carrying that agent's per-section rollup under `sections` —
one object per agent, so everything `doctor` has to say about one agent is in
one place. A section an agent files nothing in is absent from its list rather
than null, and a host with no agent at all emits `{}` rather than dropping the
key.

### Exit codes

| Code | Meaning |
| ---- | ------- |
| 0 | Healthy |
| 1 | Something enabled is not working |
| 2 | Usage error |
| 3 | Resource not found — for `init`, no supported agent to connect; for a sign-in, no organization matching `--org` / `OPENLATCH_ORG` (`OL-1610`) |
| 4 | Permission denied |
| 5 | Conflict — `OL-1501` only, because systemd's `RestartPreventExitStatus=5` keys off it |
| **7** | **Degraded** — everything runs, but something is switched off or not at full capability |
| 130 | Interrupted |

`7` matters: `openlatch doctor && deploy` must not succeed on a host where nothing is captured or enforced.

`init` exits on its customer verdict rather than on doctor's: `7` only when a warning
needs you, never for a known coverage limit or a check still settling — see
[`init` output](#init-output).