WordPress remove_action and remove_filter: Safely Deregister Core and Plugin Hooks

WordPress’s hook system is additive by default — you add callbacks, and they run. But removing callbacks added by core WordPress, themes, or third-party plugins is equally important: disabling the emoji JavaScript on sites that don’t need it, unhooking a WooCommerce email notification you’ve replaced with a custom one, preventing a parent theme’s widget from loading on a child theme, or removing a plugin’s default admin notice. remove_action() and remove_filter() work identically — they remove a callback from a hook by matching the hook name, callable, and priority. The common failure modes are removing a hook too early (before the callback is registered), mismatching the priority, or trying to remove a closure (anonymous function) — which is impossible without a reference to the specific closure object. Understanding these edge cases is what separates reliable hook management from code that silently fails.

Problem: You want to remove WordPress's emoji scripts (loaded by default since WP 4.2), unhook a parent theme's custom header function, and disable a WooCommerce email you've replaced — without modifying core or plugin files.

Solution: Use remove_action() on the same hook the callback was registered on, matching the exact priority. For object method callbacks, get the object instance first. For closures, there is no workaround — restructure to use named functions instead.

<?php
// ── Remove WordPress emoji scripts (registered on 'init' at priority 10) ──
add_action( 'init', function () {
    remove_action( 'wp_head',             'print_emoji_detection_script', 7 );
    remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
    remove_action( 'wp_print_styles',     'print_emoji_styles' );
    remove_action( 'admin_print_styles',  'print_emoji_styles' );
    remove_filter( 'the_content_feed',    'wp_staticize_emoji' );
    remove_filter( 'comment_text_rss',    'wp_staticize_emoji' );
    remove_filter( 'wp_mail',             'wp_staticize_emoji_for_email' );
} );

// ── Remove a callback added by a parent theme (named function) ─────────
// Must run AFTER the parent theme registers the callback — e.g. after_setup_theme priority 11
add_action( 'after_setup_theme', function () {
    remove_action( 'wp_head', 'parent_theme_custom_header_output', 20 );
}, 11 );

// ── Remove a callback from a plugin class instance ─────────────────────
// The class registers via: add_action('wp_footer', [ $this, 'render_popup' ], 10)
// You need to find the registered instance:
add_action( 'wp_footer', function () {
    global $my_plugin_instance; // if the plugin exposes its instance as a global
    if ( isset( $my_plugin_instance ) ) {
        remove_action( 'wp_footer', [ $my_plugin_instance, 'render_popup' ], 10 );
    }
}, 5 ); // run at priority 5 — BEFORE the popup renders at priority 10

// ── Disable a WooCommerce email class ──────────────────────────────────
add_filter( 'woocommerce_email_classes', function ( $email_classes ) {
    unset( $email_classes['WC_Email_New_Order'] ); // remove new-order admin email
    return $email_classes;
} );

// ── IMPORTANT: Timing — remove_action fails if run before add_action ───
// BAD: registering removal on 'init' when plugin adds callback on 'init' at same priority
// GOOD: always hook your removal callback AFTER the target is registered
add_action( 'init', 'remove_late_callback', 20 ); // priority 20 > plugin's priority 10
function remove_late_callback() {
    remove_action( 'wp_head', 'plugin_added_this_at_init_10' );
}

NOTE: You cannot remove a closure (anonymous function) registered by a plugin: remove_action('hook', function(){...}) always fails because PHP creates a new closure object each time — it is not the same object as the one that was registered. If you maintain a plugin and want your callbacks to be removable by others, always use named functions or expose the object instance. To remove callbacks registered by inaccessible objects inside a plugin, use a workaround: iterate $wp_filter['hook_name'] to find and remove callbacks by callable signature.