Skip to content

cwp doctor

shipped 1.0.0
cwp doctor [flags]

Acts on the environment the project makes obvious: the only one it defines, or the one called dev. Name another to act there. A project with several and no dev has to name one.

FlagWhat it doesDefault
--env <env>environment to diagnose remotely
--with-agentwith —dry-run on a host with no shell: install and remove the PHP agent so the plan is realoff

Plus the shared flags --json, -v, --verbose, -q, --quiet and --dry-run.

What it does

Read-only diagnostics. Each check reports ok, warn or fail, and every failure prints a copy-pasteable fix. It exits non-zero if anything failed.

Three checks weigh by the environment’s stage. The sandbox captcha key, the mail guard on the environment and the scrub’s placeholder accounts warn while you build the site. They fail once the environment is in launch or live. The same three stop a push there.

It also reads the reasons beside the lines of cwp.yml and names every one whose until is spent, and every until it cannot read (the reason beside a line).

The first check names which cwp is answering. It prints the release number, the commit the bundle came from and the build time:

✓ cwp build — 1.0.0 (49fa29e, built 2026-08-17 19:17)

A published install has one identity and prints it. An installation linked from a working copy has two, and they can disagree: the bundle runs, and you read the source beside it. When the source is newer, the line warns and names npm run build. It warns rather than fails, because bisecting means running an older bundle on purpose. -dirty in the commit means the build came from a tree with uncommitted changes and corresponds to no commit at all.

It checks the Node version, the Docker runtime, DDEV, mkcert, the host CLI and its login, and both config files. It checks that the remote app is reachable and its wp responds. It checks PHP parity between local and remote, and WordPress parity between them. It checks that the configured cloudron_package matches the probed remote wp-content path, that WordPress core is present in the docroot, and whether DDEV is running. It checks whether outbound mail is still intercepted, and, on pull.uploads: proxy, whether the uploads fallback serves a file.

It also names the captcha keys on both sides, in the builder’s own vocabulary. The local site should hold the vendor’s sandbox pair, which every cwp db pull writes. An environment should not:

✓ Captcha keys (local) — turnstile sandbox keys — every submission passes locally
! Captcha keys (dev) — turnstile site key on dev is the sandbox key, and every submission passes
  → cwp edge captcha dev writes the zone's widget into the builder
! Captcha keys (local) — turnstile site key on local is not the sandbox pair; the widget refuses the DDEV host
  → cwp db pull sets the sandbox keys

An empty slot says nothing: whether a form needs the captcha is editorial.

It also reports the project layout, as warnings rather than failures. A .gitignore that does not cover cwp/tmp. A hooks_dir pointing at a directory that is gone. A cwp/ directory sitting inside the document root, where a web server would serve your hook scripts.

The first is the sharp one: cwp db pull writes the production database dump into cwp/tmp, so a .gitignore that names some other directory leaves a copy of the live database where git offers to commit it.

It also refuses a configuration that cannot be right. A taxonomy declared in content: and listed in pull.truncate_taxonomies would reach the tree and then lose its terms in the same command. That is two features fighting, not a preference to resolve. The check fails with the one-line fix.

An access: entry naming an environment whose mode is source is the same shape: an availability the project wants converged onto a site the same file declares read-only. cwp access refuses that run anyway. The finding saves you making the run to hear it.

WordPress parity warns; PHP parity fails. A PHP mismatch means the local copy can run code the remote cannot. A core mismatch is the ordinary result of a managed host applying its own updates on its own schedule while DDEV applies none. It is usually hours old, and one command resolves it:

cwp wp -- core update && cwp wp -- language core update

That command is local. There is no upward equivalent on purpose. A core update is the platform, and the platform belongs to the host.

The uploads-fallback check is a live request, not an inventory. It asks for one uploads path the database knows and the filesystem does not, and expects a 200. Under proxy almost every file is missing locally by design, so “there are attachments without files” says nothing. An installed fallback that does not work looks like one that does.

The mail check asks the running site, not the disk. Whether the guard sits on disk is a question about a file. Whether it can still see a message is a question about wp_mail(). WordPress declares that as a pluggable function. A plugin that declares its own wp_mail() replaces the implementation both of the guard’s hooks live in. The guard stays loaded and stops seeing anything. So doctor asks the guard itself what it would do with a message right now, and reports where a diverted one goes:

✓ Outbound mail is intercepted — diverted to 127.0.0.1:1025 — read it with `ddev launch -m`

