Both the skills.sh and autoskills.sh scans (dispatched this session, see
granja/_temp/codex-logs/{skills-sh-scan,autoskills-sh-scan}.md) independently
flagged wordpress/agent-skills' official wp-wpcli-and-ops module as high-value
source material for this skill: safe search-replace, db export/import,
multisite targeting, and deterministic environment inspection before any
destructive command.
Merged in (adapted to this fleet's bastille cmd + su -m www invocation model,
not the upstream's local/SSH-direct WP-CLI assumption):
- pre-write environment/blast-radius inspection sequence (plain wp-cli calls
through the existing invocation pattern, not the upstream's Node.js
wpcli_inspect.mjs script)
- multisite targeting checklist (--url requirement, site list iteration)
- safe search-replace/domain-migration workflow (backup, dry-run, flush)
- db export/import, plugin/theme, and cron/cache-flush guardrails
Ties the upstream --allow-root warning to this fleet's own documented
wp-content ownership-drift incident (docs/server-granja.md) and cites the
real jail counts/CVE rollout from docs/server-granja.md, server-staging.md,
and server-gringo.md for grounding. Provenance noted in the file with source
URLs. Deliberately left out generic plugin-development/performance/PHPStan
material from the same upstream repo — out of scope for this remote-execution
skill.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
224 lines
12 KiB
Markdown
224 lines
12 KiB
Markdown
---
|
|
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, 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
|
|
|
|
## Invocation pattern
|
|
|
|
```bash
|
|
ssh user@host "bastille cmd <jail-name> sh -c 'cd /path/to/site && su -m www -c \"wp <command> --path=/path/to/site\"'"
|
|
```
|
|
|
|
Key points:
|
|
- Run wp-cli as the **web server user** (`www`, `www-data`, etc.), not
|
|
root — WordPress file ownership assumptions and some plugin behavior
|
|
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 <jail-name> 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 <jail-name> 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 <jail-name> 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 <jail-name> 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=<site-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=<site-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 <hook>` 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
|
|
|
|
A wrapper like `bastille cmd <jail>` prints a `[jailname]:` header line
|
|
before the actual command output — this is the wrapper's own framing,
|
|
not part of the real output. Any script that captures this output for
|
|
further parsing must filter it out:
|
|
|
|
```bash
|
|
OUTPUT=$(ssh user@host "bastille cmd <jail> <command>" 2>&1 | grep -v "^\[<jail>\]:$")
|
|
```
|
|
|
|
Forgetting this is a common, easy-to-miss source of "the command
|
|
mysteriously failed to parse" bugs when scripting on top of jail-exec
|
|
wrappers.
|
|
|
|
## GOTCHA: `error_reporting(0)` in bootstrap files can mask real errors
|
|
|
|
Some CMS/application bootstrap files set `error_reporting(0)` globally
|
|
near the very top (often to suppress noisy legacy warnings). If you're
|
|
writing a standalone diagnostic/one-off script that `require`s such a
|
|
bootstrap file, your own errors after that point are ALSO silently
|
|
suppressed — a script can fail with **zero output and just a bad exit
|
|
code**, giving no clue why. Fix: explicitly
|
|
`error_reporting(E_ALL); ini_set('display_errors', '1');` again
|
|
**after** requiring the bootstrap, and add a
|
|
`register_shutdown_function` that dumps `error_get_last()` — this turns
|
|
a silent failure into an actual, readable error message.
|
|
|
|
## GOTCHA: glob patterns fail silently under some remote shells
|
|
|
|
`bastille cmd <jail> grep -rln 'pattern' /some/path/*.php` can fail with
|
|
`zsh: no matches found: ...` if the jail's default shell is zsh and the
|
|
glob doesn't expand as expected in that invocation context — with no
|
|
useful indication that this is a shell-globbing issue rather than "no
|
|
files matched." Fix: drop the glob and grep the directory recursively
|
|
instead (`grep -rln 'pattern' /some/path/`, no trailing `/*.ext`) — this
|
|
avoids the shell needing to expand anything at all.
|
|
|
|
## When you need to run PHP with full application bootstrap, standalone
|
|
|
|
If you need to call an application's own PHP functions/classes outside
|
|
the normal web request flow (e.g. to directly test a code path), don't
|
|
guess at which files to `require` — trace the **actual real bootstrap
|
|
sequence** the application's own front controller uses (e.g. its
|
|
`index.php`) by reading it, and replicate that exact require order in
|
|
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.
|