---
title: Doctor checks
description: What hubzoid doctor checks, its text and JSON output, every stable check id with its possible statuses, and how to fix each failure, including configuration secrets.
canonical_url: https://hubzoid.com/docs/reference/doctor
last_updated: 2026-09-27
---

# Doctor checks

What hubzoid doctor checks, its text and JSON output, every stable check id with its possible statuses, and how to fix each failure, including configuration secrets.

`hubzoid doctor` checks a Hub and the deployment around it: files, configuration layers and secrets, the agent build, schedules and workflows, database schemas, sign-in, network exposure, model credentials, backups and scheduled work. It only reads. It never creates a database, runs a migration or starts the workflow engine, so it is safe to run against a live deployment. It reports setting names and where they come from, never their values.

```bash
hubzoid doctor ./ops-desk
hubzoid doctor ./ops-desk --json
hubzoid doctor ./ops-desk --skip-secret-fetch
```

Doctor loads the Hub's `.env` into its own process before it checks anything, the same way the Hub does when it starts. Run it with the environment the Hub runs with, for example as the service user on the server, so checks such as `auth.bridge_keys` see the real values. When the configuration names AWS Secrets Manager secrets, doctor reads each one once to prove it is reachable. `--skip-secret-fetch` lists them without calling AWS, and the other checks then use the files alone.

## Output

Each check has a status:

| Status | Mark | Meaning |
| --- | --- | --- |
| `ok` | `✓` | Fine. |
| `info` | `·` | Worth knowing. Nothing to fix. |
| `warn` | `!` | Works, but needs attention. |
| `fail` | `✗` | Will not work, or is unsafe. |

The text output prints one line per check with its id in brackets, and lists the details under any `warn` or `fail`. Under `config.layers` it lists each configured key with its layer and source:

```text
✓ AGENTS.md present (hub.agents_md)
✓ .env present (hub.env)
· 2 configured key(s): 2 from the hub (config.layers)
    BRIDGE_API_KEYS: hub (/srv/hubs/ops-desk/.env)
    MODEL: hub (/srv/hubs/ops-desk/.env)
✓ Agent builds: 'ops-desk' via ClaudeRuntime (runtime.build)
✓ 2 enabled task(s) (schedule.tasks)
· hubzoid 1.0.1 (deps.versions)
✓ SQLite 3.46.1 (deps.sqlite)
✓ Schema at op_0007 (db.operational)
✓ Schema at hub_0001 (db.hub)
✓ 1 bridge key(s) set (auth.bridge_keys)
· Chat app sign-in is off (fine for local use only) (auth.chat_signin)
✓ The public port listens on 127.0.0.1; bridges stay on 127.0.0.1 (exposure.bind)
· claude-local: uses the local `claude` login (check it with `claude /status`) (model.credentials)
! No backup recorded. Run `hubzoid backup`. (backup.age)
✓ Scheduled work is not held or paused (scheduler.health)
```

Exit codes: `0` when no check fails, `1` when any check fails, `2` when the Hub folder does not exist. Warnings do not change the exit code.

### JSON format

`--json` prints one object. Check ids are only ever added, never renamed, so scripts and monitoring can key on them.

```json
{
  "format": 1,
  "hub": "/srv/hubs/ops-desk",
  "hubzoid": "1.0.1",
  "ok": false,
  "checks": [
    {
      "id": "auth.bridge_keys",
      "status": "fail",
      "summary": "BRIDGE_API_KEYS includes the public key 'dev'",
      "detail": null
    },
    {
      "id": "backup.age",
      "status": "ok",
      "summary": "Last backup 1.2 days ago",
      "detail": { "at": 1790000000.0, "path": "/srv/backups/hubzoid-backup-20260924-020000.tar.gz", "by": "cli:hubzoid@ops-server" }
    }
  ]
}
```

| Field | Meaning |
| --- | --- |
| `format` | Output format version. Hubzoid 1.0.1 writes `1`. |
| `hub` | Absolute path of the Hub checked. |
| `hubzoid` | Installed Hubzoid version. Absent when the Hub folder does not exist. |
| `ok` | `false` when any check has status `fail`. |
| `checks[].id` | Stable check id. |
| `checks[].status` | `ok`, `info`, `warn` or `fail`. |
| `checks[].summary` | One line for people. |
| `checks[].detail` | A list, an object or `null`, depending on the check. |

