Doctor (preflight checks) 

    basil doctor answers "will the daemon even get off the ground here?" before you start it. It resolves the same config the daemon does (-c/--config TOML plus --catalog/--policy/--bundle/--socket overrides and the BASIL_* env vars), then runs a set of independent read-only diagnostics (backend binary & reachability, socket sanity, sealed-bundle readability/permissions/freshness, catalog/policy validation, and cargo-feature compatibility) and reports each as ok / warn / fail with actionable remediation.

    It is the single first-boot / pre-deploy preflight (the former basil config check is folded into it). Its offline tier probes whether the environment is wired up at all: the catalog/policy load, each backend can provide the capabilities its keys need, and any invocation broker-identity/key bindings resolve. Adding --keys layers on an authenticated tier — doctor unlocks the sealed bundle and runs the read-only per-key existence probe the old config check --require performed.

    📝 Read-only, non-mutating, secret-free by default

    Default doctor never unlocks the bundle, binds the socket, reconciles, generates a key, or writes the epoch sidecar. Every step handles its own error into a check result, so one failing check never aborts the rest. The bundle checks report readability, permissions, and freshness only: no bundle contents, key material, passphrases, or tokens ever reach stdout, the JSON, or an error string. --keys is the deliberate boundary crossing: it unlocks the bundle to build the backend manager, then performs only metadata/KV existence reads. It still never reconciles, generates, rotates, imports, or writes the epoch sidecar.

    The subcommand & flags 

    basil doctor -c /etc/basil/agent.toml                  # human-readable, offline
    basil doctor -c /etc/basil/agent.toml --json           # stable machine output
    basil doctor -c /etc/basil/agent.toml --keys --json    # add the authenticated per-key probe
    basil doctor -c /etc/basil/agent.toml --strict         # warnings exit non-zero too
    FlagMeaning
    -c / --configThe daemon TOML config to diagnose (same file agent loads). Individual paths can also be supplied via --catalog/--policy/--bundle/--socket or BASIL_*.
    --jsonEmit the stable, versioned JSON document instead of human-readable text.
    --keysOpt in to the authenticated key-material probe: unlock the bundle, build the manager, and read-only-check every catalog key. A missing required key is fatal; an optional (or missing=generate) key only warns. Emits an aggregate key_material row plus one key_material:<key> row per key.
    --strictTreat warnings as failures: exit non-zero if any check is a warning, not just on a fatal condition.

    The checks 

    nameWhat it meansfatal / warn
    catalog_policyThe catalog + policy load and validate via the same loader agent uses.fatal if either does not load.
    capabilityOffline backend-capability enforcement: each backend provides what its catalog keys (and explicit requires) need. Honors capability-policy.fatal on a capability gap under strict; relaxed to advisory under degraded/off.
    invocation_bindingsOffline validation that, when invocation is enabled, its broker-identity and request/response key bindings resolve to catalog keys of the right shape.ok when invocation is disabled or valid; fatal when enabled but a binding is invalid.
    feature_compatibilityThe binary's enabled cargo features cover the optional unlock slots and backends the config declares.fatal on any mismatch; it names the exact missing feature.
    backend_binaryFor a vault-kind backend, bao or vault is on PATH.warn if neither is found (the daemon talks HTTP and does not strictly need the CLI).
    socketParent dir writable, mode not world-writable, socket-group resolves to a gid; the socket is not bound.fatal on a bad parent or group; warn on a world-writable mode.
    bundle_readableThe sealed bundle exists at the configured path and is readable.fatal if absent/unreadable.
    bundle_permsThe sealed bundle is strict 0600 (owner-only).fatal on any broader mode.
    bundle_freshnessThe bundle's epoch is not behind the .epoch sidecar (anti-rollback).fatal if the epoch is behind or corrupt; warn if the sidecar is absent (first boot).
    backend_reachabilityEach distinct vault address answers an unauthenticated GET /v1/sys/health within ~3s.fatal on any unreachable address (it never hangs the run).
    key_material--keys only: probes every catalog key read-only, like startup reconcile; no writes. Emits an aggregate row plus one key_material:<key> row per key.fatal on an absent required key or probe error; warn on absent optional / missing=generate keys.

    feature_compatibility checks the unlock-slot features (unlock-bip39, unlock-age-yubikey) and the backend features (keystore-backend, aws-kms, gcp-kms), naming any the binary lacks. A normal default build includes 1Password, BIP39, and age/YubiKey support; this check matters most for --no-default-features, cloud KMS, and other custom builds.

    Exit codes 

    The exit code is derived from the worst severity among the checks that ran. Only a fatal condition — one that would stop the broker from starting — exits non-zero on its own; every other finding is a report-only warning.

    ExitMeaning
    0No fatal check. Advisory warns alone still exit 0 (unless --strict).
    non-zeroAt least one fatal check: catalog/policy won't load, a backend is unreachable, the bundle won't unlock or is stale, a capability gap under strict, or a missing=error key the probe can't satisfy. With --strict, a warn (e.g. a missing=generate key, bao not on PATH, loose bundle perms) also exits non-zero.

    JSON schema (--json

    The document is versioned and stable (schema_version is currently 2; version 1 used a fail status token and carried no fatal count). Operators script against the field names and the status tokens.

    {
      "schema_version": 2,
      "checks": [
        { "name": "backend_reachability", "status": "fatal",
          "detail": "1 of 1 vault backend address(es) unreachable: http://127.0.0.1:8200",
          "remediation": "Start/reach the backend (OpenBao/Vault) at the configured address, or fix `addr` …" }
      ],
      "summary": { "total": 11, "ok": 9, "warn": 0, "fatal": 2, "blocking": true }
    }
    FieldTypeMeaning
    schema_versionuintDocument schema version (currently 2).
    checks[]arrayOne object per check, in a deterministic order.
    checks[].namestringStable machine identifier (e.g. backend_reachability, or key_material:<key> for a per-key row under --keys).
    checks[].statusstringok / warn / fatal.
    checks[].detailstringHuman-readable finding (no secret bytes).
    checks[].remediationstringActionable fix for a non-ok result (empty for ok).
    summary.total / ok / warn / fataluintPer-status counts.
    summary.blockingbooltrue iff any check is fatal: the run exits non-zero.
    ✅ Best practice

    Run basil doctor on every node as a pre-start gate (e.g. a systemd ExecStartPre= or a first-boot provisioning step) so a missing run dir, a wrong bundle mode, an unreachable backend, or a feature-mismatched binary is caught before the daemon tries to unlock and bind. Scrape the --json document if you orchestrate fleets: assert summary.blocking == false, or alert on any checks[].status == "fatal". Add --keys when the pre-start environment has access to the same unlock material as the daemon. Pair it with policy explain for full pre-deploy coverage.

    Where to go next