❯ 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, whose failure modes stay invisible until a client notices. None of it is hard. All of it is unpredictable, which is worse — and 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 145 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 — and 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 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.
Nothing an agent changes reaches production without passing through a diff you read.
The way up is cwp bricks pull, then a commit, then cwp deploy. An upward write with no committed source in the repository is refused — not warned about.
This is the sentence the whole tool exists to make true, and it is the one that 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.
46 of the 175 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 is permitted to be read-only; it is not permitted to 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: a remote is already covered by the provider backup. The local database is the only copy of design work, which is exactly why 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, and each name is asserted by a test that the build requires 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 answer “what would happen?”. This one also answers “would you let me?” — which is the question you needed answered first.
enforced byREFUSAL_SURVIVES_DRY_RUN
Data flows down. Code flows up. Nothing moves 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 a corrupt remote file is reported as up to date forever and re-running the push 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 how a reproducibility principle quietly stops being used.
The daily part
The rest of it is just 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.
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 bricks 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 — which is what the tutorial does, and what cwp pull exists to produce — 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 deliberately unguarded, because 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 it has never been built.