Safety

Refusals you cannot configure away.

A stated direction, a refusal that runs even in a dry run, a restore point taken before the write and a verification taken after it. None of it is a setting.

Data flows down. Code flows up.

Nothing moves sideways by accident — and the rule that makes that true is finer than it looks. Direction is a property of each effect, never of the command that produced it, so a command called pull that writes one thing upward is guarded on that one thing and nothing else.

  1. local
  2. dev
  3. prodprotected
  • databasedev → localno guard — nothing upstream changes
  • uploadsdev → localno guard — nothing upstream changes

Data comes down. Nothing is guarded, because nothing upstream changes.

The ladder

Every upward write passes the same rungs in the same order, and the order is the guarantee. Refuse comes before Consent, so “a refused push has no side effects” is a property of the sequence rather than of anyone remembering it — no hook runs, no backup is taken, nothing is transferred.

  1. DirectionRefuseexit 5

    An effect that writes to a remote without the guard ledger recording that the remote-write set passed for that target.

  2. Protected environmentRefuseexit 5

    Any upward write to an environment marked protected, unless --force is given.

  3. No source in the repositoryRefuseexit 5

    An upward effect that cannot name the committed file it came from. --from names one; --adhoc records the payload instead and is refused outright on a protected environment.

  4. Escaping or empty trackedRefuseexit 5

    A path that resolves outside the tracked list, and a tracked list with nothing in it.

  5. A host that cannot back upRefuseexit 5

    The write itself, rather than the backup step — a capability that cannot be honoured refuses instead of degrading.

  6. ConfirmationConsentexit 6

    Proceeding without an answer. Where the blast radius is named items the prompt enumerates them, because the text is generated from the plan.

  7. Restore pointConsent

    Nothing — this is where the remote backup and the local snapshot are taken, once every refusal above has passed.

  8. Verification, per effectApplyexit 1

    The rest of the fold. Each transfer is verified by digest as it lands, and a failure stops the run before the next path moves.

Everything marked Refuse runs under --dry-run as well. Everything marked Consent does not — there is nothing to consent to, and nothing has happened yet that would need a restore point.

12 rules. 12 tests.

Anyone can publish a list of safety rules. The third column is the part that matters: each rule names the test that asserts it, and a test in the catalogue asserts that every one of those tests exists and passes. Delete the enforcement and the build says which rule just lost its cover.

The 12 safety invariants, the rule each states, and the test that asserts it.
InvariantWhat it saysAsserted by
PROTECTED_NEEDS_FORCESPEC §5.4a protected environment refuses every upward write without --forcepolicy: a protected environment refuses an upward write without --force
REFUSAL_SURVIVES_DRY_RUNSPEC §5.4 / §19.1the refusal runs under --dry-run too; the consent does notpolicy: a protected environment refuses under --dry-run as well
BACKUP_BEFORE_UPWARDSPEC §5.4no remote write proceeds without a restore point, or an explicit waiverpolicy: an upward write needs a restore point or an explicit waiver
BACKUP_CAPABILITY_REFUSES§9.2a provider that cannot back up refuses the upward write rather than skipping itpolicy: a provider that cannot back up refuses rather than skipping
UPWARD_HAS_A_SOURCEP2 / F-043an upward effect with no repo source is refusedpolicy: an upward effect with no repo source is refused
WRITE_SCOPE_IS_PER_EFFECT§13 / §19.3direction lives on the effect, never on the commandpolicy: direction is read from the effect, not the command
TRACKED_NEVER_OVERWRITTENSPEC §5.8a downward fetch never overwrites a path listed in tracked — that is your source, not the remote'spolicy: a fetch never overwrites a tracked path
NEVER_RAW_SQLCLAUDE.md rule 6URL replacement goes through wp search-replace, never raw SQLpolicy: a URL rewrite must use search-replace, never raw SQL
SNAPSHOT_ABORTS_PULLSPEC §5.3 / §19.5a failed pre-pull snapshot aborts the pull — the one step of seven that is not best-effortops/pull: a failed pre-pull snapshot aborts the pull
MAIL_GUARD_ALWAYSCLAUDE.md rule 5 / §19.4every pull installs the mail guard, on every terminating path, even with scrubbing disabledops/pull: the mail guard is installed on every terminating path
REFUSAL_HAS_NO_EFFECTSSPEC §5.4a refused push runs no hook, takes no backup and transfers nothingops/push: a refused push has no side effects at all
VERIFY_BEFORE_NEXTSPEC §5.4 / §19.2each transfer is verified as it lands; a failed digest stops the foldops/push: a failed digest stops the fold before the next transfer

The build fails if any named test disappears. These are not documentation of intent — they are the tests, indexed by the rule they defend.

Exit codes

Stable across releases, and the reason a script or an agent can tell “this was refused” from “this went wrong”.

0
success
1
generic error
2
configuration error
3
missing dependency
4
remote error
5
refused by a safety guard
6
aborted by user

cwp wp and the other conduits forward WP-CLI's own exit code verbatim, even where it collides with this table. A pass-through that rewrites exit codes is not a pass-through.

What is deliberately missing

cwp db push does not exist. It was designed, then deferred, and it has never been built — so there is no command that moves a database upward, with or without a flag. If that changes it will appear on the roadmap before it appears in the tool.

cwp wp, cwp shell and cwp logs 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: it teaches you to pass the flag without reading. They announce their target instead.