Integrations
Automattic/wordpress-activitypub
Third-party WordPress plugin integration patterns. An agent skill from Automattic/wordpress-activitypub.
WordPress plugin architecture review and WordPress.org submission standards.
$ npx skills add jorgerosal/wordpress-skills --skill wp-plugin-development -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install jorgerosal/wordpress-skills wp-plugin-development --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/jorgerosal/wordpress-skills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/claude-skills/wp-plugin-development .claude/skills/wp-plugin-development && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "wp-plugin-development" agent skill from https://github.com/jorgerosal/wordpress-skills/tree/main/claude-skills/wp-plugin-development into .claude/skills/wp-plugin-development/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "wp-plugin-development", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/jorgerosal/wordpress-skills/tree/main/claude-skills/wp-plugin-developmentType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add jorgerosal/wordpress-skills --skill wp-plugin-development -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install jorgerosal/wordpress-skills wp-plugin-development --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/jorgerosal/wordpress-skills.git skills-src && mkdir -p .agents/skills && cp -r skills-src/claude-skills/wp-plugin-development .agents/skills/wp-plugin-development && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "wp-plugin-development" agent skill from https://github.com/jorgerosal/wordpress-skills/tree/main/claude-skills/wp-plugin-development into .agents/skills/wp-plugin-development/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "wp-plugin-development", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add jorgerosal/wordpress-skills --skill wp-plugin-development -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install jorgerosal/wordpress-skills wp-plugin-development --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/jorgerosal/wordpress-skills.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/claude-skills/wp-plugin-development .cursor/skills/wp-plugin-development && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "wp-plugin-development" agent skill from https://github.com/jorgerosal/wordpress-skills/tree/main/claude-skills/wp-plugin-development into .cursor/skills/wp-plugin-development/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "wp-plugin-development", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/jorgerosal/wordpress-skills.git --path claude-skills/wp-plugin-development--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add jorgerosal/wordpress-skills --skill wp-plugin-development -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install jorgerosal/wordpress-skills wp-plugin-development --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/jorgerosal/wordpress-skills.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/claude-skills/wp-plugin-development .gemini/skills/wp-plugin-development && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "wp-plugin-development" agent skill from https://github.com/jorgerosal/wordpress-skills/tree/main/claude-skills/wp-plugin-development into .gemini/skills/wp-plugin-development/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "wp-plugin-development", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install jorgerosal/wordpress-skills wp-plugin-developmentInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add jorgerosal/wordpress-skills --skill wp-plugin-development -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/jorgerosal/wordpress-skills.git skills-src && mkdir -p .github/skills && cp -r skills-src/claude-skills/wp-plugin-development .github/skills/wp-plugin-development && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "wp-plugin-development" agent skill from https://github.com/jorgerosal/wordpress-skills/tree/main/claude-skills/wp-plugin-development into .github/skills/wp-plugin-development/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "wp-plugin-development", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add jorgerosal/wordpress-skills --skill wp-plugin-development -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install jorgerosal/wordpress-skills wp-plugin-development --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/jorgerosal/wordpress-skills.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/claude-skills/wp-plugin-development .opencode/skills/wp-plugin-development && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "wp-plugin-development" agent skill from https://github.com/jorgerosal/wordpress-skills/tree/main/claude-skills/wp-plugin-development into .opencode/skills/wp-plugin-development/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "wp-plugin-development", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
wp-plugin-developmentWordPress plugin architecture review and WordPress.org submission standards.
Wp Plugin Development is an agent skill from jorgerosal/wordpress-skills. WordPress plugin architecture review and WordPress.org submission standards. Use when reviewing WordPress plugin code, auditing plugin architecture, preparing WordPress.org plugin submission, checking plugin best practices, analyzing custom post types, taxonomies, Settings API, hooks system, internationalization, or when user mentions "plugin review", "plugin development", "plugin best practices", "WordPress.org submission", "Plugin Check", "plugin headers", "activation hook", "deactivation hook", "uninstall"…
Its SKILL.md is about 12k tokens, which your agent loads only when the skill is triggered. The skill folder holds 5 other files, including reference files (for example `references/api-patterns.md`, `references/architecture-patterns.md` and `references/hooks-guide.md`).
It sits in Frontend & Design, covering Hooks and plugins, Internationalization and Software architecture. It works with WordPress and PHP. The repository describes itself as: ✅ 🎉 Claude skills and Codex skills for Wordpress development❗️. The licence is MIT.
6 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 8c96442. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
rgFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
gnu.orgFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Wp Plugin Development loads about 12k tokens when it runs, and up to ~36k if it reads all its reference files. Until then it costs about 193 tokens; SKILL.md has 2,853 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from jorgerosal/wordpress-skills at commit 8c96442, republished under its MIT licence (© jorgerosal). 2,853 words, ~11,557 tokens.
.claude/skills/wp-plugin-development/SKILL.md (or your agent's skills folder). This skill also uses 4 other files; get the full folder from GitHub.Systematic plugin architecture review for WordPress plugins targeting WordPress 6.x+. Core principle: WordPress plugins extend functionality through a standardized API ecosystem—lifecycle hooks (activation/deactivation/uninstall), Settings API, hooks system (actions/filters), custom post types/taxonomies, and internationalization. Review validates plugin architecture, naming conventions, WordPress.org compliance standards, and cross-references wp-security-review for security-specific patterns (nonces, capabilities, sanitization). Report findings grouped by file with line numbers, severity labels (CRITICAL/WARNING/INFO), and BAD/GOOD code pairs showing proper implementations.
Use when:
Don't use for:
Follow this six-step workflow for systematic plugin reviews:
Identify plugin type and context
Check plugin header completeness and metadata
Plugin Name field presentScan for CRITICAL patterns (breaks functionality or causes WordPress.org rejection)
defined( 'ABSPATH' ) || exit; check in PHP filesflush_rewrite_rules() called on init hook (expensive operation on every page load)$wpdb->query() instead of dbDelta()permission_callback (WordPress 5.5+ requirement)Check WARNING patterns (non-standard but functional)
__() or _e() calls without text domain parameter/wp-content/plugins/)sanitize_callback in register_setting()show_in_rest for Gutenberg compatibilityis_admin() check)Note INFO improvements (best practices and optimizations)
Requires at least, Requires PHP)show_in_rest for Block Editor supportDomain Path in headerApply context-aware severity and report
/wp-sec-review for comprehensive security analysis."my-plugin.php)Plugin headers (PLG-02):
Plugin Name field → WordPress won't recognize pluginText Domain → Breaks internationalizationRequires at least or Requires PHP → Can't enforce version requirementsABSPATH check (PLG-03):
defined( 'ABSPATH' ) || exit; at top → Direct file access possiblePrefixing (PLG-04):
function [a-z_]+\( without plugin prefix (4-5 char minimum)Activation/deactivation hooks (PLG-04):
register_activation_hook() but plugin has setup needs → No way to initializeregister_deactivation_hook() but plugin has temp data → No cleanup on deactivationregister_activation_hook( __FILE__, 'callback' )uninstall.php)WP_UNINSTALL_PLUGIN constant check (PLG-05):
uninstall.php without WP_UNINSTALL_PLUGIN check → Can be executed directlyif ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { exit; }Cleanup completeness (PLG-05):
delete_option()) → Leaves plugin data in databasedelete_transient()) → Database bloatDROP TABLE) → Orphaned tablesdelete_metadata( 'user', ... )) → User data remainsregister_post_type() usage (PLG-06):
show_in_rest => true → Not accessible in Block Editorflush_rewrite_rules() called on init → Performance issue (expensive DB write on every page load)register_post_type( 'key', array( ... ) ) hooked to initRewrite flush timing (PLG-06):
flush_rewrite_rules() on init hook → DB write on every page loadregister_taxonomy() usage (PLG-07):
show_in_rest => true → Not accessible in Block Editorregister_taxonomy( 'name', 'post_type', array( ... ) ) hooked to initregister_setting() usage (PLG-08):
add_settings_field() without corresponding register_setting() → Field won't saveregister_setting() without sanitize_callback → Unsanitized data storedregister_setting() without type parameter → Type inference unreliableregister_setting( 'option_group', 'option_name', array( ... ) ) on admin_initSettings API workflow (PLG-09):
add_settings_section() page slug doesn't match do_settings_sections() call → Section won't renderadd_settings_field() section ID doesn't match add_settings_section() ID → Field won't rendersettings_fields() call → No nonce protection, options won't savedo_settings_sections() call → Fields won't renderregister_setting() → add_settings_section() → add_settings_field() → form HTML with settings_fields() and do_settings_sections()add_action/add_filter patterns (PLG-10):
accepted_args not specified when callback needs multiple parameters → Parameters truncatedadd_action( 'hook_name', 'callback', $priority, $accepted_args )Filter return requirement (PLG-10):
add_filter( 'hook', 'callback' ) where callback has no return statementHook removal (PLG-10):
remove_action() or remove_filter() params don't match registration → Won't remove hookadd_action()/add_filter() calladd_action( 'init', 'callback', 15 ) requires remove_action( 'init', 'callback', 15 )Custom hooks for extensibility (PLG-10):
do_action() or apply_filters() for extensibilitydo_action( 'myplugin_after_save', $data ) or apply_filters( 'myplugin_modify_output', $output )Text domain matching (PLG-11):
Text Domain: my-plugin must match plugin folder/file name exactlyi18n function usage (PLG-11):
__() or _e() without text domain parameter → Falls back to core translations (incorrect)__( 'Text', 'text-domain' ) never __( 'Text' )sprintf( __( 'Text %s', 'domain' ), $var ) not __( "Text $var", 'domain' )Pluralization (PLG-11):
_n() → Breaks languages with complex plural rules_n( 'singular', 'plural', $count, 'domain' ) for count-dependent stringsContext-aware translation (PLG-11):
_x() for disambiguation when same English word has different translations_x( 'Post', 'noun', 'domain' ) vs _x( 'Post', 'verb', 'domain' )Escaped translation variants (PLG-11):
esc_html__() or esc_attr__() for combined translation + escapingecho esc_html__( 'Text', 'domain' ) instead of echo esc_html( __( 'Text', 'domain' ) )Translation loading (PLG-11):
load_plugin_textdomain() optional for WordPress 4.6+ (auto-loads from translate.wordpress.org)init if called: add_action( 'init', 'load_textdomain_callback' )register_rest_route() usage:
permission_callback → WordPress 5.5+ error, endpoint blockedpermission_callback => '__return_true' on write operations → Unauthenticated access allowedregister_rest_route( 'namespace/v1', '/route', array( ... ) ) on rest_api_init hook__return_true is CORRECT for public read-only endpointsNamespace and versioning:
myplugin/v1 not just mypluginParameter validation:
sanitize_callback in args → Unsanitized inputvalidate_callback in args → Invalid input acceptedHook registration:
admin-ajax.php for high-frequency calls → Consider REST API for better performanceadd_action( 'wp_ajax_action_name', 'callback' ) for authenticatedadd_action( 'wp_ajax_nopriv_action_name', 'callback' ) for publicSecurity checks:
check_ajax_referer()) and capability checksadd_menu_page/add_submenu_page:
current_user_can() → Capability param insufficient aloneadd_menu_page( $title, $menu_title, 'manage_options', $slug, $callback )Table creation:
$wpdb->query( "CREATE TABLE..." ) instead of dbDelta() → No upgrade path, charset issuesrequire_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); dbDelta( $sql );$wpdb->prefix for table names (not hardcoded wp_)$wpdb->get_charset_collate() for charset/collationdbDelta SQL formatting requirements:
wp_schedule_event() usage:
wp_schedule_event() without checking wp_next_scheduled() → Duplicate schedulesif ( ! wp_next_scheduled( 'hook' ) ) before wp_schedule_event()wp_clear_scheduled_hook( 'hook' ) in deactivation hookset_transient() usage:
set_transient( 'mypl_key', $value, HOUR_IN_SECONDS )Use these rg commands for quick plugin scanning. Organized by severity.
# Main plugin file should contain "Plugin Name" header
# If this returns nothing, the plugin likely lacks a valid main header.
rg -n "Plugin Name:" . -g '*.php'
# PHP files missing ABSPATH check (direct file access possible)
rg -L --iglob '*.php' "defined\s*\(\s*['\"]ABSPATH['\"]\s*\)|defined\s*\(\s*'ABSPATH'\s*\)" .
# Unprefixed function declarations in global scope (namespace collision risk)
rg -n "^function\s+[a-z_][a-z0-9_]*\s*\(" . -g '*.php'
# Text domain mismatch (compare header vs i18n calls)
# First: inspect the main plugin file header for the expected text domain
rg -n "Text Domain:" . -g '*.php'
# Then: compare that slug with translation calls across the plugin
rg -n "__\(|_e\(|_n\(|_x\(" . -g '*.php'
# flush_rewrite_rules outside activation/deactivation (expensive operation on every page load)
rg -n "flush_rewrite_rules" . -g '*.php'
# Filter callbacks missing return statement (manual follow-up required)
rg -n "add_filter\s*\(" . -g '*.php'
# register_rest_route without permission_callback (manual per-route review)
rg -n "register_rest_route\s*\(" . -g '*.php'# Missing register_activation_hook (no setup mechanism)
rg -n "register_activation_hook\s*\(" . -g '*.php'
# Missing uninstall cleanup (no uninstall.php and no register_uninstall_hook)
test -f uninstall.php || rg -n "register_uninstall_hook\s*\(" . -g '*.php'
# i18n functions without obvious text domain parameter (manual confirmation required)
rg -n "__\(|_e\(" . -g '*.php'
# Hardcoded /wp-content/plugins/ paths (breaks on custom WordPress installations)
rg -n "/wp-content/plugins/" . -g '*.php'
# Direct CREATE TABLE with $wpdb->query (should use dbDelta)
rg -n "wpdb->query\s*\(.*CREATE TABLE" . -g '*.php'
# Post type key length check (max 20 characters)
rg -n "register_post_type\s*\(" . -g '*.php'
# Missing show_in_rest in register_post_type (manual per-registration review)
rg -n "register_post_type\s*\(" . -g '*.php'# Missing "Requires at least" or "Requires PHP" in plugin header
rg -n "Requires at least:|Requires PHP:" . -g '*.php'
# Functions not using PHP namespaces (could modernize)
rg -n "^function\s+myprefix_" . -g '*.php'
# Admin code loading on frontend (manual context check)
rg -n "add_action\s*\(\s*['\"]admin_" . -g '*.php'
# Missing Domain Path in plugin header
rg -n "Domain Path:" . -g '*.php'WordPress plugins operate in different distribution contexts. Adjust review severity based on context.
Strictest standards apply. Plugin Check (PCP) tool enforces these requirements:
Plugin Check (PCP) compliance:
defined( 'ABSPATH' ) || exit;)/wp-content/ paths (use WP_CONTENT_DIR or helper functions)How to verify: Run Plugin Check tool before submission:
# Via WP-CLI
wp plugin install plugin-check --activate
wp plugin check my-plugin.php
# Or via WordPress Admin
# Navigate to: Plugins > Add New > Search "Plugin Check"
# Then: Tools > Plugin Check > Select your pluginWordPress.org context severity escalation:
More flexibility. No WordPress.org submission requirements.
Relaxed requirements:
Still important:
Different loading behavior. Must-use plugins load automatically before regular plugins.
Key differences:
wp-content/mu-plugins/ directoryReview adjustments for mu-plugins:
register_activation_hook() → Expected for mu-pluginsregister_deactivation_hook() → Expected for mu-pluginsPattern to detect mu-plugin context:
// Check if plugin is loaded as must-use
if ( strpos( __FILE__, WPMU_PLUGIN_DIR ) !== false ) {
// Running as must-use plugin - skip activation hooks
}Special files with specific names. Drop-ins replace WordPress core functionality.
Common drop-ins:
object-cache.php → Object caching backend (Redis, Memcached)db.php → Custom database classadvanced-cache.php → Page caching systemdb-error.php → Custom database error pagemaintenance.php → Custom maintenance mode pageReview adjustments for drop-ins:
Pattern to detect drop-in:
// Drop-ins are in wp-content/ root, not plugins/
// Check filename against known drop-in names
$drop_ins = array( 'object-cache.php', 'db.php', 'advanced-cache.php', 'db-error.php', 'maintenance.php' );Common plugin patterns organized by concern. All examples use WordPress PHP Coding Standards (spaces inside parentheses, array() not [], Yoda conditions).
Complete header block with all recommended fields:
❌ BAD: Minimal header
<?php
/**
* Plugin Name: My Plugin
*/✅ GOOD: Complete header with all recommended fields
<?php
/**
* Plugin Name: My Awesome Plugin
* Plugin URI: https://example.com/my-awesome-plugin
* Description: Does amazing things with WordPress
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: John Doe
* Author URI: https://example.com
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-awesome-plugin
* Domain Path: /languages
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}Avoid global namespace pollution:
❌ BAD: No prefix (namespace collision risk)
<?php
function save_settings() {
update_option( 'plugin_settings', $_POST['data'] );
}
class Plugin_Settings {
// ...
}
add_action( 'admin_init', 'save_settings' );✅ GOOD: Prefixed functions and classes
<?php
function mypl_save_settings() {
update_option( 'mypl_settings', $_POST['data'] );
}
class MyPL_Settings {
// ...
}
add_action( 'admin_init', 'mypl_save_settings' );✅ BETTER: Use PHP namespaces
<?php
namespace MyPlugin\Admin;
function save_settings() {
update_option( 'mypl_settings', $_POST['data'] ); // Still prefix option names
}
class Settings {
// ...
}
add_action( 'admin_init', __NAMESPACE__ . '\save_settings' );Proper plugin lifecycle management:
❌ BAD: No lifecycle hooks
<?php
// Plugin registers CPT on init but never flushes rewrites
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'mypl_book', array( /* args */ ) );
}✅ GOOD: Complete lifecycle with activation/deactivation/uninstall
<?php
// Main plugin file: my-plugin.php
// Activation hook
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_activate() {
// Set default options
add_option( 'mypl_version', '1.0.0' );
// Register CPT before flushing
mypl_register_cpt();
// Flush rewrite rules ONLY on activation
flush_rewrite_rules();
}
// Deactivation hook
register_deactivation_hook( __FILE__, 'mypl_deactivate' );
function mypl_deactivate() {
// Clear transients
delete_transient( 'mypl_cache' );
// Flush rewrite rules
flush_rewrite_rules();
}
// Init hook for normal operation
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'mypl_book', array( /* args */ ) );
}
// Uninstall file: uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// Delete all plugin data
delete_option( 'mypl_version' );
delete_option( 'mypl_settings' );
// Drop custom table
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}mypl_data" );Proper CPT/taxonomy registration with rewrite rule management:
❌ BAD: Flushes rewrites on every page load
<?php
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'book', array( // Missing prefix, no show_in_rest
'public' => true,
) );
flush_rewrite_rules(); // CRITICAL: Runs on EVERY page load
}✅ GOOD: Prefixed, Gutenberg-ready, flushes only on activation
<?php
add_action( 'init', 'mypl_register_cpt_and_taxonomy' );
function mypl_register_cpt_and_taxonomy() {
// Register custom post type (max 20 chars, prefixed)
register_post_type(
'mypl_book',
array(
'labels' => array(
'name' => __( 'Books', 'my-plugin' ),
'singular_name' => __( 'Book', 'my-plugin' ),
),
'public' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'books' ),
'supports' => array( 'title', 'editor', 'thumbnail' ),
'show_in_rest' => true, // Gutenberg support
)
);
// Register custom taxonomy (max 32 chars, prefixed)
register_taxonomy(
'mypl_genre',
'mypl_book',
array(
'labels' => array(
'name' => __( 'Genres', 'my-plugin' ),
'singular_name' => __( 'Genre', 'my-plugin' ),
),
'hierarchical' => true,
'public' => true,
'show_in_rest' => true, // Gutenberg support
)
);
}
// Flush rewrites ONLY on activation
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_activate() {
mypl_register_cpt_and_taxonomy(); // Register first
flush_rewrite_rules(); // Then flush (only once)
}
register_deactivation_hook( __FILE__, 'mypl_deactivate' );
function mypl_deactivate() {
flush_rewrite_rules(); // Clean up rewrite rules
}Complete Settings API workflow:
❌ BAD: add_settings_field without register_setting (won't save)
<?php
add_action( 'admin_init', 'mypl_settings_init' );
function mypl_settings_init() {
add_settings_section( 'mypl_section', 'Settings', null, 'mypl-settings' );
add_settings_field(
'mypl_api_key',
'API Key',
'mypl_api_key_callback',
'mypl-settings',
'mypl_section'
);
// Missing register_setting() - field won't save!
}✅ GOOD: Complete Settings API workflow
<?php
add_action( 'admin_init', 'mypl_register_settings' );
function mypl_register_settings() {
// 1. Register setting (declares option name and sanitization)
register_setting(
'mypl_options_group', // Option group
'mypl_settings', // Option name
array(
'type' => 'array',
'sanitize_callback' => 'mypl_sanitize_settings',
'default' => array(
'api_key' => '',
'enabled' => false,
),
)
);
// 2. Add settings section
add_settings_section(
'mypl_main_section',
__( 'Main Settings', 'my-plugin' ),
'mypl_section_callback',
'mypl-settings'
);
// 3. Add settings fields
add_settings_field(
'mypl_api_key',
__( 'API Key', 'my-plugin' ),
'mypl_api_key_callback',
'mypl-settings',
'mypl_main_section'
);
add_settings_field(
'mypl_enabled',
__( 'Enable Feature', 'my-plugin' ),
'mypl_enabled_callback',
'mypl-settings',
'mypl_main_section'
);
}
function mypl_section_callback() {
echo '<p>' . esc_html__( 'Configure your plugin settings below.', 'my-plugin' ) . '</p>';
}
function mypl_api_key_callback() {
$options = get_option( 'mypl_settings' );
$value = isset( $options['api_key'] ) ? $options['api_key'] : '';
?>
<input type="text"
name="mypl_settings[api_key]"
value="<?php echo esc_attr( $value ); ?>"
class="regular-text">
<?php
}
function mypl_enabled_callback() {
$options = get_option( 'mypl_settings' );
$checked = isset( $options['enabled'] ) && $options['enabled'];
?>
<input type="checkbox"
name="mypl_settings[enabled]"
value="1"
<?php checked( $checked, true ); ?>>
<?php
}
function mypl_sanitize_settings( $input ) {
$sanitized = array();
if ( isset( $input['api_key'] ) ) {
$sanitized['api_key'] = sanitize_text_field( $input['api_key'] );
}
$sanitized['enabled'] = isset( $input['enabled'] ) && $input['enabled'] ? true : false;
return $sanitized;
}
// Settings page HTML
function mypl_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form method="post" action="options.php">
<?php
// Output nonce, action, option_page fields
settings_fields( 'mypl_options_group' );
// Output sections and fields
do_settings_sections( 'mypl-settings' );
submit_button();
?>
</form>
</div>
<?php
}Actions and filters with priority management:
❌ BAD: Filter without return statement (fatal error)
<?php
add_filter( 'the_content', 'mypl_modify_content' );
function mypl_modify_content( $content ) {
if ( is_single() ) {
$content .= '<p>Footer text</p>';
}
// Missing return! Fatal error.
}✅ GOOD: Complete hooks usage with priority and return
<?php
// Action with priority (runs before core at priority 10)
add_action( 'init', 'mypl_register_cpt', 9 );
function mypl_register_cpt() {
register_post_type( 'mypl_book', array( /* args */ ) );
}
// Filter with return (CRITICAL)
add_filter( 'the_content', 'mypl_modify_content', 10, 1 );
function mypl_modify_content( $content ) {
if ( is_single() ) {
$content .= '<p>Footer text</p>';
}
return $content; // ALWAYS return in filters
}
// Multiple parameters with accepted_args
add_filter( 'wp_mail', 'mypl_modify_email', 10, 1 );
function mypl_modify_email( $args ) {
$args['from'] = 'Custom <custom@example.com>';
return $args;
}
// Removing hooks (MUST match exact registration params)
add_action( 'wp_footer', 'mypl_footer_code', 15 );
// To remove: priority must match
remove_action( 'wp_footer', 'mypl_footer_code', 15 );
// Create custom hooks for extensibility
function mypl_process_data( $data ) {
// Allow other plugins to modify data
$data = apply_filters( 'mypl_before_process', $data );
// Process data
$result = process( $data );
// Allow other plugins to hook after processing
do_action( 'mypl_after_process', $result );
return $result;
}Text domain matching and placeholder usage:
❌ BAD: Variable embedded in translatable string
<?php
$count = 5;
echo __( "You have $count messages", 'my-plugin' ); // Breaks translation extraction
// Missing text domain
echo __( 'Save Settings' );
// Text domain mismatch
// Plugin folder: my-awesome-plugin
echo __( 'Save', 'my_plugin' ); // Underscore instead of dash✅ GOOD: Placeholders with sprintf, correct text domain
<?php
// Basic translation with text domain
echo __( 'Settings saved successfully', 'my-awesome-plugin' );
// Translation with echo
_e( 'Click here to continue', 'my-awesome-plugin' );
// Placeholders with sprintf (NOT embedded variables)
$count = 5;
echo sprintf(
__( 'You have %d messages', 'my-awesome-plugin' ),
$count
);
// Pluralization with _n()
echo sprintf(
_n(
'One post found',
'%d posts found',
$count,
'my-awesome-plugin'
),
number_format_i18n( $count )
);
// Context-aware translation with _x()
echo _x( 'Post', 'noun - blog post', 'my-awesome-plugin' );
echo _x( 'Post', 'verb - submit', 'my-awesome-plugin' );
// Escaped output + translation
echo '<h1>' . esc_html__( 'Welcome', 'my-awesome-plugin' ) . '</h1>';
echo '<input placeholder="' . esc_attr__( 'Enter your name', 'my-awesome-plugin' ) . '">';
// Load text domain on init (optional in WP 4.6+)
add_action( 'init', 'mypl_load_textdomain' );
function mypl_load_textdomain() {
load_plugin_textdomain(
'my-awesome-plugin',
false,
dirname( plugin_basename( __FILE__ ) ) . '/languages'
);
}❌ BAD: Missing permission_callback (WordPress 5.5+ error)
<?php
add_action( 'rest_api_init', 'mypl_register_routes' );
function mypl_register_routes() {
register_rest_route( 'mypl/v1', '/posts', array(
'methods' => 'GET',
'callback' => 'mypl_get_posts',
// Missing: 'permission_callback' (WordPress 5.5+ blocks this)
) );
}✅ GOOD: Complete REST endpoint with permission_callback and validation
<?php
add_action( 'rest_api_init', 'mypl_register_routes' );
function mypl_register_routes() {
// Public read-only endpoint
register_rest_route(
'mypl/v1', // Namespace with version
'/posts',
array(
'methods' => 'GET',
'callback' => 'mypl_get_posts',
'permission_callback' => '__return_true', // Explicitly public (OK for read-only)
'args' => array(
'per_page' => array(
'default' => 10,
'sanitize_callback' => 'absint',
'validate_callback' => function( $param ) {
return is_numeric( $param ) && $param > 0 && $param <= 100;
},
),
),
)
);
// Protected write endpoint
register_rest_route(
'mypl/v1',
'/posts/(?P<id>\d+)',
array(
'methods' => 'DELETE',
'callback' => 'mypl_delete_post',
'permission_callback' => function() {
return current_user_can( 'delete_posts' );
},
'args' => array(
'id' => array(
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
),
),
)
);
}
function mypl_get_posts( $request ) {
$per_page = $request->get_param( 'per_page' );
$posts = get_posts( array( 'posts_per_page' => $per_page ) );
return rest_ensure_response( $posts );
}
function mypl_delete_post( $request ) {
$post_id = $request->get_param( 'id' );
$result = wp_delete_post( $post_id, true );
if ( ! $result ) {
return new WP_Error(
'delete_failed',
__( 'Failed to delete post', 'my-plugin' ),
array( 'status' => 500 )
);
}
return rest_ensure_response( array( 'deleted' => true, 'id' => $post_id ) );
}Note: For security depth on REST endpoints (nonce validation, capability checks, input sanitization), see wp-security-review skill.
✅ GOOD: AJAX handler registration
<?php
// Authenticated users
add_action( 'wp_ajax_mypl_save_data', 'mypl_save_data_callback' );
// Public (non-authenticated)
add_action( 'wp_ajax_nopriv_mypl_save_data', 'mypl_save_data_callback' );
function mypl_save_data_callback() {
// Security checks (see wp-security-review for depth)
check_ajax_referer( 'mypl_nonce', 'nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( 'Insufficient permissions' );
}
$data = sanitize_text_field( $_POST['data'] );
// Process data
update_option( 'mypl_data', $data );
wp_send_json_success( 'Data saved' );
}Separate admin from public code:
❌ BAD: Admin code loading on every page
<?php
add_action( 'wp_enqueue_scripts', 'mypl_enqueue_admin_assets' );
function mypl_enqueue_admin_assets() {
// Admin assets loading on public pages (unnecessary)
wp_enqueue_script( 'mypl-admin', plugin_dir_url( __FILE__ ) . 'admin.js' );
}✅ GOOD: Conditional admin loading
<?php
// Only load admin code in admin context
if ( is_admin() ) {
require_once plugin_dir_path( __FILE__ ) . 'admin/class-admin.php';
}
// Admin-specific assets
add_action( 'admin_enqueue_scripts', 'mypl_enqueue_admin_assets' );
function mypl_enqueue_admin_assets() {
wp_enqueue_script(
'mypl-admin',
plugin_dir_url( __FILE__ ) . 'admin/admin.js',
array( 'jquery' ),
'1.0.0',
true
);
}
// Public-facing assets
add_action( 'wp_enqueue_scripts', 'mypl_enqueue_public_assets' );
function mypl_enqueue_public_assets() {
wp_enqueue_script(
'mypl-public',
plugin_dir_url( __FILE__ ) . 'public/public.js',
array( 'jquery' ),
'1.0.0',
true
);
}Prevent fatal errors from conflicts:
✅ GOOD: Existence checks for functions and classes
<?php
// Function existence check
if ( ! function_exists( 'mypl_init' ) ) {
function mypl_init() {
// Plugin initialization
}
}
// Class existence check
if ( ! class_exists( 'MyPL_Plugin' ) ) {
class MyPL_Plugin {
// Plugin class
}
}| Severity | Definition | Examples |
|---|---|---|
| CRITICAL | Will cause WordPress.org rejection OR breaks plugin functionality OR causes fatal errors | Missing Plugin Name header, unprefixed global functions causing namespace collision, text domain mismatch, missing ABSPATH check, direct DB table creation without dbDelta, filter callback without return statement, post type key > 20 chars, taxonomy name > 32 chars, flush_rewrite_rules() on init hook, REST route without permission_callback (WP 5.5+) |
| WARNING | Non-standard patterns causing maintainability or compatibility issues, but plugin still functions | Missing internationalization (i18n), __() without text domain, hardcoded /wp-content/ paths, missing sanitize_callback in register_setting(), missing show_in_rest for Gutenberg support, wp_schedule_event() without checking for existing schedule, transients without expiration |
| INFO | Best practice improvements and modernization opportunities | Could use PHP namespaces instead of prefixing, missing version requirements in header (Requires at least, Requires PHP), admin code loading on frontend, missing Domain Path in header, could use _x() for context-aware translation |
Report findings grouped by FILE, with line numbers and severity labels. Use BAD/GOOD code pairs for each finding.
# WordPress Plugin Review: my-awesome-plugin
## FILE: my-awesome-plugin.php
### Line 15: CRITICAL - Missing ABSPATH check
Direct file access possible. Add ABSPATH check at top of all PHP files.
❌ BAD:
<?php
/**
* Plugin Name: My Plugin
*/
function mypl_init() { ... }
✅ GOOD:
<?php
/**
* Plugin Name: My Plugin
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function mypl_init() { ... }
### Line 45: WARNING - Missing text domain in i18n call
Translation won't work. Always include text domain parameter.
❌ BAD:
echo __( 'Save Settings' );
✅ GOOD:
echo __( 'Save Settings', 'my-awesome-plugin' );
## FILE: includes/class-cpt.php
### Line 23: CRITICAL - flush_rewrite_rules on init hook
Expensive DB write on every page load. Move to activation hook.
❌ BAD:
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'book', array( /* ... */ ) );
flush_rewrite_rules(); // Runs on EVERY request
}
✅ GOOD:
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'mypl_book', array( /* ... */ ) );
}
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_activate() {
mypl_register_cpt();
flush_rewrite_rules(); // Only on activation
}
## SUMMARY
**Total issues: 15**
- CRITICAL: 4 (must fix for WordPress.org submission)
- WARNING: 8 (non-standard patterns)
- INFO: 3 (best practice improvements)
**WordPress.org readiness:** NOT READY - 4 critical issues block submission
**Security note:** This review focused on plugin architecture and WordPress.org compliance. Security issues detected (missing nonce in AJAX handler, unsanitized input in form). Run `/wp-sec-review` for comprehensive security analysis.Patterns that look like issues but are NOT problems:
| Pattern | Why It's NOT a Problem | Context |
|---|---|---|
| mu-plugin without activation hooks | Must-use plugins load automatically, don't activate/deactivate | Normal for mu-plugins in wp-content/mu-plugins/ |
| Drop-in plugin without standard header | Drop-ins use special filenames, not plugin headers | Valid for object-cache.php, db.php, advanced-cache.php, etc. |
| Private plugin without readme.txt | readme.txt only required for WordPress.org submissions | Private/enterprise plugins use internal docs |
__return_true in REST permission_callback for GET | Public read-only endpoints don't need authentication | Correct pattern for public REST API reads |
| Settings API sanitize_callback changing type | Can convert checkbox string to boolean, text to array, etc. | Valid transformation, not a bug |
| No text domain for core WordPress strings | Core strings already translated | Valid when passing strings to wp_die(), __() with core strings |
| Short prefix (3 chars) with namespace | PHP namespace provides collision protection | Acceptable when using namespace MyPlugin\Feature; |
| No activation hook when plugin has no setup | Not all plugins need database tables, options, or rewrite flushes | Valid for simple plugins (shortcodes, filters only) |
flush_rewrite_rules() in admin settings save | Acceptable when user explicitly changes permalink structure | Valid in settings pages when slug/rewrite changes |
Direct $wpdb->query() for DROP TABLE | dbDelta() is for CREATE/ALTER, not DROP | Correct pattern in uninstall.php |
| Missing i18n on internal debug strings | Developer-facing strings don't need translation | Valid for error_log(), internal comments |
register_taxonomy() without show_in_rest | Not all taxonomies need Gutenberg UI | Valid for backend-only taxonomies |
Quick checklist for Plugin Check standards before WordPress.org submission:
defined( 'ABSPATH' ) || exit;)/wp-content/ or /wp-content/plugins/ pathseval(), base64_decode(), or obfuscated codeRequires at least, Requires PHP)Verification command:
wp plugin install plugin-check --activate
wp plugin check my-plugin.phpFor advanced plugin development patterns, load these companion reference documents:
| Task | Reference to Load |
|---|---|
| Plugin file structure, singleton patterns, autoloading, dependency injection, main plugin class architecture | references/architecture-patterns.md |
| Action/filter lifecycle, priority system, hook removal, custom hooks, pluggable functions, hook naming conventions | references/hooks-guide.md |
| Class-based plugin architecture, PHP namespaces, PSR-4 autoloading, trait usage, abstract classes, service containers | references/oop-patterns.md |
| Settings API workflow, Options API best practices, REST API schema validation, Transients API, custom database tables with dbDelta | references/api-patterns.md |
Note: Reference docs provide deep-dive content. This SKILL.md is self-sufficient for standard plugin reviews.
Security crossover: When encountering security-relevant patterns (form handlers without nonces, AJAX without capability checks, REST endpoints without permission validation), this skill provides brief reminders but defers to wp-security-review for comprehensive security analysis. The three-step security pattern (nonce + capability + sanitize) applies to all state-changing operations. Use /wp-sec-review command for detailed security audit.
© jorgerosal, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 4 other files (references) in claude-skills/wp-plugin-development of jorgerosal/wordpress-skills.
Open the folder on GitHubat commit 8c96442
Wp Plugin Development next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Wp Plugin Development this skilljorgerosal/wordpress-skills | 100 | — | ~12k | Automated safety check: Pass | MIT | |
| IntegrationsAutomattic/wordpress-activitypub | 582 | — | ~1.4k | Automated safety check: Pass | MIT | |
| I18n Lang FilesMes-Open/OpenMes | 150 | — | ~1.1k | Automated safety check: Pass | AGPL-3.0 | |
| WordPress Code GuardamElnagdy/guard-skills | 1.3k | — | ~2.4k | Automated safety check: Pass | MIT | |
| Tidyfactor StylerTidyFactor/Styler | 118 | — | ~1.7k | Automated safety check: Pass | Apache-2.0 | |
| Vue Route Breadcrumbschamilo/chamilo-lms | 1k | — | ~2k | Automated safety check: Pass | GPL-3.0 |
Automattic/wordpress-activitypub
Third-party WordPress plugin integration patterns. An agent skill from Automattic/wordpress-activitypub.
Mes-Open/OpenMes
Editing or merging backend/lang/en.json and pl.json. An agent skill from Mes-Open/OpenMes.
amElnagdy/guard-skills
Reviews WordPress plugin, theme and block code after an agent writes or edits it, catching missing escaping, nonces, capability checks and unprepared queries.
TidyFactor/Styler
Production framework styler and surgical RTL UI polish engine with Contextual Decision Layer (CDL).
chamilo/chamilo-lms
Gives a route in Chamilo's Vue app its breadcrumb entirely from router meta fields, without editing the Breadcrumb component or naming pages.
Zhongye1/KnowAgenticRAG
FastAPI Best Architecture (fba) project development guide. An agent skill from Zhongye1/KnowAgenticRAG.
jorgerosal/wordpress-skills
WordPress accessibility review for themes, blocks, plugins, and admin interfaces.
jorgerosal/wordpress-skills
WordPress ACF and content modeling review. An agent skill from jorgerosal/wordpress-skills.
jorgerosal/wordpress-skills
WordPress admin UI review and development guidance. An agent skill from jorgerosal/wordpress-skills.
jorgerosal/wordpress-skills
WordPress CI/CD and release engineering review guidance. An agent skill from jorgerosal/wordpress-skills.
jorgerosal/wordpress-skills
Headless WordPress and WPGraphQL review guidance. An agent skill from jorgerosal/wordpress-skills.
jorgerosal/wordpress-skills
WordPress migration and upgrade review. An agent skill from jorgerosal/wordpress-skills.
WordPress plugin architecture review and WordPress.org submission standards. Wp Plugin Development is an agent skill from jorgerosal/wordpress-skills.org submission standards.
Wp Plugin Development fits situations like: reviewing WordPress plugin code; auditing plugin architecture; preparing WordPress.org plugin submission; checking plugin best practices.
Run `npx skills add jorgerosal/wordpress-skills --skill wp-plugin-development -a claude-code`. Or copy the skill folder (claude-skills/wp-plugin-development in jorgerosal/wordpress-skills) into .claude/skills/wp-plugin-development in your project. Claude Code loads it when a task matches its description.
Run `npx skills add jorgerosal/wordpress-skills --skill wp-plugin-development -a codex`. Or copy the skill folder (claude-skills/wp-plugin-development in jorgerosal/wordpress-skills) into .agents/skills/wp-plugin-development in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add jorgerosal/wordpress-skills --skill wp-plugin-development -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/wp-plugin-development, .gemini/skills/wp-plugin-development, .github/skills/wp-plugin-development and .opencode/skills/wp-plugin-development in your project.
Going by SKILL.md and its folder, Wp Plugin Development needs the command-line tools its instructions call (rg).
SKILL.md names 1 domain. In commands or code: gnu.org; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Wp Plugin Development is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 12k tokens (SKILL.md is roughly 46k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 24k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Wp Plugin Development: Integrations (Automattic/wordpress-activitypub, 582 stars), I18n Lang Files (Mes-Open/OpenMes, 150 stars), WordPress Code Guard (amElnagdy/guard-skills, 1.3k stars) and Tidyfactor Styler (TidyFactor/Styler, 118 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
jorgerosal (a GitHub user) maintains it in jorgerosal/wordpress-skills, which has 100 GitHub stars. The repository holds 35 skills in this directory. The repository was last updated on June 7, 2026.
Source: jorgerosal/wordpress-skills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.