wp-plugin-hooks — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited wp-plugin-hooks (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill is about hooks the plugin emits as its public extension surface — the actions and filters other developers wire into to modify or react to the plugin's behavior. Using core WP hooks (init, wp_enqueue_scripts, the_content, etc.) is basic WP and out of scope; designing your own hooks well is what separates a plugin people can extend from one they have to fork.
A custom hook is part of your plugin's API contract. Once a third party builds on it, breaking the signature in a minor release is a backwards-incompatibility bug — same as renaming a public PHP method.
Trigger when ANY of the following is true:
do_action or apply_filters call that other plugins / themes will hook into.do_action, apply_filters, apply_filters_deprecated, do_action_deprecated, or a docblock starting with Fires / Filters above a hook call.Both are events your code emits during execution. The semantic difference:
| Hook type | Question it answers | What listeners do | Return |
|---|---|---|---|
Action (do_action) | "This thing happened — anyone want to react?" | Side effects (log, send email, update meta, schedule cron). Don't return a value. | None |
Filter (apply_filters) | "I have this value. Anyone want to modify it before I use it?" | Take the value, optionally transform it, return it. | The (possibly modified) value |
Concrete examples:
// ACTION — "the user just submitted the form"; downstream side effects
do_action( 'myplugin/form_submitted', $form_id, $submission );
// Listeners: send Slack notification, log to audit table, dispatch webhook.
// FILTER — "here's the response message; anyone want to override?"
$message = apply_filters( 'myplugin/response_message', $default_message, $form_id );
// Listeners: replace the message based on form_id, append "(internal)" prefix.If listeners might want to MUTATE the data flow, it's a filter. If they want to REACT to an event, it's an action. When in doubt: would removing all listeners change the plugin's output? Yes → filter. No → action.
WP core mostly uses underscore_style names (pre_get_posts, rest_pre_serve_request). Slash-style names are common in plugin ecosystems for namespaced surfaces (myplugin/before_request), but they are not the dominant core convention. The hard rule: prefix with the plugin slug so collisions are impossible.
// Slash-separated - visually clear that this is plugin-namespaced
do_action( 'myplugin/before_request', $payload );
apply_filters( 'myplugin/api_response', $response, $request );
// Underscore-separated - matches WP core convention
do_action( 'myplugin_before_request', $payload );
apply_filters( 'myplugin_api_response', $response, $request );Pick one style and stay consistent across the plugin. Mixing myplugin/before_request and myplugin_after_request in the same plugin is confusing for documentation tools and developers searching the source.
The @since + @param block above each hook call is non-negotiable — IDE tooltips, source-search tools (hooks.wp.org, AI assistants suggesting hooks), and humans grepping for hooks all depend on it.
/**
* Fires before the AI request is sent.
*
* @since 1.2.0
*
* @param array $payload The request payload.
* @param string $context Reason for the request: 'verdict' or 'enrichment'.
*/
do_action( 'myplugin/before_request', $payload, $context );
/**
* Filters the AI response message that will be shown to the user.
*
* @since 1.0.0
* @since 1.3.0 Added the `$decision` parameter.
*
* @param string $message The response message.
* @param int $form_id ID of the form being processed.
* @param bool $decision The AI's TRUE/FALSE verdict.
*
* @return string Possibly modified message.
*/
$message = apply_filters( 'myplugin/response_message', $message, $form_id, $decision );Conventions:
@since line for each later parameter addition.Three rules govern parameters that age well:
For filters, the first arg is always the value being filtered. For actions and filters alike, arrange other args in order of likely use — listeners often only want one or two pieces of context. If the form ID is the most useful piece for filtering, put it second.
// Better — form_id is more often useful than the full submission array
apply_filters( 'myplugin/should_process', true, $form_id, $submission );
// Worse — listener has to accept submission they don't need to skip past
apply_filters( 'myplugin/should_process', true, $submission, $form_id );Listeners may only need the form ID; passing the whole Form object means every listener carries the full object even if they ignore it. When the work to fetch the object is cheap (small DB hit), pass the ID; when listeners always need the object anyway, pass it.
Beyond 4 args, listeners get unwieldy. If you find yourself wanting 5+ args, bundle them into an associative array:
// 6-arg action — listeners must accept all six in order
do_action( 'myplugin/render', $template, $context, $vars, $depth, $strict, $cache_key );
// Better — single context array, listeners pick what they need
do_action( 'myplugin/render', array(
'template' => $template,
'context' => $context,
'vars' => $vars,
'depth' => $depth,
'strict' => $strict,
'cache_key' => $cache_key,
) );The trade-off: array-as-arg loses static analysis benefits. For 2-3 strongly-typed args, prefer separate parameters; for 5+ heterogeneous fields, bundle.
do_action_ref_array / apply_filters_ref_array — when args are dynamicThe standard do_action( $hook, ...$args ) and apply_filters( $hook, $value, ...$args ) use variadic spread (PHP 5.6+). When you DON'T know the args at compile time — typically when proxying / forwarding hook calls — use the array variants:
$args = array( $payload, $context, $extra );
// Spread variant — only when args are statically known
do_action( 'myplugin/event', $payload, $context, $extra );
// Array variant — args are an array dynamically
do_action_ref_array( 'myplugin/event', $args );The naming _ref_array is historical. These functions accept the hook arguments as an array and pass that array to WP_Hook; they are primarily "args-as-array" variants. References only matter if the array elements themselves are references, so do not reach for these functions as a generic "make callback args mutable" tool. Verified in wp-includes/plugin.php apply_filters_ref_array / do_action_ref_array.
99% of plugin code uses the spread variants. Reach for the array variants only when forwarding (apply_filters_deprecated uses them internally for exactly this reason).
Once a hook is documented and shipped:
@since annotation; reordering or removing is a major version bump.int $form_id to string $form_slug is a breaking change.string shouldn't suddenly return string|null.Track public hooks in your plugin's documentation / README under a "Hooks" section. Treat them with the same rigor as the public methods of a class.
When you genuinely must change or remove a hook, deprecate, don't delete. WordPress provides apply_filters_deprecated and do_action_deprecated (wp-includes/plugin.php, since WP 4.6) that fire the hook for any remaining listeners AND emit a _deprecated_hook notice (wp-includes/functions.php).
// OLD HOOK (now deprecated): myplugin/old_response
// NEW HOOK: myplugin/response_message
// Step 1: emit BOTH hooks so existing listeners keep working.
$message = apply_filters( 'myplugin/response_message', $default, $form_id );
// Fire the deprecated hook with the same args; emits _deprecated_hook notice.
$message = apply_filters_deprecated(
'myplugin/old_response', // old hook name
array( $message, $form_id ), // args (as array)
'1.5.0', // version when deprecated
'myplugin/response_message', // replacement
'Use myplugin/response_message instead.' // optional message
);For actions:
do_action_deprecated(
'myplugin/old_event',
array( $payload ),
'1.5.0',
'myplugin/new_event'
);The deprecation helpers short-circuit when no listener is attached (has_filter / has_action returns false), so they're cheap when nobody's listening. The notice fires only when someone IS still using the old hook — exactly when you want them informed.
Deprecation policy: keep the deprecated hook for at least one major version. 1.5.0 deprecates → 2.0.0 removes. Document the migration in the changelog.
slash/style or underscore_style, stay consistent.Fires / Filters opening, @since, every @param typed and described, @return for filters.string returns string (or you've designed it badly). Listeners who break the type get to fix their callbacks; you don't change the contract on them.// WRONG — action used as a filter (return value lost)
$result = do_action( 'myplugin/transform', $value ); // do_action returns void
// WRONG — filter that doesn't return the value
add_filter( 'myplugin/response_message', function ( $message ) {
error_log( $message ); // side effect
// missing: return $message;
} );
// Other listeners receive the previous value or null; chain breaks.
// WRONG — bare hook name, collides with the world
do_action( 'before_save', $data );
// WRONG — adding a parameter in the middle, breaking existing listeners
// v1.0
apply_filters( 'myplugin/response_message', $msg, $form_id );
// v1.1
apply_filters( 'myplugin/response_message', $msg, $context, $form_id ); // 💥
// RIGHT — append at the end, document with @since
apply_filters( 'myplugin/response_message', $msg, $form_id, $context );
// WRONG — silent removal
// (the hook just stops firing, listeners get no warning)
// RIGHT — deprecate first
$message = apply_filters_deprecated(
'myplugin/old_response',
array( $message, $form_id ),
'1.5.0',
'myplugin/response_message'
);
// WRONG — undocumented hook
do_action( 'myplugin/before_request', $payload );
// IDE / hooks search tools / AI assistants can't find or describe this hook.@param line.init, the_content, save_post, etc.) — every WP plugin does this; not a custom-hook design topic.add_action( $hook, $cb, $priority )) — basic WP, not a design topic for the plugin emitter.remove_filter / remove_action — adjacent topic, separate skill.wp-plugin-architecture).'all' meta-hook (a debugging tool, not a design pattern).apply_filters / do_action: wp-includes/plugin.phpapply_filters_deprecated / do_action_deprecated: wp-includes/plugin.php (since WP 4.6)_deprecated_hook (the underlying notice trigger): wp-includes/functions.phpdid_action / did_filter: wp-includes/plugin.php — useful in tests / debugging to assert a hook fired.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.