Every WordPress plugin does one of two things: it runs code at a specific moment (action), or it modifies a value before it's used (filter). That's it. The entire WordPress plugin ecosystem is built on this distinction.
Understanding the difference — and when to use each — is what separates plugins that work from plugins that cause problems.
Actions: do something at a point in time
An action hook is a signal that WordPress (or another plugin) sends at a specific moment. You attach a function to that signal, and your function runs when the signal fires.
add_action( 'wp_enqueue_scripts', 'my_plugin_load_assets' );
function my_plugin_load_assets() {
wp_enqueue_style( 'my-plugin', plugin_dir_url( __FILE__ ) . 'style.css' );
wp_enqueue_script( 'my-plugin', plugin_dir_url( __FILE__ ) . 'script.js', ['jquery'], '1.0', true );
}
wp_enqueue_scripts fires when WordPress is preparing to load assets for the front end. Hooking into it is the correct way to load CSS and JS — not dropping <script> tags directly into templates.
Common action hooks you'll use constantly:
init— runs early, good for registering post types and taxonomieswp_enqueue_scripts— load front-end assetsadmin_enqueue_scripts— load admin assetssave_post— runs when a post is saved, good for processing custom fieldswp_ajax_{action}— handle AJAX requests from authenticated userswp_ajax_nopriv_{action}— handle AJAX from non-logged-in users
Filters: modify a value before it's used
A filter hook gives you a value, lets you modify it, and expects you to return the modified version. WordPress then uses your modified value instead of the original.
add_filter( 'the_content', 'my_plugin_append_cta' );
function my_plugin_append_cta( $content ) {
if ( ! is_single() ) {
return $content;
}
$cta = '<div class="post-cta">Ready to try NoDevZone? <a href="#">Get started free →</a></div>';
return $content . $cta;
}
The critical rule: always return something from a filter. If you forget the return, the value becomes null and you've just broken whatever was using it.
Common filter hooks:
the_content— the post content before displaythe_title— the post titlewp_nav_menu_items— navigation menu HTMLupload_mimes— allowed file upload typesplugin_action_links_{plugin-file}— links shown on the plugins list page
How NoDevZone AI uses hooks
When the Plugin Builder generates a plugin, it structures the code around a class with methods attached to hooks in the constructor. This pattern avoids global function name collisions and keeps the code organized.
class My_Plugin {
public function __construct() {
add_action( 'init', [ $this, 'register_post_type' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'load_assets' ] );
add_filter( 'the_content', [ $this, 'append_content' ] );
}
public function register_post_type() { /* ... */ }
public function load_assets() { /* ... */ }
public function append_content( $content ) {
// always return
return $content;
}
}
new My_Plugin();
The security scan specifically checks that AJAX handlers include both a nonce check and a capability check before doing anything:
add_action( 'wp_ajax_my_action', [ $this, 'handle_ajax' ] );
public function handle_ajax() {
// Security: verify nonce
check_ajax_referer( 'my_nonce_action', 'nonce' );
// Security: verify permissions
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Insufficient permissions', 403 );
}
// Now safe to process
$data = sanitize_text_field( $_POST['data'] ?? '' );
wp_send_json_success( [ 'result' => $data ] );
}
Missing either of these is flagged as a security issue and blocks the plugin from being released.
Priority and the hook queue
When multiple functions are attached to the same hook, they run in order of priority (lower number = runs first). Default priority is 10.
add_action( 'init', 'runs_first', 5 );
add_action( 'init', 'runs_second', 10 ); // default
add_action( 'init', 'runs_last', 20 );
This matters when your plugin needs to run before or after another plugin's code. If you're modifying something that another plugin also modifies, priority controls who wins.
A common pattern: if you're overriding a theme's filter, use priority 20 or higher to make sure you run after the theme's code.
Removing hooks
You can remove a hook that someone else added — including theme and plugin hooks:
remove_action( 'wp_head', 'wp_generator' ); // removes WP version from <head>
remove_filter( 'the_content', 'wpautop' ); // removes automatic paragraph wrapping
The catch: you can only remove a hook if you call remove_action/remove_filter after the original add_action/add_filter has run. This usually means hooking your removal into a later action.