Security model

Nothing writes to your site until the exact change is approved.

Every mutating call previews itself first, binds the approval to that exact payload, and refuses to run if either one changes before execution.

Where "we have an approval step" still breaks

Dry-run previews are common now. Exact-match approval still isn't.

Most AI tools ask permission somehow these days. Permission alone doesn't cover these three.

01

The agent itself

Misreads a value, targets the wrong post ID, or takes a bigger action than you asked for. Nothing separate checks the write before it lands on production.

02

The person prompting it

The person typing the prompt is usually also the person who'd approve it. Same set of eyes, twice.

03

A compromised session

A leaked token or hijacked session inherits everything the agent could already do. Nothing else stands in the way.

The architecture behind the guard

Named verbs are the front door. Raw code execution is a gated side exit.

Three tiers, ordered by how much damage a single call can do.

🔑

Scoped tokens

Every bearer token carries one scope — recon, mutate, or admin — each a strict superset of the one below. Call above your scope and you get a clean 403, never a partial write.

Named verbs

The agent's only normal vocabulary is a closed set of typed actions. Update this option, edit this post, bust this cache. Raw PHP execution still exists, but as a separately gated, admin-only, SSH-only tier. It's never the front door.

Dry-run by default

Every guarded verb defaults to apply=False. The response previews the exact before-and-after plus a change digest — nothing writes until that preview is approved.

Change Packets

An approval authorizes one exact digest. Nothing else.

Every packet binds site, target, verb, arguments, preview, and pre-change state into one hash. Change any of those and the approval stops matching. It can't be reused for a different value, a different target, or a site that's since drifted.

  • Site, target resource, and verb name
  • Exact normalized arguments (or their canonical hash)
  • Risk classification, reason, and a human-readable summary
  • Pre-change state hash / ETag
  • Sanitized preview or diff
  • Approver identity, decision, comment, and timestamp
digest binding
# dry-run returns a SHA-256 digest over:
change_digest = sha256(
  site, verb, target,
  normalized_payload, current_etag
)

# execution refuses unless the live request
# still matches the approved digest exactly
wp_mutate_option(…, apply=True,
  expected_etag="<etag from preview>")
 refuses if payload or live state differs
 PacketRequiredError if no approved packet exists
Independent approval

Three policy outcomes. Approval is a role your team assigns to someone else.

Auto-allow

Low-risk, reversible actions go straight through. Nobody babysits every harmless read or trivial change — that's what keeps approval fatigue from setting in.

Require approval

Risky writes route to a human first. The approver reviews the same preview the requester saw and signs off on that exact digest. Assign the role to someone else, and no one reviews their own work.

Hard-deny

Some actions are refused outright by policy, regardless of who's asking or how they ask.

Proof over promises

What actually happens when something tries to go wrong.

No approved packet, no write
A guarded call with apply=True and no approved, open packet for that exact site, target, and payload raises PacketRequiredError. No write, no snapshot, nothing touched.
Tampering invalidates the approval
Change one argument after a packet's approved and the digest stops matching what's about to run. The old approval doesn't carry over.
Stale state fails closed
If the live value changes after preview but before execution, passing the dry-run's etag back as expected_etag catches it. The write refuses instead of silently overwriting someone else's change.
Drift gets caught after the fact
packet_close(durable_check_delay_seconds=…) re-reads the mutated value after a delay and flags it if a cache or plugin quietly reverted it. Verification happens twice — once at write time, once again after a delay.

Roadmap: a tested, one-click rollback for plugin and theme updates is next. Today's snapshot-before-write covers option and post-meta changes.

What we don't claim

Restraint is part of the trust model.

  • "Unbreakable," "zero risk," or "AI can never damage your site." We don't say it. It isn't true of any system.
  • No SOC 2, HIPAA, or similar compliance claim unless it's been independently verified.
  • We don't call the audit ledger "immutable." That word gets earned once it's cryptographically tamper-evident and externally anchored.
  • We call it a "snapshot" unless a tested, one-click restore actually exists. "Rollback" is reserved for that.
  • We won't say "works with every plugin or page builder" until integration tests prove it, plugin by plugin.
  • We won't say "never sends content to Cloud" — two of Cloud's three preview modes carry sanitized or encrypted content. See the exact boundary on the trust architecture page.
Threat model & limitations

The guard stops accidents: a wrong target, a stale write, a tampered approval. It does not stop a fully-authorized caller with a valid admin-scoped token who chooses to misuse it. Read SECURITY.md for the full trust boundaries, known open risks, and how to report a vulnerability.

Walk through the guard on a real client site.

A 30-minute review: map how AI reaches production today, test one harmless dry run, and see the evidence trail.