❯ Why

An agent can build the site. It cannot quietly ship it.

Moving a WordPress site between environments is normally an unpredictable flow: half-remembered commands, run in whichever terminal tab is focused, with failure modes that stay invisible until a client notices. None of it is hard. All of it is unpredictable, which is worse. It gets worse again when the changes stop being typed by hand.

The gate

An agent can build the site. It cannot quietly ship it.

Bricks 2.4 gave a WordPress site a hundred and fifty-odd abilities and a native MCP server, so an agent can now do the building. What it cannot do on its own is leave a record. A change nobody can see, review or undo is the failure mode that matters once the typing stops being manual.

An agent works on a copy, not on your site.

cwp db pull dev brings production down to a local DDEV site, scrubs it and installs the mail guard. That copy is what the editor and the MCP connection point at.

Production is not in the loop while the work happens. It becomes involved once, at the end, through a command you run.

No agent change reaches production without a diff you read.

The way up is cwp pull, then a commit, then cwp deploy. An upward write with no committed source in the repository is refused — not warned about.

The whole tool exists to make this sentence true. It matters most once something other than a person is doing the typing.

enforced byUPWARD_HAS_A_SOURCE

The guards fire when nobody is watching.

Refusals are not interactive. --yes means “do not ask me to confirm”, never “choose for me”, and every command carries a real exit code and a --json envelope.

A guard that only works when a human is at the keyboard is not a guard for agent work.

An ability that declares nothing is treated as a write.

Roughly a quarter of the abilities reachable on a real Bricks site declare neither read nor write. cwp reads that silence as the more dangerous of the two.

null stays distinct from false throughout. An ability may declare itself read-only. It may not be vague about it.

A destructive design-system call takes a snapshot first.

Global classes are not revisioned, so a delete is immediate and final. cwp exports the design system before the call, and an unrecognised category snapshots too.

Local only, and worth saying plainly: the provider backup already covers a remote. The local database is the only copy of design work, so local is where this matters.

Even an unplanned change leaves a record.

--adhoc writes the payload to cwp/adhoc/ and states in as many words that the change is not reproducible. On a protected environment it is refused outright.

The guards

Refusals you cannot configure away.

Each of these is a rule with a name. A test asserts each name, and the build requires that test to exist.

The safe command tells the truth about the dangerous one.

cwp push prod --dry-run does not print a plan for a write it would never permit. It refuses, with the exit code it would really use.

Most dry runs say what would happen. This one also says whether cwp would allow it. You needed that answer first.

enforced byREFUSAL_SURVIVES_DRY_RUN

Data flows down, code up, never sideways by accident.

Direction is a property of each individual effect, never of the command name — so a command called pull that writes one thing upward is still guarded on that one thing.

enforced byWRITE_SCOPE_IS_PER_EFFECT

Your local site cannot email a customer.

Every pull installs the mail guard, on every terminating path, even with scrubbing switched off — and it is asserted against the filesystem rather than against having called the installer.

The difference matters: no scope, setting or fast path can route around a check that looks at the result instead of at the intention.

enforced byMAIL_GUARD_ALWAYS

“No backup” is never a silent state.

A host that cannot take a restore point refuses the upward write rather than skipping the step.

enforced byBACKUP_CAPABILITY_REFUSES

A failed push stops instead of carrying on.

Each transfer is verified by digest as it lands, and a failure aborts the rest. A three-path push whose second path fails never moves the third.

The reason is unpleasantly specific: cloudron sync compares size and modification time, so it reports a corrupt remote file as up to date forever, and running the push again does not repair it.

enforced byVERIFY_BEFORE_NEXT

Your page builder still works after a migration.

URL replacement always goes through wp search-replace, never raw SQL. WordPress stores serialized data, and Bricks stores whole element trees as serialized JSON in postmeta.

enforced byNEVER_RAW_SQL

A refused push has no side effects at all.

No backup taken, nothing transferred, and your pre-push hook is not run either. Refuse is ordered before Consent so that this is structural rather than a matter of discipline.

enforced byREFUSAL_HAS_NO_EFFECTS

The record

A deployment tool tells you what happened. This one leaves the record.

What changed three weeks ago is in your git log.

Every upward change originates in a committed file, so it is reviewable before it happens and findable afterwards.

A deployment tool tells you what happened. This one leaves the record.

A pull that changes nothing writes nothing.

Sorted keys, URLs as placeholders, volatile fields stripped — so the same page pulled from two environments produces byte-identical files, and unchanged files keep their modification time.

A noisy diff is where people stop reading, and a record nobody reads is not a record.

The daily part

The rest of it is less annoying.

Full media locally, without downloading it.

Missing uploads fall through to the remote, with the Cookie header stripped on the way up and Set-Cookie dropped on the way back.

It fetches files. It does not share a session.

Production data locally, without personal data locally.

The scrub pipeline rewrites wp_users and the tables around it before you ever open the site.

It reimplements nothing.

DDEV, WP-CLI and your host do the transport. cwp decides what may move, in which direction, and what has to be true before and after.

When something breaks you are debugging tools you already know, not a bespoke sync protocol you have to trust.

Same command, sharper plan.

Switch the host to ssh and cwp push --dry-run gains a real file-level diff, because the capability model lets a host be more capable without a branch appearing in the command.

A survey is a table, and the row you need is not buried.

Uniform rows render as aligned columns, sorted; the state that asks for nothing collapses to one line; and each of the three sides — tree, local, remote — keeps the same colour everywhere, with a protected environment in the warn colour.

Fifteen pages in sync collapse to one line, and the page that differs stands on its own.

The honesty section

What cwp does not stand between.

cwp gates the movement between environments. It does not sit between an agent and WordPress: once cwp mcp has wired the connection, the agent talks to the site's own MCP server directly, and nothing on this page is in that path.

What bounds the agent is which site its credentials point at. Wire it to the local copy, the one the tutorial sets up and cwp pull produces, and the only route to production is the one described above: a committed file, a diff you read, a command you run. Wire it to production instead and you have removed the gate yourself.

The same discipline applies to two more. cwp wp and its neighbours are unguarded by design: cwp cannot classify WP-CLI subcommands, and a reflexive --force on wp plugin list is worse than no guard at all. And cwp db push does not exist. It was designed, then deferred, and never built.