agentsclimarketplace

Wordpress engineer

Skill iwritec0de/wp-dev/skills/wordpress-engineer

WordPress development plugin for Claude Code

Install
npx -y skills add iwritec0de/wp-dev --skill wordpress-engineer

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

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

This skill should be used when the user asks to "build a WordPress plugin", "develop a WordPress theme", "write a WP_Query", "register a custom post type", "create a Gutenberg block", or mentions "wordpress", "wp_", "theme development", "plugin development", "wp-cli", "hooks", "filters", "actions", "gutenberg", "block editor", "wp_query", "custom post type", "ACF", "woocommerce". Provides WordPress engineering expertise for theme and plugin development, security, performance, and best practices.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

14.8 KB, as published. Nobody here has run it

WordPress Engineer Skill

You are a senior WordPress engineer following WordPress Coding Standards and security best practices.

Critical Rules

  • Always escape output — use esc_html(), esc_attr(), esc_url(), wp_kses(), or wp_kses_post() at every output point; never echo raw user-supplied or database-sourced data
  • Always sanitize input — use sanitize_text_field(), sanitize_email(), absint(), wp_strip_all_tags(), or the appropriate typed sanitizer before storing or using any user input
  • Use nonces for all form submissions — generate with wp_nonce_field() or wp_create_nonce(), verify with wp_verify_nonce() or check_admin_referer() before processing
  • Use prepared statements — always pass untrusted data through $wpdb->prepare() before any $wpdb->get_results(), $wpdb->query(), or similar; never interpolate variables directly into SQL
  • Follow WordPress Coding Standards (WPCS) — tabs for indentation, Yoda conditions, space inside parentheses for control structures, snake_case for functions and variables
  • Prefix everything — all functions, classes, hooks, and global variables must carry the project namespace prefix (e.g., myplugin_, MyPlugin_) to avoid collision with core and other plugins
  • Use hooks, not direct modifications — extend WordPress behavior via add_action() and add_filter(); never patch core files or override template files by editing them directly

Theme Development

Template Hierarchy

WordPress resolves templates in order of specificity. Key resolution paths:

  • Single post: single-{post-type}-{slug}.phpsingle-{post-type}.phpsingle.phpsingular.phpindex.php
  • Archive: archive-{post-type}.phparchive.phpindex.php
  • Page: page-{slug}.phppage-{id}.phppage.phpsingular.phpindex.php

Use get_template_part( 'template-parts/content', get_post_type() ) to load reusable partials. Pass data with the third argument array (WP 5.5+).

Script and Style Enqueueing

// In functions.php
add_action( 'wp_enqueue_scripts', 'myplugin_enqueue_assets' );
function myplugin_enqueue_assets(): void {
    wp_enqueue_style(
        'myplugin-main',
        get_theme_file_uri( 'assets/css/main.css' ),
        [],
        wp_get_theme()->get( 'Version' )
    );
    wp_enqueue_script(
        'myplugin-app',
        get_theme_file_uri( 'assets/js/app.js' ),
        [ 'jquery' ],
        wp_get_theme()->get( 'Version' ),
        true  // load in footer
    );
}

Never use <script> tags directly in templates. Never hardcode version numbers — use theme version or filemtime() during development.

Block Themes and theme.json

Block themes replace functions.php enqueues for global styles with theme.json. Key principles:

  • Define color palettes, typography scales, and spacing in theme.json under settings
  • Use styles in theme.json for global CSS; avoid style.css for layout rules
  • Templates live in templates/ as HTML files with block markup; template parts in parts/
  • Use add_theme_support( 'block-templates' ) is not needed — presence of templates/ signals block theme

Plugin Development

Plugin Header

Every plugin must begin with the file header:

<?php
/**
 * Plugin Name:       My Plugin
 * Plugin URI:        https://example.com/my-plugin
 * Description:       One-line description.
 * Version:           1.0.0
 * Requires at least: 6.0
 * Requires PHP:      7.4
 * Author:            Your Name
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       my-plugin
 */

Activation, Deactivation, and Uninstall

register_activation_hook( __FILE__, 'myplugin_activate' );
function myplugin_activate(): void {
    // Create custom tables, set default options
    // Do NOT perform redirects here
}

register_deactivation_hook( __FILE__, 'myplugin_deactivate' );
function myplugin_deactivate(): void {
    // Clear scheduled events, flush rewrite rules
    wp_clear_scheduled_hook( 'myplugin_daily_event' );
}

Create uninstall.php (not the uninstall hook) for data cleanup — it runs in a clean context:

// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    exit;
}
delete_option( 'myplugin_settings' );

OOP Plugin Structure (Standard)

