WordPress Hooks Index
A searchable index of the WordPress actions and filters developers actually use — with the exact add_action/add_filter signature, when it fires, a working example, and the thing that bites people.
39 hooks, with the exact
add_action /
add_filter signature, when it fires, a working
example, and the thing that catches people out.
1 are marked
unverified — we have not checked their argument count against WordPress
source, and we would rather flag that than present a guess as fact.
This is not exhaustive and does not pretend to be — WordPress has thousands of hooks. It covers the ones people actually reach for.
Runs after WordPress has finished loading but before any headers are sent. The standard place to register custom post types, taxonomies, and rewrite rules.
Fires
On every request, front end and admin, after core, plugins and the theme are loaded.
Signature
add_action( 'init', 'my_callback' );Example
add_action( 'init', function () {
register_post_type( 'book', array(
'public' => true,
'label' => 'Books',
'show_in_rest' => true, // required for the block editor
) );
} );The gotcha
init runs on EVERY request including AJAX, REST and cron. Doing anything slow here (an HTTP request, a heavy query) taxes every single page load. Also: the current user is available here, but you cannot output anything — headers have not been sent yet, but they are about to be.
Runs once the theme is loaded. The correct place for add_theme_support(), add_image_size(), and registering nav menus.
Fires
After the theme's functions.php is loaded, BEFORE init.
Signature
add_action( 'after_setup_theme', 'my_callback' );Example
add_action( 'after_setup_theme', function () {
add_theme_support( 'post-thumbnails' );
add_theme_support( 'title-tag' );
add_image_size( 'card', 600, 400, true ); // true = hard crop
} );The gotcha
Registering image sizes on init instead of here works by accident in most cases and then fails mysteriously in some. Use after_setup_theme — it is what it is for.
Runs once all active plugins are loaded. The earliest safe point to interact with another plugin.
Fires
After every plugin file is included, before the theme is loaded.
Signature
add_action( 'plugins_loaded', 'my_callback' );The gotcha
The current user is NOT yet available here — wp_get_current_user() will not work reliably. If you need the user, use init.
Runs after WordPress is fully loaded — plugins, theme, init, and the user are all ready.
Fires
After init and after the current user is set up.
Signature
add_action( 'wp_loaded', 'my_callback' );Runs at the start of every admin request. Used for register_setting(), permission checks, and admin redirects.
Fires
On every admin-side request.
Signature
add_action( 'admin_init', 'my_callback' );The gotcha
It also fires on admin-ajax.php requests, which are not really "admin screens". If your callback assumes a screen exists, guard it with wp_doing_ajax().
The one correct place to enqueue front-end CSS and JavaScript.
Fires
On front-end page loads, before the head is rendered.
Signature
add_action( 'wp_enqueue_scripts', 'my_callback' );Example
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style(
'child-style',
get_stylesheet_uri(),
array( 'parent-style' ), // load AFTER the parent
wp_get_theme()->get( 'Version' ) // cache-bust on version change
);
} );The gotcha
Despite the name it handles STYLES as well as scripts — there is no wp_enqueue_styles hook, and people waste real time looking for one. Also: this does not fire in the admin (use admin_enqueue_scripts) or in the block editor (use enqueue_block_editor_assets).
Enqueue CSS/JS in the admin. Receives the current screen's hook suffix so you can load assets on one screen only.
Fires
On admin page loads.
Signature
add_action( 'admin_enqueue_scripts', 'my_callback' ); // receives $hook_suffixExample
add_action( 'admin_enqueue_scripts', function ( $hook ) {
if ( 'settings_page_my-plugin' !== $hook ) {
return; // do not load our JS on every admin screen
}
wp_enqueue_script( 'my-admin', plugins_url( 'admin.js', __FILE__ ), array(), '1.0', true );
} );The gotcha
Not guarding on $hook is the single most common cause of a plugin breaking unrelated admin screens. Load your assets where they are needed and nowhere else.
Enqueue assets into the block editor (Gutenberg) only — not the front end, not the rest of the admin.
Fires
When the block editor loads.
Signature
add_action( 'enqueue_block_editor_assets', 'my_callback' );Prints output into the <head>. Used for meta tags and inline critical CSS.
Fires
Inside <head>, on the front end.
Signature
add_action( 'wp_head', 'my_callback' );The gotcha
Do NOT enqueue scripts or styles here — use wp_enqueue_scripts, which lets WordPress handle dependencies and deduplication. Echoing a <script> tag into wp_head bypasses all of that and is why so many sites load jQuery three times.
Prints output just before </body>. The right place for analytics snippets and deferred markup.
Fires
At the end of the front-end page.
Signature
add_action( 'wp_footer', 'my_callback' );The gotcha
A theme that forgets to call wp_footer() breaks the admin bar, breaks any plugin that enqueues a footer script, and is an automatic rejection in the wordpress.org theme review.
Filters post content immediately before it is displayed. The usual place to append or prepend markup to a post.
Fires
Every time the_content() is called.
Signature
add_filter( 'the_content', 'my_callback' ); // receives $contentExample
add_filter( 'the_content', function ( $content ) {
if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
return $content; // <- the guard everyone forgets
}
return $content . '<p class="cta">Enjoyed this? Read more.</p>';
} );The gotcha
This runs far more often than you think — in widgets, in REST responses, in some plugins' excerpt generation, and once per post in a loop on an archive. Without the is_singular / in_the_loop / is_main_query guard, your CTA appears fifteen times on the blog index and inside the RSS feed. This is the most-misused filter in WordPress.
Filters the post title before display.
Fires
Every time the_title() or get_the_title() is called.
Signature
add_filter( 'the_title', 'my_callback', 10, 2 ); // receives $title, $post_idThe gotcha
It also fires for titles in the admin menu, in nav menus, and in the document <title>. Filtering it unconditionally will rename things in places you did not intend.
Sets how many WORDS an auto-generated excerpt contains. Default is 55.
Fires
When WordPress generates an excerpt for a post that has no manual excerpt.
Signature
add_filter( 'excerpt_length', 'my_callback' ); // receives $length (words)Example
add_filter( 'excerpt_length', function ( $length ) {
return 25;
}, 999 ); // high priority: many themes set this too, and last one winsThe gotcha
It has no effect on posts that have a MANUAL excerpt — those are used verbatim. And many themes set this filter themselves, so you often need a high priority number to win.
Changes the string appended to a truncated excerpt. Default is " […]".
Fires
When an auto-generated excerpt is truncated.
Signature
add_filter( 'excerpt_more', 'my_callback' ); // receives $moreModifies a query BEFORE it runs. The correct way to change what appears on an archive — far better than a second WP_Query.
Fires
After the query vars are parsed, before the SQL is built.
Signature
add_action( 'pre_get_posts', 'my_callback' ); // receives WP_Query $query (by reference)Example
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return; // <- both guards are mandatory
}
if ( $query->is_category() ) {
$query->set( 'posts_per_page', 12 );
}
} );The gotcha
It runs in the ADMIN too, and it runs for EVERY query, including menus, widgets and the media library. Omit the is_admin() and is_main_query() guards and you will silently change what the admin post list shows — a bug that is genuinely hard to trace back. Also note: modify $query, do not return anything.
Filters the raw SQL WHERE clause of a query.
Fires
While the query SQL is being assembled.
Signature
add_filter( 'posts_where', 'my_callback', 10, 2 ); // receives $where, WP_Query $queryThe gotcha
You are writing raw SQL. Always guard with $query->is_main_query() and always use $wpdb->prepare() for any user input. This is the fastest way to introduce SQL injection into a WordPress site.
Filters the total number of posts a query found — which is what pagination is calculated from.
Fires
After the query runs, before pagination is computed.
Signature
add_filter( 'found_posts', 'my_callback', 10, 2 ); // receives $found_posts, WP_Query $querySets the pixel size above which WordPress downscales an upload and serves a "-scaled" copy instead of your original. Default 2560.
Fires
On upload, when WordPress decides whether an image is "big".
Signature
add_filter( 'big_image_size_threshold', 'my_callback' ); // receives int $thresholdExample
// Serve originals at full resolution — no -scaled copy.
add_filter( 'big_image_size_threshold', '__return_false' );
// Or just raise the ceiling:
add_filter( 'big_image_size_threshold', function () {
return 3840;
} );The gotcha
This is the single most common cause of "my image looks blurry after uploading to WordPress", and almost nobody knows it exists. Your original is still on disk — it is simply never served. Note it applies to the LONGEST edge, and only affects images uploaded after the filter is active.
Filters the array of image sizes WordPress is about to generate for an upload. Unset a size to stop generating it.
Fires
On upload, before the derivative files are created.
Signature
add_filter( 'intermediate_image_sizes_advanced', 'my_callback', 10, 2 ); // receives $sizes, $metadataExample
add_filter( 'intermediate_image_sizes_advanced', function ( $sizes ) {
unset( $sizes['1536x1536'] );
unset( $sizes['2048x2048'] );
return $sizes;
} );The gotcha
Removing a size that your theme actually USES means WordPress falls back to the full-size original — which is worse than the disk space you saved. And this only stops NEW files being made; the existing ones stay until you regenerate.
Sets the JPEG quality WordPress re-encodes uploaded images at. Default 82.
Fires
Whenever WordPress writes a JPEG.
Signature
add_filter( 'jpeg_quality', 'my_callback', 10, 2 ); // receives $quality, $contextThe gotcha
Quality 82 is fine for photographs and visibly poor for screenshots, logos and flat graphics — those have hard edges and no photographic noise to hide artefacts in. Also: wp_editor_set_quality is the more modern filter and covers the image editor classes directly; setting both is common.
Sets the output quality used by the WP_Image_Editor classes (GD and Imagick).
Fires
When an image editor instance sets its quality.
Signature
add_filter( 'wp_editor_set_quality', 'my_callback', 10, 2 ); // receives $quality, $mime_typeAdds your custom image sizes to the size dropdown in the media modal and block editor.
Fires
When the media UI builds its size selector.
Signature
add_filter( 'image_size_names_choose', 'my_callback' ); // receives $sizesExample
add_filter( 'image_size_names_choose', function ( $sizes ) {
return array_merge( $sizes, array( 'card' => __( 'Card', 'textdomain' ) ) );
} );The gotcha
add_image_size() registers the size but does NOT make it selectable in the editor. Every "my custom image size does not appear in the dropdown" question is this filter being missing.
Inspects or rejects a file BEFORE it is moved into the uploads folder.
Fires
During upload, before the file is written.
Signature
add_filter( 'wp_handle_upload_prefilter', 'my_callback' ); // receives $fileThe gotcha
To reject a file, set $file['error'] to a message string and return $file. Returning false or throwing will not do what you want.
Filters the metadata array (including the generated sizes) after an attachment is processed.
Fires
After the derivative images are created on upload.
Signature
add_filter( 'wp_generate_attachment_metadata', 'my_callback', 10, 2 ); // receives $metadata, $attachment_idThe gotcha
This is the hook image-optimization plugins use. It can be slow — it runs after every image size is written, and doing an HTTP request here makes every upload wait on it.
Filters the srcset candidates WordPress generates for a responsive image.
Fires
When a responsive image is rendered.
Signature
add_filter( 'wp_calculate_image_srcset', 'my_callback', 10, 5 );The gotcha
Removing the medium_large (768w) size elsewhere silently degrades srcset — that size exists almost entirely to serve it.
We have not verified this hook's exact argument count against WordPress source. Check developer.wordpress.org before relying on it. We would rather flag the uncertainty than quietly present a guess as fact.
Registers admin menu and submenu pages.
Fires
When the admin menu is being built.
Signature
add_action( 'admin_menu', 'my_callback' );Example
add_action( 'admin_menu', function () {
add_options_page(
'My Plugin',
'My Plugin',
'manage_options', // capability — NOT a role
'my-plugin',
'my_render_settings_page'
);
} );The gotcha
The capability argument is a CAPABILITY, not a role. Passing "administrator" appears to work (because admins have a capability of that name in some setups) and then quietly grants access to the wrong people. Use manage_options.
Prints a notice at the top of an admin screen.
Fires
On admin page render.
Signature
add_action( 'admin_notices', 'my_callback' );The gotcha
It fires on EVERY admin screen. A plugin that shows an unconditional "thanks for installing!" notice on all of them is the single most-hated pattern in the WordPress admin. Guard it, and make it dismissible.
Runs whenever a post is created or updated. The usual place to save meta box data.
Fires
After a post is written to the database.
Signature
add_action( 'save_post', 'my_callback', 10, 3 ); // receives $post_id, $post, $updateExample
add_action( 'save_post', function ( $post_id ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
if ( wp_is_post_revision( $post_id ) ) return;
if ( ! current_user_can( 'edit_post', $post_id ) ) return;
// ... now it is safe to save
}, 10, 1 );The gotcha
It fires on AUTOSAVES and on REVISIONS as well as real saves. Without the three guards above, your meta gets overwritten with empty values every time the editor autosaves — a bug that looks like data randomly disappearing.
Registers meta boxes on the post edit screen.
Fires
When the edit screen is built.
Signature
add_action( 'add_meta_boxes', 'my_callback', 10, 2 ); // receives $post_type, $postThe gotcha
Classic meta boxes do not appear in the block editor unless the block editor falls back to compatibility mode. For a block-editor-native site, register post meta with show_in_rest and build a sidebar panel instead.
Runs after a user successfully logs in.
Fires
On successful authentication.
Signature
add_action( 'wp_login', 'my_callback', 10, 2 ); // receives $user_login, WP_User $userRuns immediately after a new user is created.
Fires
On registration.
Signature
add_action( 'user_register', 'my_callback', 10, 2 ); // receives $user_id, $userdataFilters the authentication result. Return a WP_Error to block a login.
Fires
During the login attempt.
Signature
add_filter( 'authenticate', 'my_callback', 30, 3 ); // receives $user, $username, $passwordThe gotcha
Priority matters a lot here. WordPress core hooks its own checks at priorities 20 and 30 — attach too early and you run before the password has even been checked.
Registers REST routes and fields.
Fires
When the REST API initialises.
Signature
add_action( 'rest_api_init', 'my_callback' );Example
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/items', array(
'methods' => 'GET',
'callback' => 'my_get_items',
'permission_callback' => '__return_true', // BE DELIBERATE ABOUT THIS
) );
} );The gotcha
permission_callback is REQUIRED — since WP 5.5 omitting it throws a notice, and setting it to __return_true makes the endpoint fully public. That is fine for genuinely public data and a serious hole for anything else. Decide, do not default.
Adds a custom recurrence interval for wp_schedule_event().
Fires
When WordPress builds its list of available schedules.
Signature
add_filter( 'cron_schedules', 'my_callback' ); // receives $schedulesExample
add_filter( 'cron_schedules', function ( $schedules ) {
$schedules['every_five_minutes'] = array(
'interval' => 300, // seconds
'display' => __( 'Every 5 minutes', 'textdomain' ),
);
return $schedules;
} );The gotcha
WP-Cron is not a real cron. It only fires when someone visits the site, so a low-traffic site will not run a five-minute schedule every five minutes. If the timing actually matters, disable WP-Cron (DISABLE_WP_CRON) and call wp-cron.php from a real system cron.
Runs immediately after a comment is saved.
Fires
On comment submission.
Signature
add_action( 'comment_post', 'my_callback', 10, 3 ); // receives $comment_id, $approved, $commentdataFilters comment data before it is saved. Used for spam checks and validation.
Fires
Before the comment is written.
Signature
add_filter( 'preprocess_comment', 'my_callback' ); // receives $commentdataThe gotcha
To reject a comment here, call wp_die() with a message — returning false does not stop it.
Runs on the order-received page after a successful checkout.
Fires
When the customer lands on the thank-you page.
Signature
add_action( 'woocommerce_thankyou', 'my_callback' ); // receives $order_idThe gotcha
This is NOT a reliable place to trigger fulfilment. It only fires if the customer actually reaches the thank-you page — if they close the tab after paying, it never runs. Use woocommerce_payment_complete or an order-status transition hook for anything that must happen.
Outputs markup immediately before the add-to-cart button on a product page.
Fires
While the product page renders.
Signature
add_action( 'woocommerce_before_add_to_cart_button', 'my_callback' );Adds, removes or reorders fields on the classic checkout.
Fires
When the checkout form is built.
Signature
add_filter( 'woocommerce_checkout_fields', 'my_callback' ); // receives $fieldsThe gotcha
This only affects the CLASSIC (shortcode) checkout. The newer block-based checkout ignores it entirely and is customised through Slot & Fill in JavaScript. This trips up almost everyone the first time.