wp-plugin-hooks

Design custom action/filter hooks emitted by a plugin: action vs filter semantics, prefixed names, docblocks, parameter stability, *_ref_array forwarding, and deprecated hook migration. Use when adding, reviewing, evolving, or deprecating a public hook surface. Triggers on do_action, apply_filters, apply_filters_deprecated, do_action_deprecated, apply_filters_ref_array, do_action_ref_array, did_action, did_filter, or hook @since docblocks.

Best use case

wp-plugin-hooks is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Design custom action/filter hooks emitted by a plugin: action vs filter semantics, prefixed names, docblocks, parameter stability, *_ref_array forwarding, and deprecated hook migration. Use when adding, reviewing, evolving, or deprecating a public hook surface. Triggers on do_action, apply_filters, apply_filters_deprecated, do_action_deprecated, apply_filters_ref_array, do_action_ref_array, did_action, did_filter, or hook @since docblocks.

Teams using wp-plugin-hooks should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/wp-plugin-hooks/SKILL.md --create-dirs "https://raw.githubusercontent.com/nvdigitalsolutions/mcp-ai-wpoos/main/.agents/skills/wp-plugin-hooks/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/wp-plugin-hooks/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How wp-plugin-hooks Compares

Feature / Agentwp-plugin-hooksStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Design custom action/filter hooks emitted by a plugin: action vs filter semantics, prefixed names, docblocks, parameter stability, *_ref_array forwarding, and deprecated hook migration. Use when adding, reviewing, evolving, or deprecating a public hook surface. Triggers on do_action, apply_filters, apply_filters_deprecated, do_action_deprecated, apply_filters_ref_array, do_action_ref_array, did_action, did_filter, or hook @since docblocks.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# WordPress plugin: custom hooks (the ones YOU emit)

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.

## When to use this skill

Trigger when ANY of the following is true:

- Adding a `do_action` or `apply_filters` call that other plugins / themes will hook into.
- Reviewing a PR that introduces or modifies a custom hook.
- Evolving a hook signature across plugin versions (adding parameters, renaming, deprecating).
- Removing or renaming an existing hook — read the deprecation section before doing this.
- The diff contains: `do_action`, `apply_filters`, `apply_filters_deprecated`, `do_action_deprecated`, or a docblock starting with `Fires` / `Filters` above a hook call.

## Action vs filter — pick by semantics

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:

```php
// 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.

## Naming — prefix everything, pick one separator

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.

```php
// 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.

## Document every hook with a docblock

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.

```php
/**
 * 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:

- **`Fires` for actions, `Filters` for filters** as the opening verb in the description (matches WP core docblocks; documentation generators grep for it).
- **`@since X.Y.Z`** for the version when the hook first appeared, plus a separate `@since` line for each later parameter addition.
- **Every parameter documented** with type and meaning. The first arg in a filter is always "the value being filtered".
- **For filters: `@return`** describes the return type — same shape as the input value (filters MUST preserve type).

## Parameter design

Three rules govern parameters that age well:

### 1. Order: most-likely-to-be-modified first

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.

```php
// 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 );
```

### 2. Pass IDs and primitives, not heavy objects when avoidable

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.

### 3. Keep the parameter count below 4

Beyond 4 args, listeners get unwieldy. If you find yourself wanting 5+ args, bundle them into an associative array:

```php
// 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 dynamic

The 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:

```php
$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](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).

## The stability promise

Once a hook is documented and shipped:

- **Don't change parameter count or order** in minor / patch releases. Adding a NEW parameter at the END is OK with proper `@since` annotation; reordering or removing is a major version bump.
- **Don't change parameter types.** Going from `int $form_id` to `string $form_slug` is a breaking change.
- **Don't change return-type semantics for filters.** A filter that returned `string` shouldn't suddenly return `string|null`.
- **Don't move the hook to a different code path** that significantly changes timing. Listeners may rely on "this fires before X happens".

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.

## Deprecation pathway

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](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](wp-includes/functions.php)).

```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:

```php
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.

## Critical rules

- **Action for events, filter for value transformations.** Don't use a filter for side effects or an action for "let me modify this".
- **Prefix every custom hook** with the plugin slug. Pick `slash/style` or `underscore_style`, stay consistent.
- **Docblock every hook** — `Fires` / `Filters` opening, `@since`, every `@param` typed and described, `@return` for filters.
- **Filters MUST preserve type.** A filter receiving `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.
- **Parameter contract is your API.** No reordering, no type changes, no removals in non-major releases.
- **Deprecate via `apply_filters_deprecated` / `do_action_deprecated`**, never silent-delete a public hook.
- **Bundle 5+ args into an array** instead of growing the parameter list.

