6 WordPress Hooks AI Assistants Keep Hallucinating (and How to Catch Them Before Production)
Last Updated: May 25, 2026

Last Updated: May 25, 2026
AI coding assistants invent WordPress hook names that look correct but do not exist in core, and WordPress will register the callback silently without warning you. The six most common hallucinations seen in Copilot, Cursor, Claude, and ChatGPT output are save_posts, the_content_filter, pre_get_post, wp_init, wp_login_user, and woocommerce_order_completed. Verify every AI-generated hook against the WordPress Code Reference before merging.
AI coding assistants are quietly shipping broken WordPress code into production. The function compiles, the file passes lint, the PR gets approved, and the callback simply never fires because the hook name is invented.
WordPress does not validate hook names. add_action() and add_filter() register a callback against any string you give them, and do_action() is never called with that string because the string does not exist. The result is a silent failure in core flows like saving posts, completing WooCommerce orders, or modifying the main query.
This article catalogs six WordPress hook hallucinations seen repeatedly in code generated by GitHub Copilot, Cursor, Claude (Sonnet and Opus), and ChatGPT, anchored against the WordPress 7.0 (“Armstrong”) Code Reference released on May 20, 2026. It is a direct follow-up to how to pass a WordPress technical interview, where missed hooks rank among the top reasons candidates fail.

WordPress hook hallucinations slip through because the core hook system has no schema and no runtime validation. The functions add_action() and add_filter() accept any string as a hook name and store it in the global $wp_filter array. If do_action('made_up_hook') is never fired, the callback sits there forever, registered against nothing.
Reviewers usually spot-check that the callback signature matches the parameters, not that the hook name matches a real one. Static analyzers like PHPStan do not flag the issue unless you install WordPress stubs with strict-hook checking enabled. Unit tests pass because the hook is not invoked during the test. The bug surfaces in production when a user saves a post and the expected email never goes out. The fundamentals are covered in the skilled WordPress developer plugin tutorial, but few teams enforce them on AI-generated PRs.
The list below pairs each hallucinated hook with the real hook from the WordPress Code Reference, the verified signature, and the AI tools most likely to produce the error in recent testing across Copilot, Cursor, Claude, and ChatGPT.
save_posts (plural) → real hook is save_postAI assistants pluralize this hook because the verb is “save posts.” The real hook is singular: save_post. It fires once a post has been saved and passes three parameters: $post_ID, $post, and $update.
php
add_action( 'save_post', 'my_callback', 10, 3 );
function my_callback( $post_ID, $post, $update ) { /* ... */ }For post-type-specific work, use the dynamic save_post_{$post->post_type} hook (added in WordPress 3.7), which fires before the general save_post.
the_content_filter → real hook is the_contentCopilot and ChatGPT sometimes append _filter to filter names. The real filter is the_content. It receives a string and must return a string.
php
add_filter( 'the_content', 'my_content_filter' );
function my_content_filter( $content ) { return $content; }The same pattern affects the_title_filter (invented) versus the_title (real).
pre_get_post (singular) → real hook is pre_get_postsThe singular form is invented. pre_get_posts fires before the main query executes, receiving the WP_Query object by reference. Always guard with is_main_query() and is_admin() checks.
php
add_action( 'pre_get_posts', function( $query ) {
if ( is_admin() || ! $query->is_main_query() ) return;
$query->set( 'posts_per_page', 12 );
} );wp_init → real hook is initAI assistants prefix many hooks with wp_ even when the prefix is wrong. The real hook is just init. For work that must run after all plugins and the active theme are fully loaded, use wp_loaded, which runs after init. The execution order is plugins_loaded → setup_theme → after_setup_theme → init → wp_loaded → wp.
wp_login_user → real hook is wp_loginThe real hook is wp_login, fired by wp_signon() immediately after wp_set_auth_cookie(). It passes two parameters, and you must request both explicitly.
php
add_action( 'wp_login', 'my_login_handler', 10, 2 );
function my_login_handler( $user_login, $user ) { /* $user is WP_User */ }For pre-authentication work, use wp_authenticate instead.
woocommerce_order_completed → real hook is woocommerce_order_status_completedWooCommerce hooks follow the pattern woocommerce_order_status_{status}, not woocommerce_order_{status}. The real action when an order moves to “completed” is woocommerce_order_status_completed. It passes the order ID, and as of WooCommerce 3.0 also passes the WC_Order object.
php
add_action( 'woocommerce_order_status_completed', 'my_order_done', 10, 2 );
function my_order_done( $order_id, $order ) { /* ... */ }This hook does not always fire when the status is changed through the WooCommerce REST API, per the WooCommerce Action and Filter Hook Reference.
For trashed (not force-deleted) posts, use wp_trash_post.
| Hallucinated hook | Real hook | Type | First introduced | Verified in |
|---|---|---|---|---|
save_posts | save_post | action | WP 1.5 | Code Reference |
the_content_filter | the_content | filter | WP 0.71 | Code Reference |
pre_get_post | pre_get_posts | action | WP 2.0 | Code Reference |
wp_init | init | action | WP 1.5 | Code Reference |
wp_login_user | wp_login | action | WP 1.5 | Code Reference |
woocommerce_order_completed | woocommerce_order_status_completed | action | WC 2.1 | WooCommerce Hook Reference |
The pattern is consistent across all six: AI tools pluralize, depluralize, prefix with wp_, suffix with _filter, or add before_/after_ to existing names. None of these variants are flagged at registration time.