When something has taken wp_mail() away from WordPress, the check fails and names the plugin to deactivate. No mu-plugin can prevent that. This check stops it from being silent.

The tree never holds WordPress core, so a freshly cloned or freshly scaffolded project has none. doctor flags that before cwp pull runs into it: the import would succeed and then wp search-replace would fail with no installation to load. The fix is ddev wp core download.

With a builder configured

On builder.id: bricks it also checks, on both sides: Bricks ≥ 2.4 and WordPress ≥ 6.9. Whether the Bricks AI surface is on. Whether “coming soon” mode is on. That an ability user resolves and the abilities answer. That every custom font the design system uses still renders. Then the MCP exposure of the remote, the adapter plugin, and the custom post types. For the post types it names which exist on only one side, and which WordPress registers but the builder will not open.

Coming-soon mode is a warning and never a failure. A site under construction should stay closed, and cwp has no opinion about that. What it can do is make sure the state is never a surprise:

! Bricks coming-soon mode (dev) — on — visitors get the maintenance template, not the page
  → switch it off under Bricks → Settings → Maintenance, or ignore this if the site is meant
    to be closed. Administrators bypass it, so it looks fine when you are logged in

The last line of that finding is why the check exists. Administrators bypass the mode, so the person best placed to notice never sees it.

On builder: none none of that runs, and doctor prints no screen of skips. The builder contributes those checks itself. doctor asks whichever builder the project configured and splices in what comes back. It holds no Bricks-specific knowledge of its own.

These are separate checks on purpose. Bricks registers no abilities when its AI surface is off, not even the ones its documentation calls always-on. So “the abilities are missing” has four causes with four different fixes: WordPress too old, Bricks too old, the surface switched off, or BRICKS_DISABLE_MCP set. doctor reads the versions and settings directly rather than inferring them from a failed call, so it names the one that applies. It also tells an AI tab that was switched off from one nobody has ever saved. The screen is the same and the sentence is not.

BRICKS_DISABLE_MCP reports as ok: pinning the AI surface off per environment is what it is for.

Custom fonts

It also asks whether the typography renders: every custom font your theme styles and global classes name must exist on that environment, have a font file, and have an @font-face rule.

Those are three different failures with three different fixes, and the check tells them apart:

✗ Bricks custom fonts (dev) — "Example Sans" (custom_font_170) is used here and has no font file
✗ Bricks custom fonts (dev) — "Example Sans" (custom_font_170) has a font file and no @font-face rule
✗ Bricks custom fonts (dev) — custom_font_170 is used here and no such custom font exists on this site

The middle one looks fine everywhere else. The frontend renders @font-face from an option that Bricks rewrites only in wp-admin or on import, so a font whose database record was repaired by hand still renders nothing.

The check skips a plain family name. font-family: Barlow may be a Google font, a websafe family, or a face your theme’s stylesheet declares. None of those is visible to cwp, so it does not guess. It checks what it can know: a custom font is referenced by ID, and an ID either resolves or it does not.

The workspace, not only the site

Everything above is about an environment, and all of it can be true on a machine with no repository at all. Two more checks ask whether this checkout is fit to design in. Both are local-only: there is no tree on a remote to compare.

! Bricks design system in the repository — 3 item(s) differ from the local site: +classes/example-hero, -templates/Old header — fix: cwp pull --only bricks, then git diff bricks/
✓ Bricks design-system files — 4 binary file(s), all present

The first compares the committed bricks/ against what the local site holds. Bricks keeps no revisions of classes, variables, theme styles, components or the palette. The tree is the only way back from a deletion, and a tree that lags the database looks like a net without being one.

It matches on names, as Bricks’ own importer does, so it survives the canonical IDs the tree carries. That also fixes its boundary: an item that changed in place is invisible to it. A class whose padding moved has the same name on both sides, and this check stays green. The exact answer is a pull and a diff. The fix hint says so even when the check passes.

The second verifies that every binary a font file names is in the tree. Bricks reads a font with no faces as an instruction to delete it. So the check fails rather than warns: a font whose bytes never reached the tree is a deletion waiting for the next cwp push --only bricks --replace.

Example

cwp doctor
# ✓ Node 22.x
# ✗ Cloudron logged in — run: cloudron login my.example.com

What it does not do

It changes nothing. It only reports, so it accepts --dry-run and ignores it.

It does not fix what it finds. Every failure carries the command that would, and typing it is yours.