Doctor Command¶
The doctor command runs diagnostic checks to validate configuration, connectivity, and the runtime environment. It is enabled by default via the DoctorCmd feature flag.
doctor is the health verdict (pass/warn/fail/skip). Its report subcommand adds the state dump (resolved config (redacted), paths, versions, and feature flags) around that verdict.
Usage¶
Description¶
Runs a series of built-in and feature-registered health checks, then reports the results. Each check returns one of four statuses:
| Status | Meaning |
|---|---|
pass |
Check passed successfully |
warn |
Non-critical issue detected |
fail |
Critical issue that needs attention |
skip |
Check could not run (e.g., missing config) |
Built-in Checks¶
| Check | What it validates |
|---|---|
| Go version | Runtime Go version is 1.22+ |
| Configuration | A config file was found and loaded, naming which. Before init has written one the tool runs on its embedded defaults and the check reports skip rather than refusing to run, because a missing file is one of the things doctor is for. A config file left in the tool's previous format, with none in its current one, is a fail naming both files and the config convert that fixes it |
| Git | git binary is available and the current directory is a repository |
| Chat providers | With the ai feature enabled: ai.provider and every ai.fallback.providers member is a provider this binary registers. A failure names the module to blank-import, the way Forge adapters does; no ai.provider at all warns and lists what the binary links. Without ai the linked providers are the tool's own wiring: the check names them and judges nothing (a skip when none is linked). Replaced the old API keys count, which the credential resolution check had made redundant |
| Credential storage | No secrets (AI keys, VCS tokens, Bitbucket app password) are stored as literal plaintext in config: warns and lists the offending key names (never values), pointing to env-var migration |
| Release source | The tool can build its updater. On a forge, the release source type has a registered provider (a failure names the module to blank-import). On the static channel (spec 0203) the check reads the pointer at the base URL: a pass names the current tag and how many downloads it lists; nothing published yet is a warning naming the pointer URL; a pointer whose manifest does not resolve fails with both URLs. Skipped when self-update is disabled or no release source is set |
| Forge adapters | Every enabled forge feature has its adapter module linked into the binary. Fixed at build time, so a failure names the blank import to add rather than a config key to set. Skipped when no forge feature is enabled |
| Credential resolution | Whether each declared credential actually resolves, and from which rung: auth.env/api.env, the keychain, the literal, or the well-known fallback variable. Only credentials of enabled features are reported, and a chat credential only when one of the providers it serves is linked in this binary (a claude-local-only tool hears nothing about an Anthropic key). Reports the key name only, never the value |
| Permissions | Config directory has correct owner permissions (rwx). A directory that has not been created yet is a skip, not a warning |
Output Example¶
mytool v1.2.3
[OK] Go version: go1.26.0
[OK] Configuration: loaded from /home/user/.config/mytool/config.yaml
[OK] Git: repository accessible
[OK] Chat providers: claude, claude-local linked
[OK] Permissions: config dir: /home/user/.config/mytool (drwxr-xr-x)
[OK] GitHub credential: resolves from auth.env
[!!] GitLab credential: credential configured but does not resolve
malformed keychain reference "no-slash-here": want "service/account". …
[SKIP] Gitea credential: no credential configured
Run `init gitea` to configure one, or set GITEA_TOKEN.
On a fresh install, before init has run, the first two checks read:
[SKIP] Configuration: no config file yet
Running on embedded defaults; `mytool init` creates one.
[SKIP] Permissions: config directory not created yet: /home/user/.config/mytool
JSON output (--output json) returns a DoctorReport struct with the tool name, version, and an array of check results.
Why the fallback rung rarely appears
Each forge's embedded config bundle ships <forge>.auth.env: <FORGE>_TOKEN as a
default. Rung 1 therefore already reads the same variable the well-known fallback
rung would read, so a credential supplied purely as GITHUB_TOKEN reports as
resolves from auth.env rather than from the fallback. Same variable, same value,
same outcome, but worth knowing before reading a report and concluding the
fallback is broken. To reach the fallback rung, a tool must ship a bundle that
sets no auth.env default.
Extensibility¶
Features can register additional checks via the middleware system. When a feature is enabled, its registered check providers are automatically discovered and included in the report:
func init() {
setup.RegisterChecks(props.MyFeature, func(p *props.Props) []doctor.CheckFunc {
return []doctor.CheckFunc{myCustomCheck}
})
}
doctor report: support bundle¶
doctor report emits a single, secret-redacted, paste-ready support bundle a user can drop straight into a GitLab/GitHub issue. It wraps the health verdict above with a state dump, so a maintainer gets what version, what config, what paths, what flags, and what doctor says in one block.
It is available wherever doctor is (gated by DoctorCmd, default-on). There is no separate feature flag.
What it collects¶
| Section | Contents |
|---|---|
| Tool | Name, summary, version, commit, build date |
| Runtime | Go version, host OS string (pkg/osinfo), arch |
| Paths | Resolved config directory and config file in use: the highest-precedence loaded file layer in the store, the same answer Viper's ConfigFileUsed() gave (no cache dir, GTB has none) |
| Features | Every built-in feature flag, enabled/disabled |
| Config | The effective merged configuration, redacted |
| Doctor | The full report above, reused verbatim |
Safe by default: redaction¶
The entire bundle passes through go/redact before it is written, in both text and JSON. There is no flag to disable redaction: for a raw value, read the specific config key directly. Two layers protect the config:
- Credential-shaped keys are dropped to
<redacted>regardless of value: keys ending in.api.key,.auth.value,.app_password,.password,.secret,.token, or whose final segment is a known credential word. Even a malformed value cannot leak. - Every other value is scrubbed through
redact.String(best-effort): URL userinfo, well-known token prefixes (sk-,ghp_,glpat-,AKIA…), and long opaque tokens. Map keys are preserved so the structure stays legible.
The process environment is never enumerated (high leak surface, low triage value); env-derived values still appear via the resolved config snapshot (Snapshot().Values()) under the store's precedence. See go/redact for the full pattern set.
Implementation¶
The doctor command is implemented in pkg/cmd/doctor/doctor.go with built-in checks in pkg/cmd/doctor/checks.go; the report subcommand (collector + redaction) lives in pkg/cmd/doctor/report.go and report_redact.go. The check registry lives in pkg/setup/; the shared OS-version string comes from pkg/osinfo.
See the spec: 0092-bug-report-diagnostics-command.