To verify an AI-generated WordPress hook, search the exact hook name in the official Code Reference, run a local smoke test that confirms the callback fires, and configure PHPStan with WordPress stubs to fail the build on unknown hook names.
developer.wordpress.org/reference/hooks/. No result means treat the hook as hallucinated.error_log( 'fired: ' . current_action() ); at the top of the callback and trigger the expected action. If nothing logs, the hook is wrong or unfired in that context.szepeviktor/phpstan-wordpress, catches unknown hook usage at level 6+.init, wp_loaded, wp are documented in the Code Reference).$accepted_args value to add_action() matching the documented signature, or expected arguments will be null at runtime.Treat AI output as a junior contributor whose code requires the same verification you would apply to any unverified pull request — a workflow reinforced across junior WordPress developer onboarding.
AI assistants are reliable for WordPress hook work when the hook is widely documented, used heavily in open-source code, and named in a stable pattern (wp_enqueue_scripts, admin_init, the_content). Reliability drops for hooks introduced in the past 12 months, third-party plugin hooks, and dynamic naming patterns like save_post_{$post_type}. The bug-detection comparison in VS Code Copilot Enterprise vs JetBrains AI found PhpStorm with JetBrains AI ships WordPress awareness that catches more of these patterns at type-check time.
For block editor work, AI suggestions for hooks like init (used by register_block_type) are accurate. AI suggestions for block filters tend to drift toward made-up names — verify against the Block Filters reference and the native vs ACF blocks analysis for context.
save_posts, the_content_filter, pre_get_post, wp_init, wp_login_user, and woocommerce_order_completed.developer.wordpress.org/reference/hooks/ before merging AI-generated code.error_log( current_action() ) smoke test to confirm every AI-generated callback actually fires in the target environment.php-stubs/wordpress-stubs and szepeviktor/phpstan-wordpress at level 6 or higher to catch unknown hook names statically.save_post_{$post_type} over the generic save_post to avoid unnecessary callback execution.woocommerce_order_status_{status}, not the shorter invented variants.WordPress hook hallucinations are not a verdict on AI tooling — they are a consequence of a hook system that trades validation for flexibility, and that trade-off matters more when AI writes the code. Verify before merging, log before assuming, and document the Code Reference version you checked against. Anchored to WordPress 7.0 “Armstrong” (May 20, 2026); re-verify on every upgrade.
WordPress hooks are stored in a global $wp_filter array keyed by string, with no validation against a known list. This is intentional so plugins can define custom hooks. The trade-off: add_action('typo_in_hook_name', ...) is indistinguishable from a valid registration until do_action() is (or is not) called with that exact string.
Yes, with extra configuration. The php-stubs/wordpress-stubs package combined with szepeviktor/phpstan-wordpress flags unknown core hook names at PHPStan level 6 and above. Out-of-the-box PHPStan does not know about WordPress hooks.
WordPress 7.0 “Armstrong,” released May 20, 2026, does not change add_action/add_filter validation behavior. It raises the minimum PHP version to 7.4 and adds new block editor APIs, but the hook system remains forward-compatible by design.
No tool currently publishes a measured rate. In practice, assistants tuned with WordPress context (PhpStorm with JetBrains AI ships built-in WordPress support) produce fewer hallucinated hooks than general-purpose assistants. Always verify regardless of the tool.
Add add_action( $hook_name, function() use ( $hook_name ) { error_log( 'fired: ' . $hook_name ); } ); and trigger the action. If nothing logs in wp-content/debug.log, the hook does not exist or is not firing in that context.