WordPress Plugins skill for Codex

Blocks and plugins that pass PHPCS and Plugin Check. Codex writes WordPress plugins that treat every hook, AJAX handler and REST route as an entry point needing a capability check, a nonce and sanitised input, with output escaped at the last moment and SQL through prepare(). Blocks come from block.json via create-block and wp-scripts, dynamic where content changes. Work is verified with wp-env, PHPCS WordPress standards and Plugin Check.

  • Integrations
  • $wordpress-plugins
  • 205 lines
  • 2 files
  • ~2,200 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- wordpress-plugins

Installs to ~/.agents/skills/wordpress-plugins/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $wordpress-plugins, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and hardens WordPress plugins and Gutenberg blocks - plugin headers and structure, hooks, block.json blocks scaffolded with @wordpress/create-block and built with wp-scripts, dynamic render.php blocks, REST routes with permission callbacks, Settings API, nonces, capability checks, sanitising and escaping, $wpdb->prepare, plus wp-env, WP-CLI, PHPCS WordPress standards and Plugin Check. Use when asked to build a WordPress plugin or custom block, add an admin settings page or REST endpoint, security-review plugin code, or prepare a plugin for WordPress.org.

Use it when

  • You are writing or extending a WordPress plugin or custom Gutenberg block
  • Plugin code needs a security pass for nonces, capabilities, escaping or SQL
  • A plugin is being prepared for WordPress.org review
  • You need a REST route, settings page or custom post type in WordPress

Use something else when

  • You want a decoupled CMS behind a JavaScript front end: use headless-cms
  • You need modern PHP language guidance outside WordPress: use php-8-modern

How to check the result

Run PHPCS with the WordPress standard and Plugin Check with zero errors, then confirm as Author and Subscriber roles that protected actions and REST routes return 403.

The mistake it prevents

Checking a nonce but not a capability, which stops CSRF but still lets any logged-in user perform the action.

~/.agents/skills/wordpress-plugins/SKILL.md

--- name: wordpress-plugins description: Writes and hardens WordPress plugins and Gutenberg blocks - plugin headers and structure, hooks, block.json blocks scaffolded with… license: MIT ---

Done well means: every request that changes state checks a capability and a nonce, every input is sanitised on the way in and every output escaped on the way out, SQL goes through $wpdb->prepare, blocks are registered from block.json, and the plugin passes PHPCS (WordPress standard) and Plugin Check with no errors.

Inspect first

- [ ] Read AGENTS.md, composer.json, package.json and the main plugin file header
- [ ] Note "Requires at least" and "Requires PHP"; do not use APIs newer than those
- [ ] Find the prefix or namespace the plugin uses; reuse it for every new symbol
- [ ] Locate existing hooks, REST routes, AJAX handlers and admin forms (each is an entry point)
- [ ] Check for a local environment: .wp-env.json, docker-compose, or Local

Structure

my-plugin/
├── my-plugin.php          # header, constants, bootstrap only
├── uninstall.php          # cleanup on delete (not on deactivate)
├── includes/              # PHP classes (namespaced, Composer autoload)
├── src/blocks/<name>/     # block.json, edit.js, render.php, style.scss
├── build/                 # wp-scripts output (committed for releases, not for dev)
├── languages/
└── readme.txt             # WordPress.org metadata, "Stable tag"
<?php
/**
 * Plugin Name:       Acme Events
 * Description:       Event listings with a block and REST API.
 * Version:           1.4.0
 * Requires at least: 6.6
 * Requires PHP:      8.1
 * Author:            Acme
 * License:           GPL-2.0-or-later
 * Text Domain:       acme-events
 */

namespace Acme\Events;

defined( 'ABSPATH' ) || exit;

const VERSION = '1.4.0';
require __DIR__ . '/vendor/autoload.php';

add_action( 'init', __NAMESPACE__ . '\\register_blocks' );
register_activation_hook( __FILE__, __NAMESPACE__ . '\\activate' );

Without namespaces, prefix every function, class, option, hook, handle and meta key with a unique slug (acme_events_). Collisions with other plugins are the most common fatal error.

Blocks

npx @wordpress/create-block@latest acme-event-list --variant dynamic   # needs network
npm run start        # wp-scripts start (watch)
npm run build        # wp-scripts build
{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "acme/event-list",
  "title": "Event list",
  "category": "widgets",
  "attributes": { "count": { "type": "integer", "default": 5 } },
  "supports": { "html": false, "spacing": { "margin": true } },
  "textdomain": "acme-events",
  "editorScript": "file:./index.js",
  "style": "file:./style-index.css",
  "render": "file:./render.php"
}
function register_blocks(): void {
    register_block_type( __DIR__ . '/../build/event-list' );
}
Block typeChoose whenOutput
Static (save returns markup)Content does not depend on server stateMarkup stored in post content; changing save causes “invalid block” until deprecations are added
Dynamic (render.php)Lists, queries, anything that changes after publishingRendered on each request; save returns null
Interactivity API (viewScriptModule, data-wp-*)Front-end interactivity without a custom React appServer-rendered with hydration

In render.php, $attributes, $content and $block are available; escape everything and use get_block_wrapper_attributes() on the wrapper. Changing a static block’s save output requires a deprecated entry, or every existing post shows a recovery prompt.

Security rules (every entry point)