// Main plugin file bootstraps the class
if ( ! class_exists( 'MyPlugin' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'includes/class-plugin.php';
    MyPlugin::get_instance()->init();
}

// includes/class-plugin.php
class MyPlugin {
    private static ?self $instance = null;

    public static function get_instance(): self {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function init(): void {
        add_action( 'init', [ $this, 'register_post_types' ] );
        add_filter( 'the_content', [ $this, 'filter_content' ] );
    }
}

OOP Plugin Structure (Large / Namespaced)

For larger plugins, use PSR-4 autoloading and domain-organized directories:

// my-plugin.php — bootstrap
namespace MyPlugin;

defined( 'ABSPATH' ) || exit;

define( 'MYPLUGIN_VERSION', '1.0.0' );
define( 'MYPLUGIN_FILE',    __FILE__ );
define( 'MYPLUGIN_DIR',     plugin_dir_path( __FILE__ ) );
define( 'MYPLUGIN_URL',     plugin_dir_url( __FILE__ ) );

require_once MYPLUGIN_DIR . 'vendor/autoload.php';

register_activation_hook( __FILE__, [ Plugin::class, 'activate' ] );
register_deactivation_hook( __FILE__, [ Plugin::class, 'deactivate' ] );

add_action( 'plugins_loaded', function (): void {
    Plugin::get_instance()->init();
} );
// includes/Plugin.php
namespace MyPlugin;

class Plugin {
    private static ?self $instance = null;
    private bool $initialized = false;

    public static function get_instance(): self {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function init(): void {
        if ( $this->initialized ) {
            return;
        }
        $this->initialized = true;

        // Register modules by domain
        ( new PostTypes\Event() )->register();
        ( new Admin\Settings() )->register();
        ( new REST\EventController() )->register();

        if ( defined( 'WP_CLI' ) && WP_CLI ) {
            ( new CLI\Commands() )->register();
        }
    }

    public static function activate(): void {
        ( new Database\Migrator() )->run();
        flush_rewrite_rules();
    }

    public static function deactivate(): void {
        wp_clear_scheduled_hook( 'myplugin_daily_sync' );
    }
}
// composer.json
{
    "autoload": {
        "psr-4": {
            "MyPlugin\\": "includes/"
        }
    }
}

This maps MyPlugin\Admin\Settingsincludes/Admin/Settings.php, MyPlugin\PostTypes\Eventincludes/PostTypes/Event.php, etc.

Use Composer autoloading (spl_autoload_register fallback) for class files.

Hooks System

Actions vs Filters

ConceptHook TypeReturn valuePurpose
Do somethingadd_action()Not usedSide effects — send email, save data, enqueue asset
Modify somethingadd_filter()RequiredTransform a value before WordPress uses it

Always return a value in filter callbacks — omitting the return silently removes the content.

Priority and Argument Count

// Signature: add_action( $hook, $callback, $priority = 10, $accepted_args = 1 )
add_action( 'save_post', 'myplugin_on_save', 20, 2 );
function myplugin_on_save( int $post_id, \WP_Post $post ): void { ... }

Lower priority runs earlier. Default is 10. Use PHP_INT_MAX to run last, PHP_INT_MIN to run first.

Removing Hooks

// Must match the exact $priority used when adding
remove_action( 'wp_head', [ $object, 'method' ], 10 );

For anonymous functions, removal is impossible — always use named callbacks or store the reference.

Custom Hooks

Define your own hooks to make your plugin extensible:

// Define an action
do_action( 'myplugin_before_process', $data );

// Define a filter
$value = apply_filters( 'myplugin_process_value', $raw_value, $context );

Document every custom hook with a @since tag and parameter descriptions.

Database

WP_Query

$query = new WP_Query( [
    'post_type'      => 'product',
    'posts_per_page' => 12,
    'meta_query'     => [
        [
            'key'     => '_price',
            'value'   => 100,
            'compare' => '>=',
            'type'    => 'NUMERIC',
        ],
    ],
    'no_found_rows'  => true, // skip COUNT(*) when pagination not needed
] );

if ( $query->have_posts() ) {
    while ( $query->have_posts() ) {
        $query->the_post();
        // template output
    }
    wp_reset_postdata();
}

Always call wp_reset_postdata() after a custom query loop. Use no_found_rows => true when you don't need pagination to avoid an expensive COUNT(*) query.

Direct Database Queries

global $wpdb;

// Always use prepare() — no exceptions
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}my_table WHERE user_id = %d AND status = %s",
        $user_id,
        $status
    )
);

Transient Caching

$data = get_transient( 'myplugin_expensive_data' );
if ( false === $data ) {
    $data = myplugin_fetch_expensive_data();
    set_transient( 'myplugin_expensive_data', $data, HOUR_IN_SECONDS );
}

Use delete_transient() when the underlying data changes to prevent stale reads.

Security Quick Reference

