agentsclimarketplace

Wp plugin development

Skill ComeOnOliver/skillshub/skills/alessioarzenton/claude-code-wp-toolkit/wp-plugin-development

WordPress plugin development — architecture, hooks, Settings API, security, data, and lifecycle. Always-active rules when working on plugins or mu-plugins.From its SKILL.md

Install
npx -y skills add ComeOnOliver/skillshub --skill wp-plugin-development

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

9.1 KB, ~2.1k tokens by cl100k_base, as published. Nobody here has run it

WordPress Plugin Development

Skill for creating and maintaining WordPress plugins. Covers architecture, lifecycle (activation/deactivation/uninstallation), Settings API, security, and data storage.

When to use

Apply this skill when the task involves:

  • Creating or restructuring a plugin (bootstrap, namespace, classes)
  • Adding hooks, actions, and filters
  • Managing activation, deactivation, and uninstallation
  • Creating settings pages with the Settings API
  • Implementing security (nonce, capability, sanitization, escaping)
  • Data storage (options, custom tables, migrations)

Prerequisites

Before starting, verify:

  • Plugin path (under plugins/ or mu-plugins/)
  • Whether the site is single or multisite
  • Minimum WordPress and PHP version of the project
  • Any dependencies (Composer, external libraries)

Procedure

1) Predictable architecture

Fundamental rules:

  • Single bootstrap: the main file with the plugin header is the only entry point
  • No side-effects on load: do not execute logic in the require — register everything on hooks
  • Dedicated loader: use a class or function to register all hooks in a centralized location
  • Separate admin code: protect admin code with is_admin() or specific hooks (admin_init, admin_menu) to reduce frontend overhead
<?php
/**
 * Plugin Name: {{PROJECT_NAME}} — Custom functionality
 * Description: Custom extensions for the project.
 * Version:     1.0.0
 * Author:      Developer
 * Text Domain: {{TEXT_DOMAIN}}
 * Requires PHP: 8.2
 */

declare(strict_types=1);

// Prevent direct access.
defined('ABSPATH') || exit;

// Load dependencies.
require_once __DIR__ . '/src/bootstrap.php';

Recommended structure:

my-plugin/
├── my-plugin.php           # Header + require bootstrap
├── src/
│   ├── bootstrap.php       # Register hooks and load classes
│   ├── Admin/              # Admin pages, settings
│   ├── Frontend/           # Shortcode, template tags
│   ├── REST/               # REST endpoints (if present)
│   └── Includes/           # Utilities, helpers
├── assets/                 # Compiled CSS/JS
├── languages/              # .pot/.po/.mo files
├── templates/              # PHP templates for output
└── uninstall.php           # Data cleanup on uninstallation

2) Lifecycle — Activation, deactivation, uninstallation

Lifecycle hooks are delicate — errors here block plugin activation.

Rules:

  • Register register_activation_hook and register_deactivation_hook in the main file, not inside other hooks
  • Call flush_rewrite_rules() only after registering CPTs/rewrite rules
  • Uninstallation must be explicit and safe
// In the main plugin file.
register_activation_hook(__FILE__, function (): void {
    // Create tables, initial options, roles.
    add_option('my_plugin_version', '1.0.0');

    // If you register CPTs, register them BEFORE flushing.
    register_custom_post_types();
    flush_rewrite_rules();
});

register_deactivation_hook(__FILE__, function (): void {
    // Remove cron, flush rules.
    wp_clear_scheduled_hook('my_plugin_cron');
    flush_rewrite_rules();
});

Uninstallation — prefer uninstall.php (safer than register_uninstall_hook):

// uninstall.php
defined('WP_UNINSTALL_PLUGIN') || exit;

delete_option('my_plugin_version');
delete_option('my_plugin_settings');
// Remove custom tables if present.

3) Settings API — Settings pages

Always use the native Settings API to manage options:

add_action('admin_init', function (): void {
    register_setting('my_plugin_options', 'my_plugin_settings', [
        'type'              => 'array',
        'sanitize_callback' => 'sanitize_plugin_settings',
        'default'           => [
            'api_url'   => '',
            'per_page'  => 10,
            'active'    => false,
        ],
    ]);

    add_settings_section(
        'my_plugin_general',
        __('General Settings', '{{TEXT_DOMAIN}}'),
        null,
        'my_plugin_options'
    );

    add_settings_field(
        'api_url',
        __('URL API', '{{TEXT_DOMAIN}}'),
        'render_api_url_field',
        'my_plugin_options',
        'my_plugin_general'
    );
});

