Doctor checks
What hubzoid doctor checks, its text and JSON output, every stable check id with its statuses, and how to fix each failure.
Every check hubzoid doctor runs, with its statuses and the fix for each failure. Doctor checks a Hub and the deployment around it: files, configuration layers and secrets, the agent build, schedules and workflows, database schemas, required evals, sign-in, the chat UI, the deployment key, network exposure, model credentials, backups, scheduled work, who may use the agent, the Hub's own settings and stored secrets. 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.
hubzoid doctor my-hubhubzoid doctor my-hub --json
hubzoid doctor my-hub --skip-secret-fetchDoctor 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. |
pending | … | A required eval has not passed for the Hub as it is now. Rerun it. |
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. This is a real run against the starter from hubzoid init my-hub, after its first start and one scheduled task added:
✓ AGENTS.md present (hub.agents_md)
✓ .env present (hub.env)
· 2 configured key(s): 2 from the hub (config.layers)
BRIDGE_API_KEYS: hub (~/agents/my-hub/.env)
MODEL: hub (~/agents/my-hub/.env)
✓ Agent builds: 'my-hub' via ClaudeRuntime (runtime.build)
✓ 1 enabled task(s) (schedule.tasks)
· hubzoid <version> (deps.versions)
✓ SQLite 3.53.4 (deps.sqlite)
✓ Schema at op_0081 (db.operational)
✓ Schema at hub_0001 (db.hub)
✓ 1 bridge key(s) set (auth.bridge_keys)
· Sign-in is off: local mode, whoever opens the web app is the hub's owner (admin@localhost). Fine on this machine. (auth.chat_signin)
· Chat UI: Hubzoid web app (ui.mode)
✓ Local mode: the web app stays on 127.0.0.1 (exposure.local_mode)
· No deployment key yet; one is created in ~/agents/my-hub/.hubzoid/secret.key when first needed. Back it up apart from the data. (deployment.key)
✓ 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)
✓ Last backup 0.0 days ago. Scheduled backups run weekly. (backup.age)
✓ Scheduled work is not held or paused (scheduler.health)
· Only the owner (admin@localhost) may use my-hub so far. Add people in Settings, on the agent's Access tab. (access.who)Before a Hub's first start, db.operational and db.hub say they are created at first start, and the checks that read the operational store (backup.age, scheduler.health, access.who) do not appear yet.
Exit codes: 0 when no check fails, 1 when any check fails or a required eval is pending, 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. This is the same kind of run, shortened to five checks:
{
"format": 1,
"hub": "~/agents/my-hub",
"hubzoid": "<version>",
"ok": true,
"checks": [
{
"id": "hub.agents_md",
"status": "ok",
"summary": "AGENTS.md present",
"detail": null
},
{
"id": "config.layers",
"status": "info",
"summary": "2 configured key(s): 2 from the hub",
"detail": [
{ "key": "BRIDGE_API_KEYS", "layer": "hub", "source": "~/agents/my-hub/.env", "shadows": [] },
{ "key": "MODEL", "layer": "hub", "source": "~/agents/my-hub/.env", "shadows": [] }
]
},
{
"id": "auth.chat_signin",
"status": "info",
"summary": "Sign-in is off: local mode, whoever opens the web app is the hub's owner (admin@localhost). Fine on this machine.",
"detail": { "mode": "local" }
},
{
"id": "ui.mode",
"status": "info",
"summary": "Chat UI: Hubzoid web app",
"detail": { "ui_mode": "hubzoid", "auth": false, "public_url": "", "allowed_origins": [], "source": "default" }
},
{
"id": "deployment.key",
"status": "info",
"summary": "No deployment key yet; one is created in ~/agents/my-hub/.hubzoid/secret.key when first needed. Back it up apart from the data.",
"detail": { "source": "file", "path": "~/agents/my-hub/.hubzoid/secret.key" }
}
]
}| Field | Meaning |
|---|---|
format | The version of this JSON shape, currently 1. It changes only if the shape of the output changes, so a script can check it before reading the rest. |
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 or pending. |
checks[].id | Stable check id. |
checks[].status | ok, info, warn, fail or pending. |
checks[].summary | One line for people. |
checks[].detail | A list, an object or null, depending on the check. Names and sources only, never a setting's value. |
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:
hubzoid doctor ~/agents/my-hub --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 the Hub has identity/access.py |
deps.versions | info | always |
data.dir | info, warn, fail | when HUBZOID_DATA_DIR keeps state apart from the Hub folder |
deps.sqlite | ok, fail | when the workflow engine uses SQLite |
db.operational | ok, info, warn, fail | always |
db.hub | ok, info, warn, fail | always |
evals.quality, eval.<case> | ok, fail, pending | when the Hub has quality/manifest.yaml |
auth.bridge_keys | ok, warn, fail | always |
auth.chat_signin | ok, info, warn | always |
ui.mode | info | always |
ui.retired_setting | warn, fail | when an Open WebUI setting is still set |
ui.openwebui_data | info, warn, fail | when an Open WebUI install is found whose people are not in Hubzoid yet |
exposure.local_mode | ok, warn, fail | when sign-in is off |
deployment.key | ok, info, warn, fail | always |
auth.google_merge | ok, warn | when GOOGLE_CLIENT_ID is set |
exposure.bind | ok, warn | always |
tools.legacy_management_flag | warn | when HUBZOID_MANAGEMENT_TOOLS=true |
mcp.access_group | warn | when MCP_ACCESS_GROUP is set |
model.credentials | ok, info, warn, fail | always |
backup.age | ok, warn, fail | when the operational store exists |
scheduler.health | ok, info, warn, fail | when the operational store exists and the Hub has tasks or workflows |
access.who | ok, info, warn | when the operational store exists |
db.read | fail | when the operational store cannot be read |
settings.files | ok, fail | when the Hub has settings.yaml, hub.settings.yaml or hub.manifest.yaml |
settings.values | ok, fail | when the Hub declares settings |
probe.<id> | the probe's own | one per check declared in hub.manifest.yaml |
secrets.store | ok, warn | when secrets are stored in Hubzoid's database |
secrets.missing | warn | when the Hub expects a secret nothing sets |
secrets.blank | warn | when a file sets a secret to an empty value |
secrets.overrides | warn | when the Hub's files set a different value than the deployment |
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.
- 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 runs with others, 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 (shadows). Names only, never values. Use it to see which value wins.
runtime.build
Builds the agent exactly as the bridge would, and reports the agent's name and the runtime it chose: OpenAIAgentsRuntime, ClaudeRuntime or CodexRuntime. Fail shows the error. Common causes:
- Invalid frontmatter in
AGENTS.mdor 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 a supported Codex CLI (0.163.0 or 0.147.0) is installed and its file-backed login works. Fail otherwise: install one of those versions, or point HUBZOID_CODEX_BIN at one, 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.
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, for example 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:, a limit that is not a positive integer, an invalid run_as or alert_to, or publish_artifacts or send_email on a run: script task. Invalid tasks are skipped at runtime while valid ones keep firing. hubzoid schedule list shows the same problems. See markdown tasks.
workflows.definitions
Reads the code workflows under workflows/ without running them. Ok lists each one with its schedule (or manual) and timezone. Fail lists <file>: <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.
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.
identity.resolver
Loads the Hub's own sender lookup, identity/access.py. Info confirms it is present. Fail means it raised an error while loading. identity/access.csv is no longer read: record phone numbers on people in Settings instead. See Hub files.
deps.versions
Info with the installed versions of hubzoid, dbos, litellm, openai-agents, claude-agent-sdk, alembic, sqlalchemy, casbin, fastmcp, pwdlib and authlib. A missing package shows null. Include this detail when you report a problem.
data.dir
Appears when HUBZOID_DATA_DIR keeps the Hub's state apart from its folder, as on a hosted Hub whose files come from an image.
- Info names where state lives. The Hub's own files are read-only in Settings: change them in Git and redeploy.
- Warn when the Hub folder still holds an older
.hubzoid/hub.db. Stop the Hub, move that folder into the data folder or remove it, then start it, so only one copy is used. - Fail when the data folder is not a folder this process can write. Point it at a durable volume such as
/data.
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, uv, 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 (accounts, sessions, conversations, access, audit and usage) and of the Hub's own database. On a single Hub with SQLite both live in .hubzoid/hub.db.
| 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 deployment manifest. Fix the deployment's configuration rather than one bridge. |
| fail | Database unreachable: ... | Check the URL, network and credentials of the database. |
See upgrading.
evals.quality and required evals
Appear when the Hub requires evals in quality/manifest.yaml (required: case ids or globs, required_tags:). One eval.<case> row per required case:
- Ok when its latest result passed for the case and the Hub as they are now.
- Fail when its latest result failed, or the case file cannot be read.
- Pending when it never ran, ran without its judge, or the case or the Hub's instructions, tools or configuration changed since. Rerun it with
hubzoid eval run. Doctor is not ok until it passes.
evals.quality fails when the manifest itself is invalid or the results cannot be read. See evals.
auth.bridge_keys
Checks BRIDGE_API_KEYS.
- Fail when it is unset or includes
dev.devis a public default, so anyone who reaches the bridge could use it. Set one or more random keys, for example fromopenssl rand -hex 32.hubzoid initwrites a random key into a new Hub's.env. - Warn when a key is shorter than 16 characters.
auth.chat_signin
Checks sign-in for the chat UI the Hub runs.
Sign-in is on with HUBZOID_AUTH=true, or as the deployment recorded it:
- Info when sign-in is off. This is local mode: whoever opens the web app is the Hub's owner (
admin@localhost), which is fine on your own machine.exposure.local_modechecks that the port stays local. - Ok when sign-in is on and Hubzoid accounts exist, or a first administrator is configured.
- Warn when sign-in is on but there is no account yet and no first administrator configured. Set
HUBZOID_ADMIN_EMAILandHUBZOID_ADMIN_PASSWORDfor the first start, or create one withhubzoid admin create. Also warn when the accounts cannot be read. Seedb.operational.
See authentication.
ui.mode
Info naming the chat UI, the Hubzoid web app. The detail gives whether sign-in is on, the public URL and the allowed origins.
ui.retired_setting and ui.openwebui_data
For deployments upgraded from the older Open WebUI chat.
ui.retired_settingnames an Open WebUI setting Hubzoid no longer uses, with what to do instead. Fail when the setting stopshubzoid run, warn otherwise. Remove it.ui.openwebui_datafinds an Open WebUI install whose people are not in Hubzoid yet. Info with sign-in off: its chats can be imported withhubzoid migrate openwebui. Fail with sign-in on:hubzoid runstops until you run the migration. Warn or fail when the Hubzoid accounts cannot be read. See upgrading.
exposure.local_mode
Appears when sign-in is off. Ok when HUBZOID_HOST is a loopback address, so the web app stays on this machine. Fail when it is not: hubzoid run refuses to start. Turn sign-in on with HUBZOID_AUTH=true, or bind 127.0.0.1. Warn when HUBZOID_ALLOW_UNAUTHENTICATED_NETWORK=true accepts that risk: anyone who can reach the port acts as the Hub's owner.
deployment.key
The key that encrypts stored credentials, such as personal connection tokens and connector secrets, and signs identity assertions between Hubzoid processes. It is reported by fingerprint only and never created by doctor. It comes from HUBZOID_SECRET_KEY, or from a secret.key file next to the manifest of a deployment of several Hubs, or .hubzoid/secret.key for a single Hub.
- Ok with the source and fingerprint.
- Info when no key exists yet. One is created when first needed. Back it up apart from the data.
- Warn when the key file is readable by other accounts. Run
chmod 600on it. - Fail when
HUBZOID_SECRET_KEYis malformed or the key file cannot be read. Without the key, encrypted credentials such as personal connection tokens cannot be read.
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 Settings or with hubzoid admin. Warn otherwise, because people with such an 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.
tools.legacy_management_flag
Warn when HUBZOID_MANAGEMENT_TOOLS=true. The setting is deprecated: it gives every manager the access tools without a grant. Grant Manage access from chat (access_tools) in Settings to the managers who use them, then remove the setting.
mcp.access_group
Warn when MCP_ACCESS_GROUP is set. It is no longer read: who may use an agent over MCP is decided by the agent's Use this agent grant in Settings. Remove the setting.
model.credentials
Whether the Hub's model has credentials in the environment.
- For
claude-local: ok whenANTHROPIC_API_KEYorCLAUDE_CODE_OAUTH_TOKENis set, info when theclaudeCLI is on thePATH(check its login withclaude /status), fail when there is neither. On a server, setCLAUDE_CODE_OAUTH_TOKEN. The optional backup credentialsCLAUDE_CODE_OAUTH_TOKEN_SECONDARYandCLAUDE_CODE_OAUTH_TOKEN_TERTIARYare not checked. - 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.
- For
codex-local, this check reports ok without testing anything.runtime.codex_loginchecks the Codex CLI and its login.
See agents and models.
backup.age
When the last backup of this deployment completed, and how often scheduled backups run (HUBZOID_BACKUPS, weekly by default).
- Ok with the age of the last backup.
- Warn when no backup is recorded, the last one is more than 7 days old, or the last attempt failed. The message gives the error. Fix the cause, then run
hubzoid backup. - Fail when
HUBZOID_BACKUPSis notweekly,dailyoroff.
See 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, for example because of an invalid workflows/settings.yaml. 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.
access.who
Who may use this agent, from the grants in Settings. Access comes only from those grants.
- Warn when nobody may use it. Grant Use this agent in Settings (Agents, then the agent's Access), or sign in once as the configured owner.
- Info when only the owner may use it so far. Add people in Settings.
- Ok with the number of people who may use it, or when an older grant still opens it to everyone signed in.
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.
settings.files, settings.values and probes
The Hub's own declarations, only when it has them. settings.files fails when settings.yaml, hub.settings.yaml or hub.manifest.yaml does not parse. settings.values fails when a declared setting has no valid value or reference, and lists each problem. Each check the Hub declares in hub.manifest.yaml reports as probe.<id> with its own status and recovery. hubzoid hub doctor runs these alone. See Hub files.
secrets.store, secrets.missing, secrets.blank and secrets.overrides
Names and status only, never a value.
secrets.storewarns when a stored secret cannot be used, because the deployment key or its destination changed (set it again withhubzoid secrets set), or when an operator source such as.envalso sets it, so the stored one is unused.secrets.missingwarns for each secret the Hub expects that nothing sets, with thehubzoid secrets setcommand to fix it.secrets.blankwarns for a line such asOPENAI_API_KEY=. Give it a value or remove the line.secrets.overrideswarns when the Hub's files set a different value than the deployment. Keep shared values in one place.
Use it in operations
- Run
hubzoid doctorafter installing, after every upgrade and afterhubzoid restore. - With several Hubs, run it for each Hub. The operational store and backup checks read the store they all share.
- Wire
--jsoninto monitoring and alert onok: false, or on specific ids such asscheduler.health,backup.ageanddeployment.key.