When the Hub folder is missing, the output holds a single `hub.dir` check with status `fail`.

A monitoring job can alert on failures only:

```bash
hubzoid doctor /srv/hubs/ops-desk --json | jq -r '.checks[] | select(.status == "fail") | .id'
```

## Checks

Checks run in the order below. Some appear only when the thing they check exists, as noted.

| Id | Statuses | Appears |
| --- | --- | --- |
| `hub.dir` | fail | `--json` only, when the Hub folder is missing |
| `hub.agents_md` | ok, fail | always |
| `hub.env` | ok, info | always |
| `secrets.deployment` | ok, info, fail | when a deployment secret is named (`AWS_SECRET_NAME`) |
| `secrets.hub` | ok, info, fail | when a Hub secret is named (`HUBZOID_HUB_SECRET_NAME`) |
| `secrets.restricted` | ok, info, fail | when a restricted secret is named (`HUBZOID_RESTRICTED_SECRET_NAME`) |
| `secrets.names` | warn | when a secret name is set where it is ignored |
| `config.layers` | info | when a file or secret sets any key |
| `runtime.build` | ok, fail | always |
| `runtime.codex_login` | ok, fail | when the agent builds on `codex-local` |
| `schedule.tasks` | ok, fail | when `schedule/` has tasks or invalid files |
| `workflows.definitions` | ok, fail | when `workflows/` has workflows |
| `access.restricted` | info, fail | when `restricted/` has tools |
| `identity.resolver` | info, fail | when `identity/` has a roster |
| `deps.versions` | info | always |
| `deps.sqlite` | ok, fail | when the workflow engine uses SQLite |
| `db.operational` | ok, info, warn, fail | always |
| `db.hub` | ok, info, warn, fail | always |
| `auth.bridge_keys` | ok, warn, fail | always |
| `auth.chat_signin` | ok, info, fail | always |
| `auth.google_merge` | ok, warn | when `GOOGLE_CLIENT_ID` is set |
| `exposure.bind` | ok, warn | always |
| `model.credentials` | ok, info, warn, fail | always |
| `backup.age` | ok, warn | when the operational store exists |
| `scheduler.health` | ok, info, warn, fail | when the operational store exists and the Hub has tasks or workflows |
| `db.read` | fail | when the operational store cannot be read |

### hub.agents\_md

Whether `AGENTS.md` exists at the Hub root. **Fail:** create it, or run `hubzoid init` to scaffold a Hub. Nothing else works without it.

### hub.env

Whether the Hub has a `.env`. **Info** when it does not, which is normal when settings come from the process environment (a systemd unit or a container).

### secrets.deployment, secrets.hub and secrets.restricted

