Removing Hooks

remove_action() and remove_filter() unhook a callback, but only if the callback identity and the priority both match exactly.

Syntaxremove_action( string $hook_name, callable|string|array $callback, int $priority = 10 ): bool

remove_filter() and its alias remove_action() detach a registered callback. Both return bool: true if something was removed, false if nothing matched. That return value is the only feedback you get, and ignoring it is why removals fail quietly.

Three things must line up

The priority defaults to 10, so removing a callback registered at 8 or 20 without passing that number does nothing. The callback identity must produce the same internal key that registration did. The timing must be after the callback was added and before the hook fires — a removal written at the top of functions.php often runs before the plugin that added the hook has even loaded.

Why closures are hard to remove

_wp_filter_build_unique_id() keys plain function names on the name, and static methods on Class::method — so 'Acme::run' and array( 'Acme', 'run' ) are interchangeable. Object methods key on spl_object_hash( $object ) . $method, meaning you need the exact same instance, not a new one of the same class. Closures are objects too, so removing one requires the original Closure. If a plugin registered an anonymous closure and kept no reference, it cannot be removed cleanly.

Example

Example · php
<?php
// Run on init: late enough that core and every plugin have registered their hooks,
// early enough that the_content and wp_head have not fired yet.
add_action( 'init', 'acme_unhook_core' );
function acme_unhook_core() {
	// Plain function at the default priority 10 — the easy case.
	remove_action( 'wp_head', 'wp_generator' );
	remove_filter( 'the_content', 'wpautop' );

	// Registered at 7, so 7 must be passed or nothing happens.
	remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
	remove_filter( 'the_content_feed', 'wp_staticize_emoji' ); // this one is at 10

	// Object method: WP_Embed registers array( $this, 'autoembed' ) at priority 8.
	// Core keeps the instance in a global, so the same object is reachable.
	remove_filter( 'the_content', array( $GLOBALS['wp_embed'], 'autoembed' ), 8 );

	// Always check: false means nothing matched.
	if ( ! remove_filter( 'the_content', 'do_shortcode', 11 ) ) {
		error_log( 'do_shortcode was not registered where expected.' );
	}
}

When to use it

  • Strip core's generator meta tag or emoji script from the front end.
  • Disable a parent theme's filter from a child theme's functions.php.
  • Turn off another plugin's the_content filter on one custom post type only.
  • Temporarily unhook a callback around a programmatic wp_update_post() call.

More examples

Removing a parent theme's hook from a child theme

The single most common removal failure. WordPress includes the child theme's functions.php before the parent's, so any removal written at file scope in a child theme runs too early. Wrap it in after_setup_theme, or in init if the parent registers its hooks even later.

Example · php
// Parent theme functions.php:
//   add_action( 'wp_enqueue_scripts', 'parenttheme_fonts', 20 );

// Child theme functions.php — BROKEN. The child's functions.php is included
// BEFORE the parent's, so at this moment the hook has not been added yet.
remove_action( 'wp_enqueue_scripts', 'parenttheme_fonts', 20 );

// Child theme functions.php — CORRECT. after_setup_theme fires once both
// functions.php files have been included.
add_action( 'after_setup_theme', function () {
	remove_action( 'wp_enqueue_scripts', 'parenttheme_fonts', 20 ); // priority must match
} );

Object methods, closures, and what you can do about them

Shows both why a removal fails on object methods and the escape hatches. remove_all_filters( $hook, $priority ) is the blunt instrument for closures you cannot name; without the second argument it clears every priority, which will break core behaviour, so always pass one and always scope it to a specific request.

Example · php
class Acme_Banner {
	public static $instance;

	public function __construct() {
		self::$instance = $this;                                   // expose the instance...
		add_filter( 'the_content', array( $this, 'append' ), 20 );
	}

	public function append( $content ) {
		return $content . '<p>Banner</p>';
	}
}

// Removable, because the instance is reachable:
remove_filter( 'the_content', array( Acme_Banner::$instance, 'append' ), 20 );

// NOT removable: a new instance has a different spl_object_hash().
remove_filter( 'the_content', array( new Acme_Banner(), 'append' ), 20 ); // returns false

// Static methods key on 'Class::method', so these two are the same removal:
remove_action( 'shutdown', 'Acme_Banner::flush' );
remove_action( 'shutdown', array( 'Acme_Banner', 'flush' ) );

// Last resort when a closure blocks you — this removes EVERYTHING on the hook,
// including core's callbacks, so scope it tightly.
add_action( 'template_redirect', function () {
	if ( is_singular( 'acme_doc' ) ) {
		remove_all_filters( 'the_content', 20 ); // only priority 20
	}
} );

Unhooking around a programmatic save

The standard guard for any save_post handler that writes back to the same post. Note that the re-add must repeat the priority and the accepted_args exactly; forgetting the trailing 2 leaves the callback registered but fataling with ArgumentCountError on the next save.

Example · php
// Prevents a save_post callback recursing when you update the post from inside it.
add_action( 'save_post_acme_doc', 'acme_stamp_slug', 10, 2 );
function acme_stamp_slug( $post_id, $post ) {
	if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
		return;
	}

	// wp_update_post() fires save_post again, which would call this function forever.
	remove_action( 'save_post_acme_doc', 'acme_stamp_slug', 10 );

	wp_update_post(
		array(
			'ID'        => $post_id,
			'post_name' => sanitize_title( $post->post_title . '-' . $post_id ),
		)
	);

	// Re-add with the identical priority and accepted_args, or later saves break.
	add_action( 'save_post_acme_doc', 'acme_stamp_slug', 10, 2 );
}

Discussion

  • Be the first to comment on this lesson.