I started writing WordPress plugins when the update API didn’t exist yet. I was hand-copying function signatures out of the Codex, debugging hook firing order with error_log() calls, and spending half a day chasing a typo in a filter name. So I have some context for what “fast” looks like when it comes to this kind of work.
These days I use Claude Code in the terminal, GitHub Copilot inside VS Code, and Cursor. I’ve been running that stack for most of the past year across WordPress plugin work on SeedProd and RafflePress at Awesome Motive, plus Cloudflare Workers and TypeScript projects. What follows is what I’ve actually found useful — and where I’ve been burned.
The Stack and Why
Claude Code runs in terminal and handles anything requiring reasoning across multiple files — architectural questions, reviewing a whole feature branch, understanding what a 400-line plugin class is actually doing. GitHub Copilot is pure muscle memory: it’s fast inline completion that stays out of your way. Cursor gets used when I want to hand an isolated refactor to an agent and watch the diff.
Using all three at once is a bad idea. There’s a mental overhead to switching context between AI suggestions mid-task, and you end up accepting stuff you haven’t really read. I pick the right tool for the shape of the work.
The Cloudflare Workers/TypeScript side of things is where Claude’s contextual reasoning genuinely shines over the alternatives — types, interface contracts, async edge cases. But this post is about WordPress because that’s where the bulk of the work is.
ACF Boilerplate: The Most Obvious Win
Writing acf_add_local_field_group() calls by hand is the kind of task where your brain goes on autopilot and you fat-finger a field key 20 minutes before a deadline. The array structure is verbose, repeaters nest in non-obvious ways, and the difference between a field type of 'true_false' and 'boolean' will silently break your field if you mix them up.
I describe the data model I need and let Claude generate the scaffold:
// Prompt I give Claude:
// "Generate an ACF field group for a 'Service' custom post type.
// Fields: title (text), short_description (textarea), icon (image),
// features (repeater with sub-fields: label (text), included (true/false)).
// Use acf_add_local_field_group(), follow ACF's array structure exactly."
The output is a complete, correctly-nested acf_add_local_field_group() call — right field type keys, repeater sub-fields in the proper format, location rules scoped to the CPT. What used to take 15 minutes of careful writing and a debugging round takes under two minutes. The low-level syntax errors just don’t happen anymore.
That said: I always read the output. Claude occasionally reaches for ACF Pro field type slugs when the free version has a different key, and location rules sometimes come out subtly wrong — like using post_type == service when the CPT slug is actually services. The review is fast. Writing it from scratch is not. That’s the trade.
Code Review Before It Becomes a Problem
One of the more useful habits I’ve built is running AJAX handlers and REST endpoints through Claude before committing them. Not for style — for the security gaps that are easy to miss when you’re moving fast.
Here’s an actual example. I had a rough draft of a settings handler in a plugin:
// Before — rough draft
add_action( 'wp_ajax_sp_save_settings', 'sp_save_settings_handler' );
function sp_save_settings_handler() {
$settings = $_POST['settings'];
update_option( 'sp_plugin_settings', $settings );
wp_send_json_success();
}
I asked Claude to review it for security issues. It flagged three things: no nonce check, no capability check, raw $_POST data written straight to the database. Then it produced the corrected version:
// After — reviewed and fixed
add_action( 'wp_ajax_sp_save_settings', 'sp_save_settings_handler' );
function sp_save_settings_handler() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Insufficient permissions.', 403 );
}
check_ajax_referer( 'sp_save_settings_nonce', 'nonce' );
$raw = isset( $_POST['settings'] ) ? $_POST['settings'] : [];
$settings = array_map( 'sanitize_text_field', $raw );
update_option( 'sp_plugin_settings', $settings );
wp_send_json_success();
}
I knew I needed the nonce. The snippet was a placeholder. But that’s exactly the scenario where things slip through — the placeholder never gets updated before it ships. Having a quick “review this for WordPress security” pass in my pre-commit routine catches those. It hasn’t replaced code review; it’s just a fast first filter.
Debugging Plugin Conflicts
Plugin conflicts are the worst category of WordPress debugging because the failure is usually indirect. Two plugins hooking the same filter with incompatible assumptions. A JavaScript dependency getting deregistered and re-registered at a different version. The symptom appears three interactions away from the actual cause.
I’ve been using Claude as a sounding board for this — paste the relevant hook registration from both plugins, describe the symptom, and ask it to reason through what could produce that behavior. It’s less useful when the failure is purely runtime (Claude can’t run the code), but for static “here are two things that interact — what’s the conflict” questions it’s often faster than bisecting manually.
One recent case: SeedProd’s block editor was losing its toolbar on a client site after a specific admin action. The symptom was intermittent and I couldn’t reliably reproduce it locally. I pasted the theme framework’s admin_enqueue_scripts callback alongside our own script registration from SeedProd. Claude spotted that the theme was deregistering a shared lodash dependency and re-registering its own build at a lower version number, which caused a silent failure downstream in the Vue components. That would have taken me much longer to find through normal bisection — especially since the Vue console errors pointed nowhere useful on their own.
It gets this wrong when the behavior is stateful or timing-dependent. Don’t expect it to debug race conditions or anything that depends on plugin load order at runtime. But static code analysis of hook interactions is a real strength.
Agentic Workflows: What I Actually Trust It With
Claude Code’s agentic mode lets you hand it a task and have it execute multi-step operations — reading files, writing code, running commands, iterating. I use it, but the scope matters a lot.
Tasks I’ll hand off to an agentic run: scaffolding a new theme template set (header, footer, page.php, single.php, archive.php with the correct template hierarchy comments), writing an initial PHPUnit test suite for a utility class, doing a bulk refactor across a plugin — like updating deprecated $wpdb->prepare() placeholders to the current %i/%s format across 30+ files in RafflePress.
Tasks I won’t hand off: anything touching production data, anything that modifies database schema, anything where the blast radius of an error is large. The agent is good at volume. It is not good at knowing when the scope of a task has grown past what you originally described, or when it should stop and ask rather than proceed. That boundary-setting is entirely on me up front.
My rule: agentic mode runs on greenfield work or isolated refactors in a clean Git branch. The diff gets reviewed before anything merges. That step is not optional.
Scaffolding Theme Templates
Theme development is a smaller part of my work now than it used to be, but when it comes up — especially building out a block theme or a hybrid classic/block setup — the scaffolding phase is tedious in a way that doesn’t require a lot of thought. It just requires time.
I’ll give Claude a brief: “Generate a WordPress block theme structure for a business site. Include templates for front-page, single, archive, 404, and search. Use theme.json with fluid typography, a 4-step spacing scale, and a two-column layout for single posts using block templates.” The output isn’t always perfect — fluid type scales in theme.json have some gotchas with clamp() values that Claude sometimes gets slightly wrong — but it’s a real starting point. Not a blank folder and three documentation tabs.
Writing Tests
For years I deferred PHPUnit setup more than I should have. The bootstrap configuration alone was enough friction to make me skip it when time was short. AI has made that friction small enough that I don’t have an excuse anymore.
Given a utility class or a REST controller, Claude generates a reasonable first-pass test suite — happy path, common edge cases, expected exceptions. The tests aren’t thorough by default. Claude tends to hit the obvious paths and skip the subtle integration behavior that’s usually where bugs actually live. But starting from a generated skeleton and extending it is much faster than starting from a blank file.
The habit I’ve built: when I fix a bug, I describe the failure mode to Claude and ask it to write a regression test. Something like: “This function was returning a cached result even after the underlying option was deleted, because the static cache wasn’t cleared in the deletion hook. Write a PHPUnit test that would catch this.” The tests it produces for specific, described failure modes are noticeably better than the generic ones it writes unprompted.
Where It Gets You Into Trouble
Last year I had Claude generate a WP_Query with a meta_query using 'compare' => 'BETWEEN' on a date field. Syntactically correct. Logically correct. Also tanked the page to a 4-second load because the client’s postmeta table had 600k rows and no composite index on meta_key + meta_value. Claude has no idea what your database looks like. It has no idea what your traffic looks like. It has no idea you’re running on a shared host with an 8-second max execution time and a DBA who doesn’t respond on weekends.
I’ve also had it produce Vue components for the SeedProd builder that worked fine in isolation and broke the moment they hit a WordPress admin page where jQuery was in no-conflict mode. Confidently used deprecated WooCommerce hooks — woocommerce_add_to_cart_redirect instead of the current filter. Generated REST endpoints with 'permission_callback' => '__return_true' as a placeholder and didn’t flag it as something to fix before shipping.
These failures share a pattern: the code looks right. It passes a fast read. It breaks later, in production, in ways that aren’t immediately traceable to the AI-generated snippet. That’s a harder failure mode to catch than an obvious error.
The question of whether this replaces developers is the wrong frame. The right question is whether you can tell when the output is wrong. After 18 years working with WordPress internals — filter execution order, $wpdb query performance, how Vue state behaves inside a WordPress admin context — I catch most of the bad output. Someone earlier in their career probably doesn’t catch as much of it, and that’s where real problems come from.
The fundamentals haven’t become less important. If anything, they’ve become more load-bearing, because the AI will confidently produce something that looks right but isn’t, and the only thing standing between that and production is someone who knows enough to be skeptical.
My workflow six months from now will probably look different — these tools move fast. But the review step is structural. It doesn’t go away.