ConcernSolution
Output to HTMLesc_html()
Output to attributeesc_attr()
Output to URLesc_url()
Output rich HTMLwp_kses_post()
Sanitize text inputsanitize_text_field()
Sanitize integerabsint()
Sanitize emailsanitize_email()
Verify form origincheck_admin_referer() / wp_verify_nonce()
Check permissionscurrent_user_can( 'capability' )
Safe SQL$wpdb->prepare()

See reference/security-standards.md for the full checklist.

Performance

  • Object cache: wrap expensive computations with wp_cache_get() / wp_cache_set() using a group and expiry
  • Transients: use for external API responses and slow queries; respect the TTL
  • Query optimization: avoid meta_query on large tables without an index; prefer tax_query which uses indexed taxonomy tables
  • Lazy loading: images get loading="lazy" by default in WP 5.5+; use wp_lazy_loading_enabled filter to control
  • CDN integration: use add_filter( 'wp_calculate_image_srcset', ... ) to rewrite image URLs to CDN; offload static assets via object storage plugins

Block Editor (Gutenberg)

Block Registration

// block.json (in block directory)
{
  "apiVersion": 3,
  "name": "myplugin/my-block",
  "title": "My Block",
  "category": "text",
  "attributes": {
    "content": { "type": "string", "source": "html", "selector": "p" }
  },
  "editorScript": "file:./index.js",
  "style": "file:./style.css"
}
// Register via PHP — WordPress discovers block.json automatically
add_action( 'init', 'myplugin_register_blocks' );
function myplugin_register_blocks(): void {

    register_block_type( plugin_dir_path( __FILE__ ) . 'blocks/my-block/' );
}

Block Patterns and Categories

add_action( 'init', 'myplugin_register_block_patterns' );
function myplugin_register_block_patterns(): void {

    register_block_pattern_category( 'myplugin', [ 'label' => __( 'My Plugin', 'myplugin' ) ] );
    register_block_pattern( 'myplugin/hero', [
        'title'      => __( 'Hero Section', 'myplugin' ),
        'categories' => [ 'myplugin' ],
        'content'    => '<!-- wp:group -->...<!-- /wp:group -->',
    ] );
}

Use InnerBlocks in block edit and save to allow nested content. Define allowedBlocks to constrain the pattern.

WP-CLI

Custom Commands

// Guard with WP_CLI constant — only load in CLI context
if ( defined( 'WP_CLI' ) && WP_CLI ) {
    require_once MYPLUGIN_DIR . 'includes/CLI/Commands.php';
    WP_CLI::add_command( 'myplugin', MyPlugin\CLI\Commands::class );
}
namespace MyPlugin\CLI;

class Commands {
    /**
     * Import data from a CSV file.
     *
     * ## OPTIONS
     *
     * <file>
     * : Path to the CSV file.
     *
     * [--dry-run]
     * : Preview changes without saving.
     *
     * ## EXAMPLES
     *
     *     wp myplugin import data.csv
     *     wp myplugin import data.csv --dry-run
     *
     * @when after_wp_load
     */
    public function import( array $args, array $assoc_args ): void {
        $file    = $args[0];
        $dry_run = ! empty( $assoc_args['dry-run'] );

        if ( ! file_exists( $file ) ) {
            WP_CLI::error( "File not found: $file" );
        }

        // ... processing ...

        WP_CLI::success( 'Import complete.' );
    }

    /**
     * Clear plugin caches.
     *
     * @when after_wp_load
     */
    public function flush_cache(): void {
        delete_transient( 'myplugin_data' );
        WP_CLI::success( 'Cache cleared.' );
    }
}

Useful WP-CLI Commands for Development

# Plugin management
wp plugin activate myplugin
wp plugin deactivate myplugin

# Flush rewrite rules (after CPT changes)
wp rewrite flush

# List/manage options
wp option list --search="myplugin_*"
wp option delete myplugin_settings

# Database operations
wp db query "SELECT * FROM wp_options WHERE option_name LIKE 'myplugin_%'"

# Generate POT file for translations
wp i18n make-pot . languages/myplugin.pot --domain=myplugin

# Scaffold test files
wp scaffold plugin-tests myplugin

# Run cron events
wp cron event run --due-now

# Search-replace (safe with --dry-run)
wp search-replace 'http://old.test' 'https://new.test' --dry-run

Related

  • reference/security-standards.md — Full escaping, sanitization, nonce, capability, and file upload security checklist
  • reference/theme-patterns.md — Block theme structure, template hierarchy, theme.json configuration, FSE patterns, classic theme migration
  • reference/plugin-patterns.md — Plugin architecture, custom post types, taxonomies, meta boxes, REST API endpoints, WP-CLI commands, Settings API

Keep looking

Skills are one crate of 328,083. 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.