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 first-boot / pre-deploy companion to basil config check: check probes whether the keys exist in a reachable backend; default doctor probes whether the environment is wired up at all. When an operator explicitly adds --check-keys, doctor also unlocks the sealed bundle and runs the same authenticated read-only key-existence probe that powers basil config check --require.

    📝 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. --check-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
    basil doctor -c /etc/basil/agent.toml --json     # stable machine output
    basil doctor -c /etc/basil/agent.toml --check-keys --json
    FlagMeaning
    -c / --configThe daemon TOML config to diagnose (same file run 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.
    --check-keysOpt in to the authenticated key-material probe: unlock the bundle, build the manager, and read-only-check every catalog key. Required missing keys fail; optional keys warn.

    The checks 

    nameWhat it meansfail / warn
    catalog_policyThe catalog + policy load and validate via the same loader check/run use.fail if either does not load.
    feature_compatibilityThe binary's enabled cargo features cover the optional unlock slots and backends the config declares.fail 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.fail on a bad parent or group; warn on a world-writable mode.
    bundle_readableThe sealed bundle exists at the configured path and is readable.fail if absent/unreadable.
    bundle_permsThe sealed bundle is strict 0600 (owner-only).fail on any broader mode.
    bundle_freshnessThe bundle's epoch is not behind the .epoch sidecar (anti-rollback).fail 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.fail on any unreachable address (it never hangs the run).
    key_material--check-keys only: probes every catalog key read-only, like startup reconcile; no writes.fail on an absent required key or probe error; warn on absent optional 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 

    ExitMeaning
    0No fail check. Advisory warns alone still exit 0.
    1At least one blocking (fail) check: a misconfiguration that would prevent (or endanger) a clean start.

    JSON schema (--json

    The document is versioned and stable. Operators script against the field names and the status tokens.

    {
      "schema_version": 1,
      "checks": [
        { "name": "backend_reachability", "status": "fail",
          "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": 8, "ok": 6, "warn": 0, "fail": 2, "blocking": true }
    }
    FieldTypeMeaning
    schema_versionuintDocument schema version (currently 1).
    checks[]arrayOne object per check, in a deterministic order.
    checks[].namestringStable machine identifier (e.g. backend_reachability).
    checks[].statusstringok / warn / fail.
    checks[].detailstringHuman-readable finding (no secret bytes).
    checks[].remediationstringActionable fix for a non-ok result (empty for ok).
    summary.total / ok / warn / failuintPer-status counts.
    summary.blockingbooltrue iff any check is fail: the run exits 1.
    ✅ 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 == "fail". Add --check-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