Skip to content

WordPress Hooks Explained: How Plugins and Themes Extend Core Without Editing It

WordPress plugins and themes change what gets displayed and how data gets processed without ever touching WordPress core’s own files (the code under wp-includes / wp-admin). What makes that possible is a mechanism called hooks, which come in two flavors: actions and filters. This blog’s own theme, wpmm-blog, uses both extensively in its functions.php, so this article walks through real code from it.

Note: a hook is a point WordPress core deliberately builds in for outside code to plug into. At specific points in core’s own source, calls like do_action() and apply_filters() are already embedded. Plugins and themes “hook” their own functions onto those points to intercept what core does there, without editing core itself.

What separates an action from a filter

The two hook types split cleanly along one line: whether they return a value.

  • Action: runs some code at a given moment. It doesn’t need to return anything — it’s for side effects (writing a log, saving data, making an outbound request)
  • Filter: receives a value core already has, transforms it, and hands it back. It’s for changing data right before core uses or displays it

In short: an action interrupts a moment to do something; a filter rewrites a value and returns it. Once you know which of the two your code needs to do, whether to register it with add_action() or add_filter() follows naturally.

A filter example: excerpt length and the “read more” marker

WordPress core truncates the excerpt shown on archive pages to 55 words by default, appending […] at the end. Changing that behavior doesn’t require touching core’s source — it just means registering functions on two filter hooks, excerpt_length and excerpt_more.

if (!function_exists('wpmm_blog_excerpt_length')) {
    function wpmm_blog_excerpt_length($length) {
        // Japanese content is measured in "words" here too, but 60
        // reads as a reasonable length even with full-width characters
        return 60;
    }
}
add_filter('excerpt_length', 'wpmm_blog_excerpt_length');

if (!function_exists('wpmm_blog_excerpt_more')) {
    function wpmm_blog_excerpt_more($more) {
        return ' …';
    }
}
add_filter('excerpt_more', 'wpmm_blog_excerpt_more');

The key detail: each function receives core’s default value as an argument and returns a replacement. wpmm_blog_excerpt_length($length) gets core’s default (55) as $length, but this implementation ignores it and always returns a fixed 60. Core’s internal logic takes whatever the function returns and uses that as the excerpt length going forward.

An action example: pre-generating an OGP image on save

For an action example, this blog pre-generates and caches a post’s OGP (Open Graph) image at the moment a post is saved or published, hooked onto the save_post action.

add_action('save_post', '_wpmm_blog_pregenerate_ogp_on_publish', 10, 3);

add_action() takes, in order: the hook name, the callback function, a priority, and the number of arguments the callback expects. This call uses priority 10 (the default) and requests 3 arguments.

save_post is capable of passing three arguments to its callbacks — $post_ID, $post (the post object), and $update (a boolean for whether this was an update vs. a new post) — but that capability only applies if the callback explicitly asks for that many; by default only the first is passed. The 3 at the end of this call exists specifically so the callback can use all three.

Why priority exists

A widely-used hook like save_post often has more than one plugin or theme registering a callback on it simultaneously, alongside core’s own internal use of the same hook. When every callback shares the same default priority (10), they run in registration order — but real requirements sometimes call for “run after this other callback” or “run before everything else,” and priority is the explicit numeric knob for that. Lower numbers run earlier; higher numbers run later.

Why this means core never has to be edited directly

WordPress core’s own source has do_action('hook-name', ...) (firing an action) and apply_filters('hook-name', $value, ...) (applying a filter) calls built into it at deliberate points throughout. Plugins and themes register callbacks against those pre-built entry points with add_action() / add_filter() — they never modify core’s files at all.

That has a very concrete practical payoff. When WordPress core is updated, automatically or manually, its files get replaced wholesale with the new version’s. Any customization made by editing core files directly would be silently wiped out on the next update. Customization made through hooks lives in its own file, under wp-content/themes/ or wp-content/plugins/, entirely separate from core — so it survives core updates untouched.

Summary

Type Purpose Return value Example from this blog
Action Run code at a given moment None (side effects only) save_post → pre-generate the OGP image
Filter Transform a value core hands it The transformed value excerpt_length → change the excerpt word count to 60

Actions and filters, together, are the entire mechanism that lets WordPress core, plugins, and themes cooperate without any of the three ever editing another’s source. When reading unfamiliar WordPress code, checking just two things — is this add_action or add_filter, and which hook name is it registered on — is usually enough to figure out exactly where in core’s flow it’s intercepting, and how.