WordPress Development
How to Review AI-Generated WordPress Code Before It Ships
AI tools write plausible WordPress PHP that fails in predictable ways: invented hooks, missing escaping, wrong load order, and queries on every request. A review checklist with before-and-after examples.
6 min read Mark Ashton
- ai
- security
- wordpress
- snippet engineer
AI assistants are now the fastest way to get a first draft of a WordPress snippet. They are also very good at producing code that looks right, passes a glance, and fails on a real site.
The failures are predictable. After reviewing a lot of AI-written WordPress PHP, the same eight problems account for almost every bug. This is the checklist, with a before-and-after for each, plus the tooling that catches them automatically.
Why AI-written WordPress code needs its own review
General-purpose models learned WordPress from fifteen years of tutorials, forum answers, and plugin code — much of it outdated, insecure, or written for a different version. They also have no idea what is installed on *your* site. So they:
- Mix current APIs with patterns from 2014
- Invent hooks and functions that sound plausible
- Assume plugins are active and loaded
- Optimize for "works in the demo," not "works on every request of a busy store"
None of this means you should not use AI. It means the review has to look for these specific failure modes.
1. Does every hook and function actually exist?
This is the most common problem and the easiest to miss, because invented names look exactly like real ones. woocommerce_after_cart_totals_row sounds right. Check whether it is.
How to check:
- Search the WordPress Developer Reference for core hooks and functions
- For plugins, search the plugin's source:
grep -rn "do_action( 'hook_name'" wp-content/plugins/woocommerce - In a running site,
has_action( 'hook_name' )tells you if anything is attached; adding a temporaryerror_loginside your callback tells you if it fires
A callback on a hook that never fires does not error. It just silently does nothing, which is often worse.
2. Is output escaped?
AI drafts routinely echo variables directly:
// Before
echo '<div class="notice">' . $message . '</div>';
echo '<a href="' . $url . '">' . $label . '</a>';// After
echo '<div class="notice">' . esc_html( $message ) . '</div>';
echo '<a href="' . esc_url( $url ) . '">' . esc_html( $label ) . '</a>';Escape at the point of output, with the function that matches the context: esc_html for text, esc_attr for attributes, esc_url for URLs, wp_kses_post for trusted HTML. "It is only an admin notice" is not a reason to skip it.
3. Are input, permissions, and intent checked?
Anything that reads a request and changes data needs three checks: sanitize the input, confirm the user is allowed, and confirm they meant to do it.
// Before
add_action( 'admin_post_acme_save', function () {
update_option( 'acme_banner', $_POST['banner'] );
wp_redirect( admin_url() );
} );// After
add_action( 'admin_post_acme_save', function () {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Not allowed.', 'acme' ), 403 );
}
check_admin_referer( 'acme_save' );
$banner = isset( $_POST['banner'] ) ? sanitize_text_field( wp_unslash( $_POST['banner'] ) ) : '';
update_option( 'acme_banner', $banner );
wp_safe_redirect( admin_url( 'options-general.php?page=acme' ) );
exit;
} );Watch for the small ones too: wp_unslash before sanitizing, wp_safe_redirect instead of wp_redirect, and exit after redirecting.
4. Is SQL prepared?
Models still concatenate variables into queries:
// Before
$rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}orders WHERE email = '$email'" );// After
$rows = $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}orders WHERE email = %s", $email )
);Better still, ask whether raw SQL is needed at all. WP_Query, get_posts, wc_get_orders, and get_users handle caching and escaping for you.
5. Does it run at the right time?
A snippet's top-level code runs when the snippet loads, which may be before other plugins, the current user, or the query are ready. AI drafts often call things too early:
// Before — WooCommerce may not be loaded, and there is no current user yet
if ( WC()->cart->get_cart_contents_count() > 5 ) { /* ... */ }// After
add_action( 'wp', function () {
if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
return;
}
if ( WC()->cart->get_cart_contents_count() > 5 ) { /* ... */ }
} );Rules of thumb: plugin APIs after plugins_loaded, user checks after init, conditional tags such as is_checkout() after wp.
6. Will it survive a missing dependency?
If WooCommerce, ACF, or a form plugin is deactivated for an update, a snippet that assumes it exists becomes a fatal. Every dependency needs a guard: class_exists, function_exists, or defined. Declared functions need function_exists guards too, so a duplicate import does not trigger "Cannot redeclare."
7. What does it cost on every request?
AI optimizes for correctness on one page view, not for performance across a million. Look for:
get_posts( array( 'posts_per_page' => -1 ) )in a global hookwp_remote_geton page load without caching- Queries inside loops
- Work in
initthat only matters on one page
// After — cache the remote call for an hour
$rates = get_transient( 'acme_fx_rates' );
if ( false === $rates ) {
$response = wp_remote_get( 'https://api.example.com/rates', array( 'timeout' => 5 ) );
$rates = is_wp_error( $response ) ? array() : json_decode( wp_remote_retrieve_body( $response ), true );
set_transient( 'acme_fx_rates', $rates, HOUR_IN_SECONDS );
}Then scope the snippet with conditional loading so it only runs where it is needed.
8. Is anything outdated or dangerous?
Red flags that should always stop a merge:
eval,create_function,assertwith strings,preg_replacewith the/emodifierunserializeon anything a user or remote server can influenceextract( $_POST )or$variablenames built from inputfile_get_contentson URLs instead of the HTTP API- Writing files to the web root
- jQuery patterns such as
.live()or a bundled jQuery copy
Also check PHP compatibility with your oldest production host. The WordPress 7.0 and PHP 8.4 guide lists what commonly breaks.
Automate the boring half
A human review should focus on logic and intent. Let tools catch the mechanical issues:
php -lfor syntax on every PHP version you run- PHPCS with WordPress Coding Standards — flags missing escaping, unsanitized input, and non-prepared SQL
- PHPStan with WordPress stubs — flags calls to functions and methods that do not exist, which catches many invented APIs
composer require --dev wp-coding-standards/wpcs phpstan/phpstan szepeviktor/phpstan-wordpress
vendor/bin/phpcs --standard=WordPress-Extra snippets/
vendor/bin/phpstan analyse snippets/ --level=5Run them in CI on every pull request. The GitHub workflow guide shows how to wire up a lint job for a snippet repository.
Make the tool run the checks before you see the code
The best time to catch these problems is before a human looks at all. This is the case for agents that execute and check their own code instead of returning a text block.
SnipVault's Snippet Engineer runs every PHP snippet in an isolated sandbox request to catch fatals, scores it with the security audit — which flags patterns such as unescaped output and unprepared queries — and cannot deploy code that exceeds your configured risk ceiling. You still review the result, but you review code that has already run once and passed a scan. More on how that loop works in Cursor for WordPress.
The checklist
Before approving any AI-written WordPress code:
- Every hook and function exists in the versions you run
- All output is escaped for its context
- Input is sanitized, permissions checked, nonces verified
- SQL is prepared, or replaced with a core API
- Code runs on the right hook, not at load time
- Every dependency is guarded
- Nothing expensive runs on every request
- No dangerous or deprecated functions
- Lint, PHPCS, and PHPStan pass
- You can explain what it does in one sentence
If you cannot do number 10, ask the AI to simplify it. Code you do not understand is code you cannot safely maintain — regardless of who wrote it.