Manage WordPress sites through the REST API. Runs as an OpenClaw skill or in Claude Code.
This skill runs the scripts/*.py directly, from wherever the skill is installed
( ~/.claude/plugins/ for a plugin install, ~/.claude/skills/wordpress-api-pro/
for the repo's bash INSTALL.sh — that installer lives in the git repo, not in
the packaged skill):
WP_URL / WP_USERNAME / WP_APP_PASSWORD, or use config/sites.json for multi-site.requests ( python3 -m pip install -r requirements.txt, ideally in a venv). The pin is a RANGE, requests>=2.32.3,<3, not an exact version or a hash lock. That is deliberate: an exact pin stops users receiving patch-level security fixes for the dependency whose advisory is the reason for the lower bound, and this skill has no lockfile-refresh process to compensate. The trade is reproducibility for patchability.
2.32.0 fixed CVE-2024-35195, where a Session that made one verify=False request silently skipped certificate verification for every later request to that host, and 2.32.3 closes out that line's follow-up regressions. The core post/page/media/WooCommerce/batch scripts use the stdlib only.http://site.local, localhost, *.test, *.localhost) work over plaintext http. Any other host must be https:// unless you name it: WP_ALLOW_HTTP=staging.example.com (comma-separated for several). A blanket WP_ALLOW_HTTP=1 is refused — it used to permit every host, so one variable set for one staging box covered production too.siteagent-elementor-studio): build page structure with the MCP, then do media uploads, SEO meta, custom fields, and WooCommerce here.config/sites.json, keep it local, untracked, and chmod 600 config/sites.json.--execute only after reviewing the dry-run output.woo_products.py --action create sends draft unless you pass --status publish, and prompts at a TTY before a live one ( --yes skips, non-interactive runs are never prompted).--allow-all only when the user explicitly approved all configured sites.--content-file and media uploads can read only from the current working directory by default. Set WP_ALLOWED_FILE_ROOTS to opt into another safe directory.upload_media.py requires --allow-remote-url or WP_ALLOW_REMOTE_URLS=1, allows HTTPS only, and blocks private/local network hosts.seo_meta.py writes only keys in the Rank Math / Yoast allowlist; anything else fails, because a typo'd friendly name would otherwise create a junk postmeta row or overwrite a key another plugin owns. Set WP_ALLOW_RAW_META=1 to write raw keys with a warning. ACF/JetEngine custom-field keys are unaffected — arbitrary keys are their intended API.create_post.py and update_post.py prompt for confirmation before --status publish when run interactively. Pass --yes / -y to bypass. Non-interactive/agent runs are unchanged.Recommended environment variables:
bash
export WP_URL="https://example.com"
export WP_USERNAME="wp-api-user"
read -rs WP_APP_PASSWORD
export WP_APP_PASSWORD
Application Password setup:
https://your-site.example/wp-admin/profile.php.bash
python3 scripts/get_post.py --post-id 123
python3 scripts/list_posts.py --per-page 10 --status publish
bash
python3 scripts/create_post.py \
--title "Draft title" \
--content "Draft content" \
--status draft
bash
python3 scripts/update_post.py \
--post-id 123 \
--title "Approved title" \
--content "Approved content" \
--status draft
By default the file must be under the current working directory:
bash
python3 scripts/update_post.py \
--post-id 123 \
--content-file ./content/post-123.html \
--status draft
To opt into another safe folder:
bash
export WP_ALLOWED_FILE_ROOTS="/absolute/path/to/approved-content"
python3 scripts/update_post.py --post-id 123 --content-file /absolute/path/to/approved-content/post.html
Copy the template locally:
bash
cp config/sites.example.json config/sites.json
chmod 600 config/sites.json
Use a dedicated user per site and keep app_password values local only.
json
{
"sites": {
"sample-site": {
"url": "https://example.com",
"username": "wp-api-user",
"app_password": "",
"description": "Sample site; put the real credential only in local config/sites.json"
}
},
"groups": {
"sample": ["sample-site"]
}
}
bash
./wp.sh --list-sites
./wp.sh sample-site get-post --id 123
./wp.sh sample-site update-post --id 123 --status draft
Group operations require an explicit flag:
bash
./wp.sh sample --execute-group update-post --id 123 --status draft
If the group is named all, add --allow-all only after explicit approval:
bash
./wp.sh all --execute-group --allow-all update-post --id 123 --status draft
Batch mode is dry-run unless --execute is present:
bash
python3 scripts/batch_update.py \
--group sample \
--post-ids 123,456 \
--status draft
Apply after review:
bash
python3 scripts/batch_update.py \
--group sample \
--post-ids 123,456 \
--status draft \
--execute
Targeting every site requires explicit opt-in:
bash
python3 scripts/batch_update.py \
--group all \
--allow-all \
--post-ids 123 \
--status draft
Local file upload, restricted to allowed file roots:
bash
python3 scripts/upload_media.py \
--file ./media/image.jpg \
--title "Image title"
Remote URL upload, explicit opt-in and HTTPS-only:
bash
python3 scripts/upload_media.py \
--file https://cdn.example.com/image.jpg \
--allow-remote-url \
--title "Image title"
scripts/detect_plugins.py — detect ACF, Rank Math, Yoast, JetEngine.scripts/acf_fields.py — read/write ACF fields.scripts/seo_meta.py — read/write Rank Math and Yoast SEO metadata.scripts/jetengine_fields.py — read/write JetEngine custom fields.scripts/site_audit.py — no-auth Tier-1 website audit (PageSpeed/SSL/security headers/CMS+PHP/SEO basics). Public probes only; run cold pre-sale.scripts/describe_cpt.py — discover a CPT's rest_base, taxonomies, and field keys (read-only).scripts/seed_content.py — batch-create CPT entries with ACF/Jet fields, taxonomies, and featured images from a JSON dataset. Dry-run by default; pass --execute to write.scripts/elementor_content.py — read/update Elementor _elementor_data.scripts/woo_products.py — manage WooCommerce products.For dynamic sites (JetEngine/ACF listings), populate the entries the listings render:
describe_cpt.py --post-type projects — learn the rest_base, taxonomies, field keys.{post_type, title, content, status, terms, featured_image, acf, jet}).seed_content.py --dataset data.json — review the dry-run plan (no writes, stdlib-only).seed_content.py --dataset data.json --execute — create (drafts by default).Notes: the CPT, taxonomies, and ACF field-groups must already exist (admin-side).
featured_image accepts a media id or a URL/path (URL fetch needs --allow-remote-url).
--execute needs the requests dependency (used by the ACF/Jet writers). Re-running
creates duplicates (no upsert yet).
Before any live mutation:
draft unless the user explicitly approves publish.