One check for each AWS Secrets Manager secret the configuration names. See [configuration secrets](https://hubzoid.com/docs/reference/configuration#configuration-secrets).

- **Ok** when the secret can be read, with the number of keys it holds. The detail names the secret, its region and where the name was set, never a value.
- **Info** with `--skip-secret-fetch`, which lists the secret without reading it.
- **Fail** when it cannot be read. The summary names the secret, the layer and the AWS error class. Check the name, the region and the credentials of the account that runs the Hub. The other checks then use the files alone.

### secrets.names

**Warn** when a secret name is set where Hubzoid ignores it: `AWS_SECRET_NAME` in the `.env` of a Hub that belongs to a gateway, `HUBZOID_HUB_SECRET_NAME` anywhere but the Hub's `.env`, or `HUBZOID_RESTRICTED_SECRET_NAME` anywhere but `restricted/.env`. Move the name to the place the message gives.

### config.layers

**Info** listing each key a file or secret sets, with the layer it comes from (hub, hub secret, restricted, restricted secret or deployment secret), its source and any lower layer it overrides. Names only, never values. Use it to see which value wins.

### runtime.build

Builds the agent exactly as the bridge would, and reports the runtime it chose: `OpenAIAgentsRuntime`, `ClaudeRuntime` or `CodexRuntime`. **Fail** shows the error. Common causes:

- Invalid frontmatter in `AGENTS.md` or a sub-agent, or a file with no body.
- A model id whose provider key is missing, for example `MODEL is set to 'openrouter/...' but OPENROUTER_API_KEY is not`.
- A skill file that cannot be parsed, or invalid JSON in `connectors/.mcp.json`.

Fix the file named in the message, then confirm with `hubzoid test`.

### runtime.codex\_login

Appears when the agent builds on `codex-local`. **Ok** when Codex CLI 0.147.0 is installed and its file-backed login works. **Fail** otherwise: install `@openai/codex@0.147.0` and run `codex -c 'cli_auth_credentials_store="file"' login` as the account that runs the Hub. The check reads the login status only and makes no model call. See [agents and models](https://hubzoid.com/docs/concepts/agents-and-models#codex-models).

### schedule.tasks

Parses every file in `schedule/`. **Ok** reports the number of enabled tasks and lists all task names. **Fail** lists each invalid file with the reason: a missing trigger (`schedule:` or `on_webhook:`), both triggers at once, a cron that never matches, an empty body on an agent task, `push: true` without `commit:`, or a limit that is not a positive integer. Invalid tasks are skipped at runtime while valid ones keep firing. `hubzoid schedule list` shows the same problems. See [markdown tasks](https://hubzoid.com/docs/guides/markdown-tasks).

### workflows.definitions

Loads the code workflows under `workflows/`. **Ok** lists each one with its schedule (or `manual`) and timezone. **Fail** lists `source: error` for each workflow that does not load. While a workflow module is broken, code workflows are off for that start, and markdown tasks keep running. See [code workflows](https://hubzoid.com/docs/guides/code-workflows).

### access.restricted

Imports `restricted/*.py`. **Info** lists the permissions the restricted tools require. **Fail** means a file does not import. Fix the Python error it names. See [restrict tools](https://hubzoid.com/docs/guides/restrict-tools).

### identity.resolver

Loads `identity/access.py` or `identity/access.csv`. **Info** confirms a roster is present. **Fail** means the roster raised an error while loading, most often an exception in `access.py`. See [Hub files](https://hubzoid.com/docs/reference/hub-files#identity-identity).

### deps.versions

**Info** with the installed versions of `hubzoid`, `open-webui`, `dbos`, `litellm`, `openai-agents`, `claude-agent-sdk`, `alembic`, `sqlalchemy`, `casbin` and `fastmcp`. A missing package shows `null`. Include this detail when you report a problem.

### deps.sqlite

Appears when the workflow engine's database is SQLite. **Ok** reports the SQLite version. **Fail** means Python 3.12 or newer is linked against SQLite older than 3.42, which the workflow engine cannot use, so scheduled tasks and workflows cannot run. Use a Python build with a newer SQLite (python.org, Homebrew, Debian 13 or Ubuntu 24.04) or PostgreSQL. The Docker image already ships a newer SQLite.

### db.operational and db.hub

The schema version of the operational store (access, identities, audit, usage) and of the Hub's own database.

| Status | Summary | What to do |
| --- | --- | --- |
| ok | `Schema at <revision>` | Nothing. |
| info | `Not created yet; created at first start` | Nothing. The database appears when the Hub first starts. |
| info | `Unversioned; upgraded to <revision> at next start` | Nothing. The next start upgrades it. |
| warn | `Schema at <old>; upgraded to <new> at next start` | Take a backup with `hubzoid backup`, then start the Hub to upgrade. |
| fail | `Schema at unknown revision ...` | The database was written by a newer Hubzoid. Upgrade Hubzoid, or restore a backup taken before the newer version ran. |
| fail | `Database is misconfigured: ...` | A database variable disagrees with the registered gateway manifest. Fix the gateway's configuration rather than one bridge. |
| fail | `Database unreachable: ...` | Check the URL, network and credentials of the database. |

See [upgrading](https://hubzoid.com/docs/deploy/upgrading).

### auth.bridge\_keys

Checks `BRIDGE_API_KEYS`.

- **Fail** when it is unset or includes `dev`. `dev` is a public default, so anyone who reaches the bridge could use it. Set one or more random keys, for example from `openssl rand -hex 32`. `hubzoid init` writes a random key into a new Hub's `.env`.
- **Warn** when a key is shorter than 16 characters.

### auth.chat\_signin

Checks web chat sign-in.

- **Ok** when `WEBUI_AUTH` is on and its configuration is valid.
- **Fail** when `WEBUI_AUTH` is on and `WEBUI_SECRET_KEY` is unset or still the Open WebUI default. Set a random value of 32 or more characters.
- **Fail** when `WEBUI_AUTH` is on and an OAuth client id (`GOOGLE_CLIENT_ID`, `MICROSOFT_CLIENT_ID`, `GITHUB_CLIENT_ID` or `OAUTH_CLIENT_ID`) is set without `WEBUI_URL`. Sign-in callbacks are built from `WEBUI_URL`.
- **Fail** when sign-in is off and `HUBZOID_HOST` exposes the port (`0.0.0.0`, `::` or empty). Turn sign-in on before you expose the port.
- **Info** when sign-in is off and the port stays on loopback, which is fine on your own machine.

See [authentication](https://hubzoid.com/docs/deploy/authentication).

### auth.google\_merge

Appears when `GOOGLE_CLIENT_ID` is set. **Ok** when `OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true`, so Google sign-in attaches to an account created in the Console. **Warn** otherwise, because people with a Console-created account cannot then sign in with Google. Set `OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true`.

### exposure.bind

Where the public port listens. **Warn** when `HUBZOID_HOST` is `0.0.0.0`, `::` or empty: put TLS in front of the port, for example with a reverse proxy. **Ok** otherwise. The detail confirms that bridges stay on `127.0.0.1`.

### model.credentials

Whether the Hub's model has credentials in the environment.

- For `claude-local`: **ok** when `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN` is set, **info** when the `claude` CLI is on the `PATH` (check its login with `claude /status`), **fail** when there is neither. On a server, set `CLAUDE_CODE_OAUTH_TOKEN`.
- For `codex-local`: the CLI and its login are checked by `runtime.codex_login`.
- For any other model: **ok** when LiteLLM finds the provider's keys, **fail** listing the missing variables, **warn** when the check itself could not run.

See [agents and models](https://hubzoid.com/docs/concepts/agents-and-models).

### backup.age

When the last `hubzoid backup` of this deployment completed. **Warn** when no backup is recorded or the last one is more than 7 days old. **Ok** otherwise, with the backup record as detail. See [backup and restore](https://hubzoid.com/docs/deploy/backup-and-restore).

### scheduler.health

The state of scheduled work, checked in this order:

| Status | Summary | What to do |
| --- | --- | --- |
| warn | `New scheduled runs are held by a backup in progress` | Wait for the backup to finish. The hold lifts on its own. |
| fail | `Workflow dispatch failed: ...` | The workflow engine could not start or load workflows at the last start. Fix the error, then restart the Hub. |
| warn | `No dispatcher heartbeat recently: ...` | Code workflow dispatch is on but has not reported for over 150 seconds. The Hub is stopped or stuck. Check the process and its logs. |
| info | `<n> paused` | Some tasks or workflows are paused. The detail lists them. Resume with `hubzoid schedule resume`. |
| ok | `Scheduled work is not held or paused` | Nothing. |

See [operating runs](https://hubzoid.com/docs/guides/operating-runs).

### db.read

**Fail** when the operational store exists but cannot be read. The message names the error. Check the database file's permissions, or the database server.

## Use it in operations

- Run `hubzoid doctor` after installing, after every upgrade and after `hubzoid restore`.
- In a gateway, run it for each Hub. The operational store and backup checks read the store the whole gateway shares.
- Wire `--json` into monitoring and alert on `ok: false`, or on specific ids such as `scheduler.health` and `backup.age`.

## Next steps

- [CLI reference](https://hubzoid.com/docs/reference/cli): All commands, including doctor, backup and schedule.
- [Configuration](https://hubzoid.com/docs/reference/configuration): The variables these checks read.
- [Upgrading](https://hubzoid.com/docs/deploy/upgrading): Move to a new release safely.
- [Observability](https://hubzoid.com/docs/deploy/observability): Logs, traces and cost in production.
