# doctor and adapter verification

> The fifteen `agentcookie doctor` check categories, the `DoctorReport` JSON envelope, `wizard verify-adapters` output, and exit-code semantics for agent consumption.

- Repository: mvanhorn/agentcookie
- GitHub: https://github.com/mvanhorn/agentcookie
- Human docs: https://grok-wiki.com/public/docs/mvanhorn-agentcookie-137da38edfae
- Complete Markdown: https://grok-wiki.com/public/docs/mvanhorn-agentcookie-137da38edfae/llms-full.txt

## Source Files

- `internal/cli/doctor.go`
- `internal/cli/wizard_verify_adapters.go`
- `internal/cli/status.go`
- `internal/cli/secret_coverage.go`
- `internal/state/state.go`

---

---
title: "doctor and adapter verification"
description: "The fifteen `agentcookie doctor` check categories, the `DoctorReport` JSON envelope, `wizard verify-adapters` output, and exit-code semantics for agent consumption."
---

`agentcookie doctor` runs fifteen role-aware self-checks against the local install and emits one row per check; `agentcookie wizard verify-adapters` reads the last sink-side adapter run out of `~/.agentcookie/sink-state.json`. Both surfaces are designed for unattended agent use: each supports a JSON envelope under the global `--json` flag, and exit codes are derived strictly from the severities of the check rows (no partial failures, no human-only signals).

## Command surface

| Command | Reads | Side effects | Exit semantics |
| --- | --- | --- | --- |
| `agentcookie doctor` | Tailscale daemon, `~/.config/agentcookie/`, `~/.agentcookie/`, Chrome Safe Storage probe | None (binds the sink listen address briefly to verify occupancy, then closes) | `0` if no row is `FAIL`; `1` if any row is `FAIL`. `WARN`, `INFO`, and `SKIPPED` never raise exit. |
| `agentcookie doctor --json` | Same | None | Same exit semantics; payload is `DoctorReport`. |
| `agentcookie wizard verify-adapters` | `~/.agentcookie/sink-state.json` | None | `0` on success or no-state-yet; `1` if any adapter result has a non-empty `error`. |
| `agentcookie wizard verify-adapters --json` | Same | None | Same exit semantics; payload is `{status, results}`. |

<Note>
`doctor` never phones home. The only network surface it touches is the local `tailscaled` introspection socket to learn the tailnet IP.
</Note>

## The fifteen check categories

Each row carries a `name`, a `severity` (`ok`, `warn`, `fail`, `info`, `skipped`), a `detail` one-liner, and an optional `remediation` string. Checks gated on role return `skipped` on the wrong role rather than being omitted from the report, so the row count is stable across source-only, sink-only, and dual installs.

