When two WordPress plugins fight over the same hook

Got a call from a client running a decent-sized WooCommerce shop. They had a custom plugin we built that offered free shipping for VIP members. Simple enough. It worked for years. Then, they installed a new table-rate shipping plugin to handle complex international orders. And just like that, our VIP free shipping was gone. The new plugin was stomping all over our logic. Total mess.

This is a classic case of plugin warfare, and it almost always comes down to a misunderstanding of the WordPress Plugin API. That is the system of hooks, both actions and filters, that lets developers change how WordPress and other plugins behave. When two plugins try to change the same thing, say a shipping rate, you get chaos unless you know how to direct traffic.

The real problem is hook priority

When a developer wants to change something, they use add_filter(). Think of it as getting in line to inspect or adjust a piece of data before it is used. The catch is that most plugins join the default line, priority 10. When everything sits at the same priority, WordPress runs them in whatever order they loaded, which is unpredictable.

My first thought was the junior-dev fix: just make my plugin run last. I could set my filter’s priority to 999 and overwrite whatever the other plugin did. That would probably work, but it is a hack. What if the client wanted the table-rate plugin to add a special handling fee, and then my plugin made shipping free? My version would wipe out that handling fee. The better answer is to get the plugins to cooperate.

// The "brute force" way -- just run last
add_filter( 'woocommerce_package_rates', 'vip_shipping_fix', 999 );

// The RIGHT way -- run after the default, and modify intelligently
function smart_vip_shipping_fix( $rates ) {
    // Check if a VIP user is logged in
    if ( is_user_logged_in() && current_user_can('vip_member') ) {
        // Instead of replacing the rates, find "free shipping" if it exists
        // Or unset other rates, leaving only the free one.
        // This is much safer than just returning a whole new array.
        foreach ( $rates as $rate_id => $rate ) {
            if ( 'free_shipping' !== $rate->method_id ) {
                unset( $rates[ $rate_id ] );
            }
        }
    }
    return $rates;
}
add_filter( 'woocommerce_package_rates', 'smart_vip_shipping_fix', 20 );

So what’s the real takeaway?

The point is not to force your code to run first or last. It is to understand the execution order and write defensive code that cooperates with other plugins. You hook in, look at the data you are handed, and change it carefully. You do not throw out everything that came before. The WordPress Plugin API gives you a lot of control, but you have to use it precisely. For a deeper, more technical treatment, there is a post on carlalexander.ca worth reading.

This stuff gets complicated fast. If you are tired of debugging someone else’s mess and just want your site to work, get in touch with my team. We have probably seen your problem before.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.