---
title: "Running Claude Code Against WordPress: The Complete Setup"
url: https://adityaarsharma.com/running-claude-code-against-wordpress-the-complete-setup/
date: 2026-08-21
modified: 2026-08-21
author: "Aditya R Sharma"
description: "Six months of AI agents against production WordPress. The MCP stack, the guardrails that block destructive commands, and four failure modes no tutorial covers."
categories:
  - "AI"
  - "Cyber Security"
  - "WordPress"
word_count: 1179
---

# Running Claude Code Against WordPress: The Complete Setup

There is a thread on r/Wordpress asking whether anyone runs Claude Code against WordPress in a real way. 118 upvotes. 153 comments.

The best answer in it is a Google Doc.

Underneath that answer, someone wrote: *"I am very intrigued by this concept but understand 10% of this comment."* Twenty-three people upvoted them.

I have been running agents against production WordPress for about six months. Plugins on 500,000+ installs, client sites, my own servers. This is the writeup that thread needed.

## The stack

Four MCP servers plus the CLI. Nothing exotic.

### wp-cli over SSH

The single highest-leverage piece. Everything WordPress can do from the command line, the agent can do. Plugin state, user capabilities, transients, cron, search-replace across the database, revision cleanup.

If you give an agent one tool for WordPress, give it this.

### A PHP execution bridge

I use one that exposes `execute-php` inside the WordPress context. This is the dangerous one and I come back to it below.

### Playwright

For anything visual or interaction-dependent. Whether a modal actually opens, whether a form actually submits, whether the layout survives at 375px.

### A crawler

I built mine as an MCP server because I was tired of exporting CSVs and pasting them into a chat. Internal linking, orphan pages, redirect chains, schema validation. The agent calls it and reasons over the result in the same context.

## The guardrails matter more than the stack

I learned this the expensive way.

In August I lost hours of work to a `git reset --hard` that an agent ran because it seemed like the reasonable way to get back to a clean state. It was reasonable. It was also destructive, and the reflog only tells you what happened after you have already lost the afternoon.

So now there is a hook that blocks it. Sixteen rules, all at the tool layer, all firing before execution:

- `git reset --hard`, `reset HEAD~`, `branch -D`, `clean -fd`, `rm -r` on tracked paths
- `gh repo create` without `--private`, because the default is public
- `docker compose down -v` on any stack the agent did not create, because named volumes do not come back
- `rm -rf` anywhere near a home directory

The pattern that generalises: **the agent will occasionally choose a destructive path because it is the shortest correct path.** Instruction-level prevention does not hold. Prompts get overridden by context. A hook that returns a non-zero exit code does not.

If you take one thing from this post, take that. Put the guardrail in the tool layer, not the prompt.

## Four things that break

### 1. Serialized data and string length

Page builder content lives in `_elementor_data` as a JSON blob in `wp_postmeta`. Agents handle this fine, mostly.

The trap is PHP-serialized data elsewhere. I did a find-and-replace across a client site recently, swapping one email address for another everywhere it appeared. Straightforward.

The new address was one character longer than the old one.

PHP serialization stores string lengths inline: `s:30:"..."`. Change the string without changing the declared length and the whole structure becomes unreadable. A raw `str_replace` across `wp_options` would have silently corrupted every serialized setting on the site.

The fix is boring and absolute: for serialized data, `get_option` into PHP, walk the structure, `update_option` back and let WordPress reserialize. Raw string replacement only where the payload is JSON or plain text.

I built a length check into the script that aborts if the replacement differs in length and the target might be serialized. It fired. That check saved the site.

### 2. Lazy-loaded admin UI lies to a headless browser

This one cost me a wrong diagnosis in front of a client.

I audited a page with Playwright, queried the DOM for a set of sections, and got nothing back. Reported them as missing. Recommended building them.

They existed. They were lazy-loaded, and in an automated context with no real scroll behaviour they never rendered. My query was correct and my conclusion was garbage.

**Not rendered is not the same as not present.** Now I check a second way before I claim absence: the dedicated detail route, the REST response, or the database directly. Whichever is authoritative rather than whichever is visible.

This is the WordPress instance of a general agent problem. The model reports what the tool returned. If the tool has a blind spot, the model inherits it with full confidence and no hedging.

### 3. Rich text editors reject programmatic input

Modern editors are ProseMirror or TipTap under the hood. They keep internal document state and ignore anything that does not look like real input.

Setting `.value` does nothing. Playwright's `fill()` appended instead of replacing, so I ended up with the old content and the new content concatenated, and a character counter reading 276 out of a 220 limit.

What works is `document.execCommand('selectAll')` followed by `execCommand('insertText')`. Deprecated, still the reliable path, because it produces the input events the editor is actually listening for.

Separately, React-controlled inputs reject programmatically set values outright. The fix is the native prototype setter for the correct element type, followed by a bubbling input event. Use `HTMLTextAreaElement.prototype` for a textarea, not `HTMLInputElement.prototype`. I lost an hour to exactly that mismatch: the DOM value changed, React state did not, and every save sent the old data.

### 4. Cache invalidation has more layers than you think

Change `_elementor_data` and the change is live in the database and invisible on the site.

The order that actually works:

- `delete_post_meta` for `_elementor_css` and `_elementor_page_assets`
- `Elementor\Plugin::$instance->files_manager->clear_cache()`
- `wp_cache_flush()`
- Host-level page cache
- CDN purge
- Delete revisions, because they carry stale copies that resurface

Skip a layer and you get the worst possible outcome: it works for you and not for the client, and you spend an hour arguing about what you can both see.

## What actually changed

The measurable difference is not that individual tasks got faster. It is that the follow-up question became free.

Old flow: run the crawl, export, filter, find something interesting, go back for more context, re-explain. Every follow-up cost a round trip.

New flow: "now check whether those pages are in the sitemap," "now show me which have inbound internal links," "now check if any are noindexed." Each of those is one sentence against context the agent already holds.

That is the actual unlock. Not speed on the first answer. Cheapness on the fourth.

## What I would tell the person in that thread

Start with wp-cli over SSH and nothing else. Learn what an agent does well and badly against a real site before adding surface area.

Add the destructive-command hook before you add the second tool. Not after your first bad afternoon.

Never let an agent touch a production database without a snapshot in the same session. Not a nightly backup. A snapshot you took two minutes ago.

Verify visual claims twice, through two different mechanisms. The agent is confident either way.

And accept that a meaningful share of your time moves from doing the work to specifying the work precisely enough that the agent does it correctly. That is a real cost. It is still worth it, but nobody selling you on this mentions it.