Skip to content

The safety model

shipped 1.0.0

cwp exists to make one class of mistake impossible: pushing the wrong thing in the wrong direction to production. Six rules encode that, and they are not configurable away.

  1. Bulk data flows down, code flows up. See Asymmetric data flow for what “bulk” is doing there.

  2. Protected environments refuse upward writes. An environment marked mode: protected rejects every upward write unless you pass --force. The refusal reads the effects rather than the command’s name. That keeps the list complete rather than remembered.

  3. There is no bulk database push. The design exists; the build waits. If it is ever built it will require the environment name typed back as confirmation. That is a condition on a future command, not a description of a current one, and cwp db is local only.

  4. cwp takes a remote restore point before any upward write, unless you disable it explicitly. On a host that cannot take one, disabling it makes cwp refuse the write rather than leave it quietly unprotected.

  5. cwp takes a local snapshot before any downward write. cwp pull replaces your local database, and on a builder site that database is the only copy of your design work. So the pull snapshots first, as pre-pull-<env>-<timestamp>, and a snapshot that fails aborts the pull rather than warning. A safety net you believe in and do not have is worse than none. This is rule 4’s mirror image: the remote has its host’s backups, the local side has this.

  6. Every pull that imports a database installs the mail guard, even when you switch scrubbing off. A pulled production database holds real customer addresses, and WordPress will mail them if something asks; the guard makes sure nothing leaves the machine.

    The rule names the reason rather than a command, so a fourth command that brings a database down inherits it by being what it is. cwp pull reads the tree and writes to no site, so there is nothing for the guard to guard. cwp db pull and cwp media pull are where a database lands, and they install it. The guard is unconditional. A builder stack that ships its own SMTP would otherwise bypass the container’s mail catcher entirely. The guard overrides that SMTP configuration rather than sitting beside it.

    Where a message ends up depends on what answers locally. DDEV runs Mailpit in the web container. When its SMTP port answers on loopback, the guard diverts the message there. You read it in full with ddev launch -m. It goes to one unroutable address, and the real recipients stay in X-CWP-Original-* headers. When nothing local answers, the guard blocks the message and logs it. Blocking is the fallback of every failure path. The guard hands a message to a transport only after proving that transport is on loopback.

    The one thing it cannot do. A safety model that hides its edge is worse than one that has none, so here it is. wp_mail() is a pluggable function. A plugin that declares its own replaces the implementation the guard’s hooks live in. No mu-plugin can prevent that. The guard checks on plugins_loaded and says so: a line in the log, a notice in wp-admin, and a failing cwp doctor check naming the plugin to deactivate.

And two that are not refusals but belong beside them. No credential enters the tree, and no flag opens it. A settings allowlist refuses a key that matches *_key, *_secret, *_token or *_password and names the pattern. The builder’s own settings travel by a rule per key. A credential, a code-execution toggle or a per-environment flag stays behind, named on every pull (cwp settings, cwp bricks pull). A secret in the tree is a secret in the history, permanently. That rules out a flag for either refusal.

Never raw SQL for URL replacement. WordPress stores serialised data and a page builder stores element trees as serialised JSON in postmeta, so a sed over a dump corrupts every array whose string lengths it changes. It is always wp search-replace.

The guards run in a dry run

--dry-run prints the plan and changes nothing, and it does not always exit 0. The refusals run; the confirmations do not. So cwp push prod --dry-run on a protected environment still exits 5, rather than printing a plan for a write it would never permit.

A dry run that stopped refusing would describe a different command from the one you are about to type.

The rules have tests, and the tests have names

Twelve invariants sit as data in the source, each naming the test that asserts it. A structural test fails the build when a named test disappears or changes its name. They are not documentation of intent; they are the tests, indexed by the rule they defend.

The safety page renders the catalogue, the guard ladder and the exit codes; Testing argues that the tests are the product rather than a chore.