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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- wordpress-plugins Installs to ~/.agents/skills/wordpress-plugins/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install wordpress-plugins The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project wordpress-plugins Installs to .agents/skills/wordpress-plugins/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/wordpress-plugins.zipunzip -o wordpress-plugins.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) wordpress-plugins PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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 type | Choose when | Output |
|---|---|---|
Static (save returns markup) | Content does not depend on server state | Markup stored in post content; changing save causes “invalid block” until deprecations are added |
Dynamic (render.php) | Lists, queries, anything that changes after publishing | Rendered on each request; save returns null |
Interactivity API (viewScriptModule, data-wp-*) | Front-end interactivity without a custom React app | Server-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)
| Concern | Do | Never |
|---|---|---|
| Authorisation | current_user_can( 'edit_post', $id ) or a specific capability | Check is_admin() (it means “admin screen”, not “administrator”) |
| CSRF | wp_nonce_field() + check_admin_referer(); check_ajax_referer(); REST uses the X-WP-Nonce cookie nonce | Treat a nonce as authentication |
| Input | sanitize_text_field, absint, sanitize_email, esc_url_raw, sanitize_key, wp_kses_post after wp_unslash | Use $_POST raw |
| Output | esc_html, esc_attr, esc_url, wp_kses_post, esc_js (inline only) at the point of output | Escape early and store escaped data |
| SQL | $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $table, $id ) | Concatenate variables into SQL |
| Files | wp_handle_upload() with an allowed mime list; validate paths with realpath inside the uploads dir | Trust filename or extension |
| Redirects | wp_safe_redirect() then exit | Redirect to user-supplied URLs |
| Data | json_decode, maybe_unserialize only on trusted data | unserialize() 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' => [...] ] )plusadd_settings_section/add_settings_field, or a React settings page usingshow_in_restand@wordpress/components. - Large or rarely used options: store with autoload off (
add_option( $name, $value, '', false )orupdate_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.phpmust checkdefined( '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_styleusing the*.asset.phpfile wp-scripts generates for dependencies and version; load admin assets only on your screen (admin_enqueue_scriptswith a$hook_suffixcheck). - Cache expensive queries with transients or
wp_cache_*; never run queries in a loop over posts. - Use
WP_Querywith'no_found_rows' => truewhen 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_DEBUGandWP_DEBUG_LOGon; 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.yaml
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.