function sanitize_plugin_settings(array $input): array {
    return [
        'api_url'   => esc_url_raw($input['api_url'] ?? ''),
        'per_page'  => absint($input['per_page'] ?? 10),
        'active'    => !empty($input['active']),
    ];
}
  • register_setting() with sanitize_callback for every option group
  • esc_url_raw() for URLs, absint() for integers, sanitize_text_field() for strings
  • Use get_option('my_plugin_settings') to read

4) Security baseline (always)

These rules apply to all plugin/theme code:

Validate input / Escape output

// INPUT — sanitize early.
$title  = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
$email  = sanitize_email($_POST['email'] ?? '');
$url    = esc_url_raw($_POST['url'] ?? '');
$html   = wp_kses_post($_POST['content'] ?? '');

// OUTPUT — escape late.
echo esc_html($title);
echo esc_attr($value);
echo esc_url($link);
echo wp_kses_post($html_content);

Nonce to prevent CSRF

// In the form.
wp_nonce_field('my_plugin_save', 'my_plugin_nonce');

// In the handler.
if (!wp_verify_nonce($_POST['my_plugin_nonce'] ?? '', 'my_plugin_save')) {
    wp_die(__('Security verification failed.', '{{TEXT_DOMAIN}}'));
}

Capability check for authorization

// ALWAYS check capabilities, not roles.
if (!current_user_can('manage_options')) {
    wp_die(__('You do not have permission for this action.', '{{TEXT_DOMAIN}}'));
}

Secure SQL

// NEVER concatenate strings in SQL queries.
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}my_table WHERE status = %s AND amount > %d",
        $status,
        $min_amount
    )
);

5) Data storage

Data typeWhere to storeWhen
Plugin configurationwp_options with get_option/update_optionFew values, frequent access
Per-post metadatapostmeta with get_post_meta/update_post_metaData tied to a single post
Structured/relational dataCustom table $wpdb->prefix . 'my_table'Many records, complex queries, performance
Temporary cacheTransients (get_transient/set_transient)API results, expensive calculations

Migrations and upgrades — always save a schema version:

function my_plugin_upgrade_check(): void {
    $current = get_option('my_plugin_db_version', '0');
    if (version_compare($current, '1.1.0', '<')) {
        my_plugin_upgrade_110();
        update_option('my_plugin_db_version', '1.1.0');
    }
}
add_action('plugins_loaded', 'my_plugin_upgrade_check');

Cron tasks — ensure idempotency:

// Register the event if it does not exist.
if (!wp_next_scheduled('my_plugin_cron')) {
    wp_schedule_event(time(), 'daily', 'my_plugin_cron');
}

add_action('my_plugin_cron', function (): void {
    // Idempotent logic — safe to run multiple times.
});

Verification checklist

  • The plugin activates without fatal errors or notices
  • Settings save and read correctly
  • Nonce and capability checks are present on every form/action
  • Uninstallation removes the plugin's data (and only that)
  • sanitize_callback defined for every register_setting
  • No direct access to $_POST/$_GET without sanitization
  • No SQL queries with string concatenation
  • The project lint/test passes (Pint, PHPUnit if present)

Common errors and solutions

ProblemLikely causeSolution
Activation hook not executedRegistered inside another hook, not in the main fileMove register_activation_hook to the main .php file
Settings not savedSetting not registered, wrong option group, nonce failedVerify register_setting(), check group and nonce
Security regressionNonce present but capability check missingAlways add both: nonce + current_user_can()
Custom table not createddbDelta() not called correctlyVerify SQL syntax and that dbDelta receives queries as a string
Plugin slow on frontendAdmin code loaded on every requestProtect with is_admin() or admin-specific hooks

What NOT to do

  • Do not execute logic at require/include — register everything on hooks
  • Do not use extract() — it makes code unreadable and insecure
  • Do not hardcode paths with ABSPATH . 'wp-content/' — use plugin_dir_path(), plugin_dir_url()
  • Do not create custom tables for data that would fit in wp_options or postmeta
  • Do not trust $_POST/$_GET/$_REQUEST without sanitization
  • Do not use wp_die() as the only error handling — return WP_Error when possible
  • Do not forget the text domain '{{TEXT_DOMAIN}}' in translatable strings

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.