## Common mistakes

```php
// 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.
```

## Cross-references

- Run **`wp-plugin-architecture`** — the hook-naming convention is part of broader plugin architecture. Schema/Constants centralization includes hook names for discoverability.
- Run **`wp-i18n-audit`** if any of your hooks pass translatable strings as args — translation timing rules apply to the values, not the hook names.
- Run **`wp-security-audit`** when your hook callback receives user input that downstream listeners will trust — document the sanitization expectation in the `@param` line.

## What this skill does NOT cover

- Using core WP hooks (`init`, `the_content`, `save_post`, etc.) — every WP plugin does this; not a custom-hook design topic.
- Hook priority gymnastics (`add_action( $hook, $cb, $priority )`) — basic WP, not a design topic for the plugin emitter.
- Removing core WP behavior via `remove_filter` / `remove_action` — adjacent topic, separate skill.
- Internal-only "hooks" used as a poor-man's event bus inside a single plugin (use proper service classes / observers instead — see `wp-plugin-architecture`).
- The `'all'` meta-hook (a debugging tool, not a design pattern).

## References

- Plugins Hooks Handbook: [developer.wordpress.org/plugins/hooks/](https://developer.wordpress.org/plugins/hooks/)
- `apply_filters` / `do_action`: [wp-includes/plugin.php](wp-includes/plugin.php)
- `apply_filters_deprecated` / `do_action_deprecated`: [wp-includes/plugin.php](wp-includes/plugin.php) (since WP 4.6)
- `_deprecated_hook` (the underlying notice trigger): [wp-includes/functions.php](wp-includes/functions.php)
- `did_action` / `did_filter`: [wp-includes/plugin.php](wp-includes/plugin.php) — useful in tests / debugging to assert a hook fired.

Related Skills

wcs-subscription-hooks

5
from nvdigitalsolutions/mcp-ai-wpoos

Curated WooCommerce Subscriptions hook and extension-point map for choosing the right action/filter around WC_Subscription creation, status transitions, date changes, renewal orders, scheduled payments, payment retries, gateway events, switching, gifting, related orders, REST/API responses, and account/admin UI. Use when the user asks for a Woo Subscriptions hook list, "where should I hook", "after renewal", "subscription status changed", or when code contains WC_Subscription, wcs_create_subscription, wcs_create_renewal_order, woocommerce_scheduled_subscription_payment, wcs_renewal_order_created, payment_retry, wcs_get_subscriptions, wcsg_, subscription_switch, _subscription_switch_data, _recipient_user, wcsg_recipient, or subscription switching.

wcm-membership-hooks

5
from nvdigitalsolutions/mcp-ai-wpoos

Curated WooCommerce Memberships hook and extension-point map for plugins that build on user memberships, membership plans, status transitions, purchase/free-signup grants, profile fields, REST API, webhooks, members-area templates, CSV import/export, and Woo Subscriptions-linked memberships. Use when the user asks for a Memberships hook list, "where should I hook", membership created/saved, status changed, grant access from order, user membership API, profile fields, member directory, or code contains wc_memberships_, WC_Memberships_User_Membership, WC_Memberships_Membership_Plan, wc_user_membership, wc_membership_plan, wcm-, _subscription_id, wc_memberships_rules, or woocommerce-memberships.

wp-plugin-rewrite-rules

5
from nvdigitalsolutions/mcp-ai-wpoos

Design and review custom WordPress URL rewrites: add_rewrite_rule, add_rewrite_tag, query_vars, CPT/taxonomy rewrite slugs, add_rewrite_endpoint, soft vs hard flushes, rewrite_rules cache behavior, and the rule that flush_rewrite_rules() must not run on every request. Use for custom pretty URLs, CPT permalink 404s, endpoint rewrites, and code containing flush_rewrite_rules or add_rewrite_rule.

wp-plugin-presenter

5
from nvdigitalsolutions/mcp-ai-wpoos

Design and review native presenter classes in WordPress plugins without requiring better-data - converting DTOs or domain objects into REST arrays, admin table rows, JS config payloads, email variables, and public view models with allowlisted fields, context methods, redaction by default, locale/date/number formatting, no DTO mutation, and correct WordPress escaping boundaries. Use when adding FooPresenter, response mappers, admin-row arrays, wp_send_json payloads, rest_ensure_response data, wp_add_inline_script config, or when code returns raw DTOs, WP_Post, WC_Order, get_object_vars, json_encode, or unescaped HTML from controllers.

wp-plugin-options-storage

5
from nvdigitalsolutions/mcp-ai-wpoos

Picks the right WordPress storage primitive for plugin data: options, user/post/term/comment meta, transients, site options, site transients, or custom tables. Covers grouped settings, autoload management, transient TTL rules, serialized/JSON blob trade-offs, multisite storage caveats, and naming conventions. Use when scaffolding settings, choosing persistence for plugin-owned data, or auditing update_option/get_option/get_post_meta/set_transient/autoload usage.

wp-plugin-lifecycle

5
from nvdigitalsolutions/mcp-ai-wpoos

Designs and reviews the three lifecycle events of a WordPress plugin — activation (one-shot setup, dbDelta, add_option seeding, cron schedule, cap seeding), deactivation (reversible cleanup, cron clear via wp_unschedule_hook, never delete user data), and uninstall.php (standalone file, WP_UNINSTALL_PLUGIN guard, no autoloader, full data removal). Multisite-aware patterns using the $network_wide / $network_deactivating callback args, plus the recommendation against register_uninstall_hook in favor of uninstall.php. Use when scaffolding a plugin or debugging ghost cron events / orphan options. Triggers on register_activation_hook, register_deactivation_hook, uninstall.php, WP_UNINSTALL_PLUGIN, dbDelta, wp_unschedule_hook, switch_to_blog.

wp-plugin-dto

5
from nvdigitalsolutions/mcp-ai-wpoos

Design and review native DTOs in WordPress plugins without requiring better-data - immutable data carriers, explicit from_array hydration, strict coercion instead of unchecked casts, WP_Error validation failures, sensitive-field discipline, nested DTO arrays, and clear separation from repositories, WP models, presenters, REST controllers, and HTML views. Use when a plugin introduces FooDto, request DTOs, settings DTOs, value objects, admin-row data shapes, REST response source objects, or when reviewing code that passes raw arrays, stdClass, WP_Post, WC_Order, $_POST, post meta, or option arrays through multiple layers. Mentions better-data only as an optional higher-level library; this skill is for native implementations.

wp-plugin-cron

5
from nvdigitalsolutions/mcp-ai-wpoos

Designs and reviews scheduled/background work in WordPress plugins: wp_schedule_event, wp_schedule_single_event, cron_schedules, wp_next_scheduled guards, activation scheduling, deactivation cleanup, WP-Cron pseudo-cron timing, DISABLE_WP_CRON/system cron, multisite per-blog cron, idempotent callbacks, chunking, and Action Scheduler graduation. Use when adding scheduled jobs, debugging late/duplicate cron events, or deciding between WP cron and Action Scheduler.

wp-plugin-bootstrap

5
from nvdigitalsolutions/mcp-ai-wpoos

Scaffolds and reviews the main entry-point PHP file of a WordPress plugin — header (with Requires Plugins for WP 6.5+), ABSPATH guard, file/path/url/version constants, Composer + PSR-4 with manual spl_autoload_register fallback, register_activation_hook requirements check, Plugin class instantiation on plugins_loaded, and the WP 6.7+ rule that translation functions must not trigger before after_setup_theme. Use when scaffolding a new plugin or reviewing its main file. Triggers on Plugin Name headers, register_activation_hook, Requires Plugins, spl_autoload_register, plugins_loaded, or composer.json at the plugin root.

wp-plugin-assets-loading

5
from nvdigitalsolutions/mcp-ai-wpoos

Register and enqueue WordPress plugin scripts/styles with modern loading behavior, especially WP 6.9 fetchpriority support, script module args, footer placement, inline style limits, and removal of legacy IE conditional asset support. Covers wp_enqueue_script args strategy/in_footer/fetchpriority, wp_register_script_module / wp_enqueue_script_module args, wp_script_add_data, wp_style_add_data, dependency handles, conditional enqueueing on the right hook, and avoiding global frontend/admin asset bloat. Use when adding or reviewing plugin JS/CSS enqueue code.

wp-plugin-architecture

5
from nvdigitalsolutions/mcp-ai-wpoos

Designs and reviews the internal architecture of a WordPress plugin — folder layout, PSR-4 one-class-per-file discipline, Schema/Constants placement, composition-root vs singleton decisions, conditional asset enqueueing, script config via wp_add_inline_script, and prefixed custom-hook naming. Use when scaffolding includes/src, reviewing class organization, asset enqueue code, repeated strings, or "should I make this a singleton" decisions.

webapp-testing

5
from nvdigitalsolutions/mcp-ai-wpoos

Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.