Hooks are how WordPress lets you change its behaviour without editing core, theme or plugin files. Almost everything in themes and plugins is built on them. There are two kinds: actions (do something at a certain moment) and filters (change a value before WordPress uses it).

Actions: run code at a point in time

add_action( 'wp_footer', function () {
    echo '<!-- served by web1 -->';
} );

WordPress calls do_action( 'wp_footer' ) while rendering the page footer, and every function attached with add_action( 'wp_footer', … ) runs at that moment. Actions don’t return anything.

Filters: modify a value

add_filter( 'excerpt_length', function ( $length ) {
    return 30;   // words
} );

WordPress calls apply_filters( 'excerpt_length', 55 ), passes the value through every attached function, and uses the result. A filter must always return a value. Forgetting return is the most common bug, and silently empties the value.

Priority and arguments

add_filter( 'the_content', 'add_cta_box', 20, 1 );

function add_cta_box( $content ) {
    if ( is_singular( 'post' ) && in_the_loop() && is_main_query() ) {
        $content .= '<div class="cta">Need a server? <a href="/usa/">See plans</a></div>';
    }
    return $content;
}
  • Priority (default 10): lower numbers run earlier. Use a higher number to run after other plugins.
  • Accepted arguments (default 1): some hooks pass several values. Raise this number to receive them:
add_action( 'save_post', function ( $post_id, $post, $update ) {
    // …
}, 10, 3 );

Commonly used hooks

Hook Type Use
init action Register post types, taxonomies, shortcodes
wp_enqueue_scripts action Load CSS/JS on the front end
admin_enqueue_scripts action Load CSS/JS in wp-admin
wp_head / wp_footer action Output in the page head or footer
save_post action React when a post is saved
template_redirect action Redirects or access control before output
the_content filter Modify post content
the_title filter Modify titles
body_class filter Add CSS classes to <body>
wp_mail filter Change outgoing email parameters
upload_mimes filter Allow extra file types
rest_api_init action Register REST routes

Removing someone else’s hook

To undo what a theme or plugin does, remove its callback with the same priority it was added with:

add_action( 'init', function () {
    remove_action( 'wp_head', 'wp_generator' );           // hide WordPress version
    remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
} );

For class methods, you need the object instance (remove_action( 'hook', [ $instance, 'method' ] )), which some plugins expose through a global or a singleton.

Create your own hooks

If you write a plugin or theme, add hooks so others can extend it:

do_action( 'mysite_before_pricing_table', $plan_group );
$columns = apply_filters( 'mysite_pricing_columns', [ 'cpu', 'ram', 'disk' ], $plan_group );

Prefix hook names to avoid collisions.

Where to put hook code

  • A site-specific plugin (wp-content/plugins/site-tweaks/site-tweaks.php, or a must-use plugin in wp-content/mu-plugins/) for functionality that should survive theme changes.
  • The child theme’s functions.php for presentation tweaks tied to that theme. See WordPress child theme.
  • Avoid “code snippets” in the database for anything important. Version control is better. See WordPress Git deployment.

Debugging hooks

  • Query Monitor shows hooks fired on a page, and which callbacks are attached (Hooks & Actions panel).
  • Check what’s attached in code:
global $wp_filter;
error_log( print_r( array_keys( $wp_filter['the_content']->callbacks ), true ) );
  • Use did_action( 'init' ) to check whether an action has already run (adding a callback after the hook has fired does nothing).

See WordPress debug mode.

Performance notes

  • Hooks that run on every request (init, wp_head, the_content on archive pages) should be cheap. Avoid remote HTTP calls or heavy queries there.
  • Cache expensive results with transients or the object cache. See Redis object cache.

Quick reference

Task Code
Run code at a moment add_action( 'hook', $callback, $priority, $args )
Change a value add_filter( 'hook', $callback ), and return it
Remove a callback remove_action/remove_filter( 'hook', $callback, $priority )
Fire your own do_action() / apply_filters()
Inspect Query Monitor → Hooks

Related: WordPress custom post types and WordPress plugin conflicts.

  • WordPress
  • Development
  • PHP