Files
agent-skills/skills/wordpress-cli-remote-execution/SKILL.md
T
MalinandClaude Sonnet 5 d6ad3fa684 Merge multisite/blast-radius safety practices into wordpress-cli-remote-execution
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>
2026-08-15 21:25:44 +02:00

12 KiB

name, description
name description
wordpress-cli-remote-execution 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

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:

# 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:

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 requires 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.