/claude-code-doctor
What the plugin currently thinks is happening, and the redacted bundle to attach to a bug report.
/claude-code-doctorPrints, in the chat, what the plugin currently thinks is happening. The plugin answers it itself: no model is called, nothing is billed, and the reply reports 0 tokens. It is the thing to paste into a bug report.
It carries the startup-diagnostics fields (plugin version, opencode version, claude path and version, the working directory and which resolution tier picked it, providers, accounts, proxyTools, the on-disk MCP servers, the permissionPreset in force per provider, transport, whether an ANTHROPIC_API_KEY is present) plus the live runtime state the startup block cannot know:
- every live
claudechild, by opencode session id and model, with its pid, whether a turn is in flight, how long it has been up, and the effort it was spawned at, - every pending proxy call, with the tool, the call id, how long it has waited, and its deadline,
- each proxy server’s URL with one unauthenticated
initializeposted to it:401, goodis the patched behaviour, and anything else is flagged unsafe with the fix (restart every opencode window, since a window opened before 0.13.2 keeps serving an open port). See Proxy endpoint security.
When Claude Code refused an entry in an --mcp-config it was handed, an MCP config entries Claude Code skipped section names each one with the CLI’s own category and sentence. That section only appears when there is something in it. It matters because a skipped server is absent from the CLI’s server list entirely rather than listed as broken, so the model silently does not have those tools; if the skipped name is opencode_proxy the report says so plainly, because then it is the plugin’s own server and every proxied tool call in the session fails. The same thing is a warning in your terminal when it happens.
A Plugins Claude Code did not load section works the same way for Claude plugins: one the CLI demoted at load time (for example, a dependency that is not installed) is absent from its plugin list, so its skills, commands and MCP servers are silently missing. The skill bridge is such a plugin (opencode-skills), and a failure there is reported as the plugin’s own bug rather than your config. A plugin warning only counts when its content did not load; advisory feedback about a plugin that did load stays in the log at INFO.
A Hooks Claude Code ran that failed section covers your own Claude Code hooks, which are the third thing that fails without leaving a trace. Claude Code runs a SessionStart hook on every claude it starts for you, and when one exits non-zero it discards the hook’s contribution and answers the turn normally: the context that hook was supposed to add is simply missing, on every turn of that session, with nothing on screen. The section names the hook, the event, its exit code and its outcome, and it is a warning in your terminal the first time it happens. Only the hook’s stderr is shown, capped: a hook’s stdout is what Claude Code splices into the model’s context, so it has no business in a bug report. These are your hooks in your Claude Code settings, not opencode’s, and the plugin never passes --include-hook-events, so only the SessionStart family is ever reported.
/claude-code-doctor usageadds a Plan usage section: the CLI’s own answer to /cost, which is the subscription-or-API-key line, how much of the 5-hour and 7-day windows is used, when each resets, and what has been contributing to them. It is measured free (num_turns: 0, $0, no API call: the CLI answers it locally), so it costs no tokens and nothing is billed. It is opt-in anyway because reading it starts a short-lived claude process, which runs your SessionStart hooks and takes a few seconds. Without the argument the section says so and the report stays instant. A CLI that cannot answer leaves one line saying why and the rest of the report is unaffected.
The permissionPreset row reads provider: preset for every registered provider, none where none is set, so two accounts configured with different postures are not collapsed into one answer. When a preset is in force, a Permission preset overrides block under the table lists the options it replaced, in the same words the log uses. A name the plugin does not recognise is reported as readonly (unknown, nothing applied) rather than shown as if it took effect: a typo’d safety option runs at full permissions, and the report is where you find that out. See Read-only mode.
Nothing secret goes in it: not the proxy bearer token, not the value of ANTHROPIC_API_KEY, not the system prompt, not a pending call’s arguments. A claude-code-doctor command you defined yourself is never overwritten. The name has no space in it because opencode reads everything after the first space as the command’s arguments. The whole exchange is kept out of any transcript replayed to the CLI, like a /btw pair.
Filing an issue: /claude-code-doctor bundle
/claude-code-doctor bundleWhen filing an issue, paste /claude-code-doctor bundle. It returns the report above plus the recent NOTICE, WARN and ERROR lines from this process’s plugin log, redacted so the whole thing is safe to put in a public issue. It starts no process and costs no tokens, so unlike usage it stays instant.
The point is plugin.log itself. It is off by default, and when it is on it has no redaction guarantee at all: it holds spawn argv with --settings JSON and absolute paths, the bridged MCP config target, your skill directories, opencode and Claude session ids, and error prose the CLI wrote. Nobody can safely attach it to a GitHub issue, so bug reports arrive as screenshots and guesses instead.
The redaction is an allowlist, not a filter, because a filter fails silently the first time someone logs a new field. Per line, what survives is:
- the timestamp and the level,
- the message text only when it is one of the 112
NOTICE/WARN/ERRORmessage literals extracted from the plugin’s own source. A message built at runtime, including every CLI error string the plugin re-logs, becomes[redacted message, N chars]and only its data fields remain, - data fields whose key is on an explicit allowlist and whose value is then the kind that entry declares: versions, counts, booleans, enums, durations, exit codes, model and tool and server names, paths, and the loopback proxy URL with its query dropped. The allowlist applies at every nesting depth.
Everything else, including every key the allowlist does not name, becomes [redacted, N chars], which keeps the shape so you can see a field was there without seeing it. Session ids become a short hash salted per bundle, so two lines about one conversation still correlate in the paste and nowhere else, and your home directory becomes ~ across the whole report, the table included.
Never in a bundle: prompt or reply text, system prompts or the appended prompt file, tool inputs or outputs, file contents, environment values, bearer tokens, the proxy authToken, API keys, Authorization headers, MCP server env or headers, URL credentials or query strings, or the raw spawn argv. The argv is kept as option names with every value replaced, which is what a spawn bug report actually needs.
Kept on purpose, so read it before pasting: folder paths below your home directory (project and config folder names such as ~/.claude-<account>) and your accounts names. A maintainer needs both to read a cwd or an account problem, and only you can tell whether a folder or account name is something you would rather not publish.
It is capped at 120 lines and 24,000 bytes, newest first, and says how many lines it left out. With file logging off it says so, tells you how to turn it on, and still returns the report:
OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencodeThe plain /claude-code-doctor output is unchanged by any of this.
