If WordPress were a school, hooks would be the bell system. At specific moments, WordPress rings a bell (“the page is loading,” “a post was saved,” “the content is about to display”), and any theme or plugin that signed up for that bell gets a chance to act. Hooks are how WordPress stays customizable without anyone editing core files.
By the end of this lesson you will understand the two kinds of hooks, read their signatures with confidence, and write your own safely.
What Are Hooks?
A hook is a named point in WordPress code where extra functions (called callbacks) can run. There are two types:
- Actions let you do something at a certain moment: output HTML, send an email, save data, enqueue a script.
- Filters let you change something before WordPress uses it: modify text, adjust a query argument, tweak a setting. A filter receives a value and must return a value.
Professor’s note: An easy way to remember it: actions are “do,” filters are “change and hand back.”
Actions in Detail
You attach a callback to an action with add_action(). Its signature is:
add_action( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 );
- $hook_name — the action to hook into, such as wp_footer or init.
- $callback — the function to run.
- $priority — lower numbers run earlier; the default is 10.
- $accepted_args — how many arguments your callback receives; the default is 1.
Example: add something to the footer
// Add a small notice to the footer on the front end.
function wpp_footer_notice() {
echo '<p class="wpp-footer-note">' . esc_html__( 'Thanks for visiting!', 'wpp' ) . '</p>';
}
add_action( 'wp_footer', 'wpp_footer_notice' );
Notice the function name starts with a prefix (wpp_) to avoid clashing with other plugins, and the output is escaped with esc_html__().
Example: using multiple arguments
Some actions pass several values. The save_post action passes the post ID, the post object, and whether it is an update. To receive all three, set $accepted_args to 3:
// Runs whenever a post is saved. Priority 10, accepts 3 arguments.
function wpp_on_save_post( $post_id, $post, $update ) {
// Skip autosaves and revisions.
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( 'post' !== $post->post_type ) {
return;
}
update_post_meta( $post_id, '_wpp_last_saved', current_time( 'mysql' ) );
}
add_action( 'save_post', 'wpp_on_save_post', 10, 3 );
Filters in Detail
Filters use add_filter(), which has the same signature as add_action():
add_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 );
The big difference is what your callback does: it receives a value, may change it, and must return it.
Example: customize excerpts
// Change the excerpt length to 30 words.
function wpp_excerpt_length( $length ) {
return 30;
}
add_filter( 'excerpt_length', 'wpp_excerpt_length' );
// Change the "read more" text after excerpts.
function wpp_excerpt_more( $more ) {
return '… ' . esc_html__( 'Keep reading', 'wpp' );
}
add_filter( 'excerpt_more', 'wpp_excerpt_more' );
Heads up: The most common filter bug is forgetting to return a value. If a the_content filter returns nothing, every post on your site suddenly appears blank. Always return something, even if it is the original value unchanged.
Creating Your Own Hooks
Hooks are not just for WordPress core. When you build a theme or plugin, you can add your own so others can extend your code without editing it.
- do_action( ‘hook_name’, $args… ) fires a custom action.
- apply_filters( ‘hook_name’, $value, $args… ) passes a value through any attached filters and returns the result.
// In your plugin: let others change the greeting, and add a place to hook in.
function wpp_render_greeting() {
$greeting = apply_filters( 'wpp_greeting_text', __( 'Welcome to class!', 'wpp' ) );
do_action( 'wpp_before_greeting' );
echo '<p class="wpp-greeting">' . esc_html( $greeting ) . '</p>';
}
// Somewhere else (a child theme or another plugin):
function wpp_custom_greeting( $text ) {
return __( 'Good morning, students!', 'wpp' );
}
add_filter( 'wpp_greeting_text', 'wpp_custom_greeting' );
This is exactly how popular plugins let developers customize them. Prefix your custom hook names (wpp_greeting_text) just like your functions.
Priorities and Removing Hooks
When several callbacks share a hook, priority decides the order. Callbacks with the same priority run in the order they were added. If you need to run after a plugin’s callback, use a higher number such as 20.
You can also detach callbacks with remove_action() or remove_filter(). The function name and priority must match exactly, and the removal must happen after the callback was added:
// Remove a callback. Priority must match the one used when it was added.
function wpp_remove_footer_notice() {
remove_action( 'wp_footer', 'wpp_footer_notice', 10 );
}
add_action( 'init', 'wpp_remove_footer_notice' );
Tip: To see which callbacks are attached to hooks on a page, install the free Query Monitor plugin on a staging site. Its Hooks & Actions panel is a fantastic learning tool.
Where to Put Hook Code
- A small custom plugin is the best home for site-specific functionality, because it survives theme changes.
- A child theme’s functions.php works for design-related tweaks tied to that theme.
- A code snippets plugin such as Code Snippets or WPCode is handy for beginners.
- Never edit WordPress core or a parent theme directly; updates will overwrite your changes.
Test every snippet on a staging site first, with a backup ready. Find more developer lessons in our web development hub, browse useful plugins, and if you want AI help writing hook code, see our vibe coding guide.
Homework
- Add the footer action example to a staging site and confirm it appears.
- Write a filter that changes the excerpt length, and remember to return the value.
- Install Query Monitor and find three hooks that fire on your homepage.
- Bonus: add a custom filter to a snippet of your own and change its output from another function.