ConcernDoNever
Authorisationcurrent_user_can( 'edit_post', $id ) or a specific capabilityCheck is_admin() (it means “admin screen”, not “administrator”)
CSRFwp_nonce_field() + check_admin_referer(); check_ajax_referer(); REST uses the X-WP-Nonce cookie nonceTreat a nonce as authentication
Inputsanitize_text_field, absint, sanitize_email, esc_url_raw, sanitize_key, wp_kses_post after wp_unslashUse $_POST raw
Outputesc_html, esc_attr, esc_url, wp_kses_post, esc_js (inline only) at the point of outputEscape early and store escaped data
SQL$wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $table, $id )Concatenate variables into SQL
Fileswp_handle_upload() with an allowed mime list; validate paths with realpath inside the uploads dirTrust filename or extension
Redirectswp_safe_redirect() then exitRedirect to user-supplied URLs
Datajson_decode, maybe_unserialize only on trusted dataunserialize() user input
add_action( 'admin_post_acme_save_event', function () {
    if ( ! current_user_can( 'edit_posts' ) ) {
        wp_die( esc_html__( 'Not allowed.', 'acme-events' ), 403 );
    }
    check_admin_referer( 'acme_save_event' );
    $title = sanitize_text_field( wp_unslash( $_POST['title'] ?? '' ) );
    $raw   = sanitize_text_field( wp_unslash( $_POST['date'] ?? '' ) );
    $date  = preg_match( '/^\d{4}-\d{2}-\d{2}$/', $raw ) ? $raw : '';
    // ... save
    wp_safe_redirect( admin_url( 'admin.php?page=acme-events&saved=1' ) );
    exit;
} );

REST routes

add_action( 'rest_api_init', function () {
    register_rest_route( 'acme/v1', '/events/(?P<id>\d+)', [
        'methods'             => \WP_REST_Server::EDITABLE,
        'callback'            => __NAMESPACE__ . '\\update_event',
        'permission_callback' => fn ( \WP_REST_Request $r ) => current_user_can( 'edit_post', (int) $r['id'] ),
        'args'                => [
            'title' => [ 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ],
        ],
    ] );
} );

permission_callback is mandatory; __return_true is acceptable only for genuinely public read endpoints. Return WP_Error with a status code for failures. Do not expose private meta or user emails in responses.

Settings, options and data

  • Settings API: register_setting( 'acme_events', 'acme_events_options', [ 'sanitize_callback' => ..., 'default' => [...] ] ) plus add_settings_section/add_settings_field, or a React settings page using show_in_rest and @wordpress/components.
  • Large or rarely used options: store with autoload off (add_option( $name, $value, '', false ) or update_option( $name, $value, false )). Every autoloaded option loads on every request.
  • Custom tables only when post types plus meta truly do not fit; create them with dbDelta() in activation and version the schema in an option.
  • uninstall.php must check defined( 'WP_UNINSTALL_PLUGIN' ) before deleting options and tables. Deleting user data is destructive: make it opt-in via a setting.

Assets and performance

  • Enqueue with wp_enqueue_script/wp_enqueue_style using the *.asset.php file wp-scripts generates for dependencies and version; load admin assets only on your screen (admin_enqueue_scripts with a $hook_suffix check).
  • Cache expensive queries with transients or wp_cache_*; never run queries in a loop over posts.
  • Use WP_Query with 'no_found_rows' => true when you do not paginate.

i18n

Wrap strings with __(), _e(), esc_html__(), _n(); text domain equals the plugin slug. JS: wp_set_script_translations( $handle, 'acme-events' ). Plugins hosted on WordPress.org get translations loaded automatically; no load_plugin_textdomain call is needed for them.

Tooling and verification

npx @wordpress/env start                                 # local WP in Docker (needs Docker)
npx wp-env run cli wp plugin activate acme-events
composer require --dev wp-coding-standards/wpcs dealerdirect/phpcodesniffer-composer-installer
vendor/bin/phpcs --standard=WordPress --extensions=php includes my-plugin.php
npx wp-env run cli wp plugin install plugin-check --activate
npx wp-env run cli wp plugin check acme-events
  • PHPCS WordPress standard: zero errors; escaping and nonce sniffs (WordPress.Security.*) are blockers, not style.
  • Plugin Check: zero errors before a WordPress.org submission.
  • Manually: log in as an Author and a Subscriber and confirm each admin action, AJAX call and REST route rejects them where it should (403).
  • Test with WP_DEBUG and WP_DEBUG_LOG on; the log must stay free of notices.

Publishing to WordPress.org (SVN tag, Stable tag bump) or pushing an update to live sites is outward-facing: prepare it, then wait for the user to confirm.

Report template

**Plugin:** acme-events 1.4.0 (requires WP 6.6, PHP 8.1)
**Changes:** dynamic block acme/event-list; REST PATCH acme/v1/events/{id}
**Security:** capability + nonce on 3 entry points; all output escaped; 2 queries moved to prepare()
**Checks:** phpcs WordPress 0 errors; Plugin Check 0 errors; role tests (author/subscriber) pass

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "WordPress Plugins"
  short_description: "Blocks and plugins that pass PHPCS and Plugin Check"
  default_prompt: "$wordpress-plugins Build a WordPress plugin with a dynamic block that lists upcoming events"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc