diff --git a/skills/wordpress-cli-remote-execution/SKILL.md b/skills/wordpress-cli-remote-execution/SKILL.md index fab0c12..0736491 100644 --- a/skills/wordpress-cli-remote-execution/SKILL.md +++ b/skills/wordpress-cli-remote-execution/SKILL.md @@ -1,6 +1,6 @@ --- name: wordpress-cli-remote-execution -description: Use when running wp-cli commands against a WordPress site living inside a remote jail/container (via ssh + a jail-exec wrapper). Covers the output-prefix stripping gotcha and the correct user/path invocation pattern. +description: Use when running wp-cli commands against a WordPress site living inside a remote jail/container (via ssh + a jail-exec wrapper). Covers the output-prefix stripping gotcha, the correct user/path invocation pattern, pre-write environment/blast-radius inspection, multisite targeting, and safe search-replace/db-export-import/cache-flush workflows. --- # WordPress CLI Remote Execution @@ -17,6 +17,141 @@ Key points: depend on this. - Always pass `--path=` explicitly rather than relying on `cd` alone propagating through every nested layer correctly. +- This is not a theoretical concern on this fleet: `docs/server-granja.md` + documents a real incident where `wp-content` (the `upgrade` dir, + `languages/`, and several plugin dirs) drifted to `root:www` ownership + from `wp` commands run as root via `bastille cmd ... wp ... --allow-root` + instead of switching to `www` first, which then broke every + admin-initiated core/plugin update until a `chown -R www:www` fixed it. + Prefer the `su -m www` pattern above over `--allow-root`-as-root for + exactly this reason. + +## Guardrails: confirm environment and blast radius before any write + +Before running anything that writes (plugin/theme changes, `search-replace`, +`db import`/`db reset`, bulk deletes, cron triggers, cache/rewrite flushes +on a busy site), confirm you're pointed at the right target. This fleet +runs 13+ real WordPress jails on granja alone (`docs/server-granja.md`), +plus more on staging and gringo (`docs/server-staging.md`, +`docs/server-gringo.md`) — mixing up a jail name or `--path` when several +`ssh`/`bastille` panes are open is the easiest way to write to the wrong +site. Don't assume production is safe to write to just because a command +"should" be harmless. + +Run this sequence (through the invocation pattern above, one `wp` call at +a time so each exit code/output is unambiguous) before the real operation: + +```bash +# 1. Confirm wp-cli actually resolves at this exact path/jail +ssh user@host "bastille cmd sh -c 'su -m www -c \"wp --path=/path/to/site core is-installed\"'" + +# 2. Confirm which site this really is — read it back, don't assume from the jail name +ssh user@host "bastille cmd sh -c 'su -m www -c \"wp --path=/path/to/site option get siteurl\"'" + +# 3. Confirm core version, for context +ssh user@host "bastille cmd sh -c 'su -m www -c \"wp --path=/path/to/site core version\"'" + +# 4. Confirm single-site vs multisite BEFORE choosing --url targeting (see below) +ssh user@host "bastille cmd sh -c 'su -m www -c \"wp --path=/path/to/site core is-installed --network\"'" +``` + +Step 4's exit code is the signal: `0` means the install is network-activated +(multisite); nonzero means an ordinary single site. Don't skip this check +and assume — this fleet's WordPress jails haven't all been surveyed for +multisite, and treating a multisite install as single-site (or vice versa) +is exactly the kind of mistake that silently affects the wrong site. + +## Multisite targeting checklist + +If step 4 above confirms multisite (or you're unsure and haven't checked): + +- Every per-site command needs an explicit `--url=` — an omitted + `--url` on a multisite install can operate on the network's primary site + instead of the one you actually meant, with no error to warn you. +- `wp --path=/path/to/site site list` to see every site in the network + before touching any of them. +- `wp --path=/path/to/site option get siteurl --url=` to confirm + you're targeting the intended site within the network. +- For a change that's meant to apply network-wide, prefer scripting it as + list-then-iterate (`site list` → loop → run the safe per-site command) + over trusting a single `--network`-flagged command to do the right thing + for every site's data — a loop you can log and interrupt is easier to + recover from than one opaque network-wide mutation. + +The jail audits in `docs/server-granja.md`, `docs/server-staging.md`, and +`docs/server-gringo.md` map each jail to a single domain via +`wp option get siteurl` — but that check confirms the site's URL, not +whether the install itself is network-activated. None of those audits ran +`core is-installed --network`, so multisite status per jail is genuinely +unverified, not "probably fine because it's one domain per jail." Run +step 4 above rather than assuming either way for a jail you haven't +personally checked. + +## Safe `wp search-replace` / domain migration workflow + +Follow this sequence for any URL/domain change, protocol switch +(`http://` → `https://`), or path migration — don't run `search-replace` +directly against production without it: + +1. **Backup first:** `wp --path=/path/to/site db export` (through the + invocation pattern above) before touching anything. Confirm the backup + file actually landed and is non-empty — `wordpress-plugin-staging-verification` + documents a cosmetic `proc_open`/`posix_spawn` error some `wp-cli` write + commands print in this sandbox even when the underlying operation + actually succeeded (and, less often, the reverse) — don't trust the + command's own success/failure text alone for something this important; + check the file. +2. **Dry run:** `wp search-replace 'OLD' 'NEW' --dry-run --all-tables-with-prefix` + and read the reported row count before doing anything else. +3. **Real replace:** the same command minus `--dry-run`. Add + `--skip-columns=...` for any known binary/blob columns, and `--precise` + if the dry run surfaced questionable matches. +4. **Flush after:** `wp cache flush` then `wp rewrite flush` — a stale + object cache or stale rewrite rules are the most common "why does the + old URL still show up" symptom after a migration that actually worked + fine at the DB level. + +On multisite, decide up front whether the replace is scoped to one site +(`--url=...`) or the whole network (iterate `wp site list`, per the +checklist above) — the same "which site did this actually touch" risk +applies here as everywhere else in this skill. + +## DB export/import guardrails + +- `wp db export` before any of the high-risk operations below — cheap + insurance, always take it. +- `wp db import` **overwrites the target database wholesale.** Re-confirm + the jail/site one more time immediately before running it (repeat step 2 + of the guardrails sequence above) — this is the single easiest way to + destroy the wrong site's data when several `ssh`/`bastille` sessions are + open at once across this fleet's jails. +- Treat these as requiring explicit confirmation before running, same as + bulk deletes (`wp post delete --force --all`, `wp user delete --reassign`) + and mass plugin/theme updates on a live production jail. + +## Plugin/theme operations guardrails + +- `wp plugin list` / `wp theme list` first, to see current state before + changing it. +- Avoid `wp plugin update --all` / `wp theme update --all` against a + production jail without a deliberate maintenance window — this fleet's + own 2026-08-12 fleet-wide core-security rollout (CVE-2026-65640, + `docs/server-granja.md`) was done as a coordinated pass across all 13 + jails, not a casual one-off `update --all`. +- On multisite, plugin activation can be per-site or network-activated — + confirm which one is intended (see the multisite checklist above) before + running `wp plugin activate`. + +## Cron and cache/rewrite flush guardrails + +- `wp cron event list` to see what's actually scheduled before running + anything. +- `wp cron event run ` to run one specific event for debugging, + rather than triggering every scheduled event at once. +- `wp cache flush` / `wp rewrite flush` are generally low-risk but can + cause a brief load spike on a high-traffic content site — worth + confirming intent rather than flushing reflexively as a first debugging + step. ## GOTCHA: jail-exec wrappers prefix their own output @@ -67,3 +202,22 @@ your standalone script. Skipping steps because "this part probably isn't needed" reliably produces a cascade of "undefined function/class" errors that have to be debugged one require statement at a time — reading the real bootstrap sequence once up front is faster than that cascade. + +## Provenance + +The guardrails/blast-radius sequencing, multisite targeting checklist, +and safe search-replace/db-export-import/cache-flush workflow above were +adapted from WordPress's own official `wp-wpcli-and-ops` skill +(https://github.com/WordPress/agent-skills/tree/trunk/skills/wp-wpcli-and-ops, +also listed at https://skills.sh/wordpress/agent-skills/wp-wpcli-and-ops), +reworked for this fleet's `bastille cmd` + `su -m www` invocation model +rather than the upstream skill's assumption of local or SSH-direct WP-CLI +access. Its bundled `wpcli_inspect.mjs` Node.js inspector script was not +copied — this fleet doesn't run wp-cli operations through Node tooling — +the same inspection sequence is reproduced above as plain `wp` calls +through the existing invocation pattern instead. Notably, the upstream +skill's own debugging notes independently warn against `--allow-root` +"unless you understand the environment and have no alternative," which +lines up with the real `wp-content` ownership-drift incident cited in the +Invocation pattern section above — two independent sources converging on +the same conclusion for this fleet.