Files
agent-skills/skills/wordpress-plugin-conventions/SKILL.md
T
iWP Claudy 39a637410c Initial skill library: 9 skills for delegate briefs
WordPress plugin rebrand/conventions/remote-CLI patterns, Gitea release
workflow, bastille jail provisioning, remote shell quoting safety, server
fleet map, delegate brief writing, and verification discipline -- all
derived from real incidents this session, plus two skills adapted (MIT
license, attributed) from obra/superpowers and andrej-karpathy-skills.
2026-08-02 20:08:29 +02:00

4.6 KiB

name, description
name description
wordpress-plugin-conventions Use when writing or modifying a WordPress plugin's PHP code. Baseline structural/security conventions to follow without being told each time.

WordPress Plugin Conventions

Baseline conventions for first-party WordPress plugin code. Follow these without needing to be told in every brief.

File structure

plugin-slug/
  plugin-slug.php        — main file: header, constants, bootstrap class
  includes/
    class-<prefix>-*.php — one class per concern, class-based not procedural
  admin/                  — admin-only UI (settings pages, dashboards)
    assets/{css,js}/
  languages/              — .pot/.po/.mo if the plugin is translatable

Main file skeleton

<?php
/**
 * Plugin Name: ...
 * Plugin URI: ...
 * Description: ...
 * Version: 1.0.0
 * Author: ...
 * Author URI: ...
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: plugin-slug
 * Requires PHP: 7.4
 */

if (!defined('ABSPATH')) {
    exit;
}

define('PREFIX_VERSION', '1.0.0');
define('PREFIX_PATH', plugin_dir_path(__FILE__));
define('PREFIX_URL', plugin_dir_url(__FILE__));

require_once PREFIX_PATH . 'includes/class-prefix-thing.php';

class Prefix_Main {
    private static $instance = null;
    public static function instance() {
        if (null === self::$instance) { self::$instance = new self(); }
        return self::$instance;
    }
    private function __construct() { /* hook registration only */ }
    public static function activate() { /* create tables/options/dirs */ }
    public static function deactivate() { /* reverse activate() side effects */ }
}
register_activation_hook(__FILE__, ['Prefix_Main', 'activate']);
register_deactivation_hook(__FILE__, ['Prefix_Main', 'deactivate']);
add_action('plugins_loaded', ['Prefix_Main', 'instance']);

Every included file starts with if (!defined('ABSPATH')) exit; — never define('ABSPATH', ...) && or any variant, exactly the guard-and-exit form, so the file can never be requested directly over HTTP.

Every file must pass php -l before you consider a task done

Not optional, not "probably fine" — actually run it, on every changed file, every time. This is the cheapest possible check and catches a meaningful fraction of real mistakes (typos, mismatched braces from a find/replace, etc.) before they ever reach a live site.

Security baseline

  • Nonces on every form/AJAX action: wp_nonce_field() / check_admin_referer() / check_ajax_referer().
  • Capability checks before any privileged action: current_user_can('manage_options') (or the narrowest capability that actually applies — don't default to manage_options for things a lower-privileged role should legitimately be able to do).
  • Escape on output, every time, using the context-correct function: esc_html(), esc_attr(), esc_url(), esc_js() — never raw-echo anything that traces back to user input or the database without one of these.
  • Sanitize on input: sanitize_text_field(), absint(), sanitize_email(), etc. — appropriate to the expected shape of the data, applied at the point the $_POST/$_GET value is first read.
  • $wpdb->prepare() for every query with a variable in it — no string- interpolated SQL, ever, no exceptions.

Database tables

If a plugin needs its own tables (not just options), use dbDelta():

require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$wpdb->prefix}prefix_thing (...) {$charset};";
dbDelta($sql);

Track a DB_VERSION constant + a stored option so future plugin updates can detect and re-run dbDelta() for schema changes.

PHP 7.4 compatibility

Unless a specific brief says otherwise, target PHP 7.4+ (a large fraction of real WordPress hosting is still on 7.4). Avoid PHP 8-only syntax (named arguments, enums, readonly properties, match). If a codebase needs str_contains/str_starts_with/str_ends_with (PHP 8.0+) on an environment that might run 7.4, guard with function_exists() polyfills rather than assuming they exist.

Don't build what WordPress core already gives you

Before writing custom code for: cron scheduling (wp_schedule_event), REST endpoints (register_rest_route), settings UI (register_setting/add_settings_field/woocommerce_admin_fields if WooCommerce context), object caching (wp_cache_* functions), HTTP requests (wp_remote_get/wp_remote_post, not raw curl) — check whether a core API already does it. Reinventing these is both wasted effort and a common source of subtle bugs core already solved correctly.