| # | Name | Role | Severities used | What it verifies |
| --- | --- | --- | --- | --- |
| 1 | `Binary signature` | both | `ok`, `warn` | `codesign -d -r-` output contains Team ID `NM8VT393AR`. Ad-hoc / unsigned local builds always `warn`, never `fail`. |
| 2 | `Tailscale` | both | `ok`, `fail` | `tsclient.RequireTailnetIP` returns a tailnet address. Remediation pins `tailscale up`. |
| 3 | `Config` | both | `ok`, `fail` | At least one of `source.yaml` / `sink.yaml` exists in `ConfigDir` and parses. Missing both or parse error is `fail`. |
| 4 | `Keystore` | both | `ok`, `fail`, `skipped` | Every configured peer hostname has a key file at `~/.config/agentcookie/keys/<peer>.json` with mode `0600`. |
| 5 | `Sink listener` | sink | `ok`, `fail`, `skipped` | A `net.Listen("tcp", addr)` on `sink.listen.addr` fails (i.e. something else is bound). A successful bind means nothing is listening. |
| 6 | `Sink state` | sink | `ok`, `warn`, `fail`, `skipped` | `~/.agentcookie/sink-state.json` exists, parses, and `LastWrite` is within 24h. Missing state is `fail`; >24h is `warn`. |
| 7 | `Source state` | source | `ok`, `warn`, `fail`, `skipped` | `~/.agentcookie/source-state.json` exists, parses, `LastPush` is within 24h, and `TotalFailures == 0`. |
| 8 | `DBSC` | source | `ok`, `warn`, `skipped` | Informational. `warn` when the last push flagged any device-bound (DBSC-suspect) cookies; carries one sample reason in `detail`. |
| 9 | `Sealing` | sink | `ok`, `skipped` | Reports whether the `agentcookie-master` Keychain item exists. Never raises exit; `disabled` is the v0.12 closed-beta default. |
| 10 | `Adapter coverage` | sink | `ok`, `warn`, `skipped` | Each unique `host_key` in the sidecar matches at least one adapter's `CookieHostPatterns`. Up to 3 uncovered hosts are listed. |
| 11 | `CDP injector` | sink | `ok`, `warn`, `skipped` | When `cdp.enabled`, `cdp.profile_dir` exists and `Google Chrome.app` is installed in `/Applications` or `~/Applications`. |
| 12 | `Secrets bus` | both | `ok`, `warn`, `fail`, `skipped` | Walks `~/.agentcookie/secrets/<cli>/` and reports CLI count, key count, sealed/plaintext/mixed mode, and freshness of the newest file. |
| 13 | `Secret coverage` | both | `ok`, `warn` | For every registered v2 manifest, the synced `secrets.env` either covers each declared `[secrets]` key or there is an `agentcookie secret alias` for it. |
| 14 | `Binary install` | both | `ok`, `warn` | Resolves `~/go/bin/agentcookie`, `~/bin/agentcookie`, `/usr/local/bin/agentcookie`, `/opt/homebrew/bin/agentcookie`, the on-PATH `agentcookie`, and the running binary; `warn` when two non-symlinked copies differ in size or mtime. |
| 15 | `Cookie delivery` | sink | `ok`, `warn`, `info`, `skipped` | Composite verdict over `skip_chrome_sqlite`, the live Chrome Safe Storage probe, and the Safe Storage Keychain item count. See [Cookie delivery verdict table](#cookie-delivery-verdict). |

<Warning>
The check list is part of the agent-facing contract. Names appear verbatim in the `DoctorReport.Checks[].name` field and should be matched by string. Severity strings (`ok`, `warn`, `fail`, `info`, `skipped`) are lowercase in JSON; the human renderer maps them to `[OK]`, `[WARN]`, `[FAIL]`, `[INFO]`, `[--]` for display only.
</Warning>

## DoctorReport JSON envelope

The envelope is intentionally small and stable. The `version` field carries the binary's `Version` string at build time; `exit_code` is the same value the process will exit with.

<ResponseField name="version" type="string" required>
Build version of the `agentcookie` binary that produced the report.
</ResponseField>

<ResponseField name="exit_code" type="integer" required>
`0` when no check has severity `fail`; `1` otherwise. `warn`, `info`, and `skipped` never raise the exit code.
</ResponseField>

<ResponseField name="checks" type="array" required>
Ordered list of check rows. Order is stable across runs and matches the table above.
</ResponseField>

<ResponseField name="checks[].name" type="string" required>
Human-readable check label, e.g. `Binary signature`, `Cookie delivery`.
</ResponseField>

<ResponseField name="checks[].severity" type="string" required>
One of `ok`, `warn`, `fail`, `info`, `skipped`.
</ResponseField>

<ResponseField name="checks[].detail" type="string" required>
One-line description of the observed state. Embeds counts, ages, addresses, and sample identifiers; safe for log capture but not stable across versions.
</ResponseField>

<ResponseField name="checks[].remediation" type="string">
Concrete next command or one-line fix. Omitted when severity is `ok`, `info`, or `skipped` (and sometimes when no remediation is meaningful).
</ResponseField>

<ResponseExample>
```json doctor --json (sink-side, all green)
{
  "version": "v0.13.2",
  "exit_code": 0,
  "checks": [
    {"name": "Binary signature", "severity": "ok", "detail": "Developer ID Application (NM8VT393AR)"},
    {"name": "Tailscale", "severity": "ok", "detail": "100.x.y.z reachable on local tailnet interface"},
    {"name": "Config", "severity": "ok", "detail": "sink.yaml present, parses OK"},
    {"name": "Keystore", "severity": "ok", "detail": "peer key for laptop present (mode 0600)"},
    {"name": "Sink listener", "severity": "ok", "detail": "bound on 100.x.y.z:8443"},
    {"name": "Sink state", "severity": "ok", "detail": "last write 12s ago, mode=cdp, 0 rejected"},
    {"name": "Source state", "severity": "skipped", "detail": "sink-only install"},
    {"name": "Sealing", "severity": "ok", "detail": "disabled (default in v0.12 closed beta)"},
    {"name": "Adapter coverage", "severity": "ok", "detail": "all 42 host_keys in sidecar covered by an adapter"},
    {"name": "CDP injector", "severity": "ok", "detail": "profile_dir=~/.agentcookie/chrome-profile is the synced/logged-in profile, Chrome=/Applications/Google Chrome.app"},
    {"name": "Secrets bus", "severity": "ok", "detail": "3 cli(s), 11 key(s), mode=plaintext, newest 12s ago"},
    {"name": "Secret coverage", "severity": "ok", "detail": "synced secrets match the auth env var each CLI reads"},
    {"name": "Binary install", "severity": "ok", "detail": "single agentcookie binary on this machine"},
    {"name": "Cookie delivery", "severity": "ok", "detail": "universal (delivery=universal): real Default profile written and Chrome Safe Storage readable; any unmodified cookie CLI works here"}
  ]
}
```
</ResponseExample>

<Note>
The `DBSC` row is source-only, so the example above (sink-only) does not contain it. On a source install it appears between `Source state` and `Sealing`. The 15th check exists in every report; `skipped` rows take the place of role-inapplicable checks rather than being omitted.
</Note>

## Cookie delivery verdict {#cookie-delivery-verdict}

`Cookie delivery` is the only composite check. It folds three signals into one row so an agent can branch on a single verdict.

```text
                              skip_chrome_sqlite?
                                      |
                  +-------------------+-------------------+
                  | true                                   | false
                  v                                        v
            [INFO] degraded                          count Safe Storage items
            (sidecar-only; not                              |
            universal)                                      +-- n > 1 -> [WARN] race
                                                            |               (duplicate items)
                                                            |
                                                            v
                                                   probe Chrome Safe Storage
                                                            |
                                +---------------------------+-------------------+
                                | keyLen > 0, no error                          | error or keyLen == 0
                                v                                               v
                          [OK] universal                                  keychain locked?
                          (any cookie CLI works)                                |
                                                                     +----------+-----------+
                                                                     | yes                  | no
                                                                     v                      v
                                                              [INFO] universal-       [WARN] partial
                                                              config-locked            (one-password grant
                                                              (SSH false negative)     has not run)
```

| Input | Result | Severity |
| --- | --- | --- |
| `skip_chrome_sqlite=true` | `degraded`: only agentcookie-aware tools see synced cookies | `info` |
| `n > 1` Safe Storage items | `race`: install-time Chrome relaunch left duplicates | `warn` |
| Probe returns `keyLen > 0` | `universal`: any unmodified cookie CLI works | `ok` |
| Probe error + login keychain locked | `universal config; key can't be verified from this session` | `info` |
| Probe error / `keyLen == 0`, single item | `partial`: real profile written, key not granted | `warn` |

The canonical remediation for the `partial` and `degraded` rows is the one-password grant `agentcookie wizard set-keychain-access` (non-interactive form: `AGENTCOOKIE_LOGIN_PASSWORD=… agentcookie wizard set-keychain-access`).

## Exit-code semantics for agents

The exit code is derived after every check runs, so a single `fail` does not short-circuit the rest of the report.

<Steps>
<Step title="Iterate Checks in order">
The runner walks `checks[]` and sets `exit_code = 1` on the first `severity == "fail"`.
</Step>
<Step title="WARN/INFO/SKIPPED never affect exit">
`Adapter coverage` warnings, `DBSC` warnings, `Binary install` divergence, locked-keychain `Cookie delivery` info, and any `skipped` row leave exit at `0`. An agent that wants to gate on these must inspect `severity` per row.
</Step>
<Step title="Process exits before any deferred work">
`runDoctor` calls `os.Exit(report.ExitCode)` directly after rendering. Any wrapper that expects Go's `RunE` error return to set the exit code should switch to inspecting the JSON envelope's `exit_code` field instead.
</Step>
</Steps>

<Tip>
For automated gating, prefer `agentcookie --json doctor` and pivot on `exit_code` plus a name/severity filter. The human renderer footer (`all green` / `N FAIL, M WARN`) is for operators, not parsers.
</Tip>

## wizard verify-adapters

`verify-adapters` is the sink-side post-mortem of the most recent sinkpush adapter run. It does not re-run adapters; it reads the `last_adapter_results` array out of `~/.agentcookie/sink-state.json`, which the sink populates after each accepted `/sync` write.

### Text output

```text
$ agentcookie wizard verify-adapters
ADAPTER                          STATUS  PUSHED  DETAIL
-------                          ------  ------  ------
instacart-pp-cli                 ok      14
airbnb-pp-cli                    ok      3
ebay-pp-cli                      skip            no ebay cookies in this batch
pagliacci-pp-cli                 FAIL            seal: master key not in Keychain
table-reservation-goat-pp-cli    ok      6

last run: 1m42s ago
```

Status values:

| Value | Meaning |
| --- | --- |
| `ok` | Adapter ran and wrote without error. `PUSHED` is the count of session entries written. |
| `skip` | Adapter ran but had nothing to do; `DETAIL` carries the `SkippedReason`. |
| `FAIL` | Adapter ran and returned an error; `DETAIL` carries the error string. Exit code becomes `1`. |

### JSON output

`agentcookie --json wizard verify-adapters` emits one of three envelopes:

```json no-state-yet
{"status": "no_state"}
```

```json no-runs-yet
{"status": "no_runs"}
```

```json ok
{
  "status": "ok",
  "results": [
    {"name": "instacart-pp-cli", "pushed": 14, "ran_at": "2026-05-31T10:14:21Z"},
    {"name": "airbnb-pp-cli", "pushed": 3, "ran_at": "2026-05-31T10:14:21Z"},
    {"name": "ebay-pp-cli", "skipped": true, "skipped_reason": "no ebay cookies in this batch", "ran_at": "2026-05-31T10:14:21Z"},
    {"name": "pagliacci-pp-cli", "error": "seal: master key not in Keychain", "ran_at": "2026-05-31T10:14:21Z"},
    {"name": "table-reservation-goat-pp-cli", "pushed": 6, "ran_at": "2026-05-31T10:14:21Z"}
  ]
}
```

<Info>
`verify-adapters` returns exit `0` for `no_state` and `no_runs`, since both are valid pre-first-sync conditions on a freshly paired sink. An agent gating on adapter health should treat `no_runs` as “run `agentcookie source --once` first”, not as a failure.
</Info>

### AdapterResult shape

The `results[]` entries are `state.AdapterResult` records, persisted by the sink writer and re-emitted verbatim.

<ResponseField name="name" type="string" required>
Registered adapter identifier (e.g. `instacart-pp-cli`).
</ResponseField>

<ResponseField name="pushed" type="integer">
Number of session entries written to the adapter's target file. Omitted when zero.
</ResponseField>

<ResponseField name="invalid" type="integer">
Entries the adapter's validate hook rejected. Omitted when zero.
</ResponseField>

<ResponseField name="skipped" type="boolean">
`true` when the adapter ran but had no work; `skipped_reason` carries the explanation.
</ResponseField>

<ResponseField name="skipped_reason" type="string">
Human-readable skip reason, e.g. `no ebay cookies in this batch`.
</ResponseField>

<ResponseField name="error" type="string">
Adapter error string. Non-empty drives `FAIL` status and exit `1`. The sink itself does not abort on adapter failures: the cookie write to Chrome's Default profile and the sidecar still succeed, so adapter `FAIL` is a partial-delivery signal, not a sync failure.
</ResponseField>

<ResponseField name="ran_at" type="string (RFC 3339)" required>
Wall-clock time the adapter run started. All entries from one sinkpush invocation share the same `ran_at`.
</ResponseField>

## What doctor does not check

These are intentionally out of scope and live in other commands:

- **Live sync correctness.** `doctor` does not POST to `/sync`. Use `agentcookie source --once` to force a push and re-run `doctor` to confirm `Sink state.last_write` advanced.
- **End-to-end secrets sync.** `Secrets bus` and `Secret coverage` describe on-disk state, not whether a CLI can read it. Use `agentcookie discover` to view the per-CLI declared keys, resolved aliases, and the live coverage verdict for one CLI at a time.
- **Adapter rerun.** `wizard verify-adapters` does not re-execute adapters. To re-run them, push again from the source side.
- **Pairing.** `Keystore` confirms a key file exists for each configured peer; it does not handshake. A missing or wrong-mode key is the signal to re-run `agentcookie pair`.

## Related pages

<CardGroup>
<Card title="CLI reference" href="/cli-reference">Every `agentcookie` subcommand and flag, including `doctor`, `wizard`, `status`, and `secret`.</Card>
<Card title="Configuration files reference" href="/config-files-reference">Schemas and validation rules `doctor` exercises against `source.yaml` and `sink.yaml`.</Card>
<Card title="Universal cookie delivery" href="/universal-delivery">The one-password grant and `teamid:` partition list behind the `Cookie delivery` verdict.</Card>
<Card title="Troubleshooting" href="/troubleshooting">Recovery paths for the most common `WARN`/`FAIL` remediations doctor emits.</Card>
<Card title="Write a cookie adapter" href="/write-cookie-adapter">How an adapter's `CookieHostPatterns` and validate hook flow into `Adapter coverage` and `verify-adapters`.</Card>
<Card title="LaunchAgent management" href="/launchagent-management">When `Sink listener` reports nothing bound, this is the bootstrap path.</Card>
</CardGroup>
