WordPress core, plugin/theme development, and security hardening reference.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add Dev-Toolbelt/dev-team-agents --skill wordpress --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Wordpress?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/dev-toolbelt-wordpress)More formats (shields.io, HTML) on the badges page.
---
name: wordpress
description: WordPress core, plugin/theme development, and security hardening reference.
---
## Detection Signals
- `wp-config.php`, `wp-content/`, `wp-includes/` in the project tree
- `functions.php` inside a `wp-content/themes/<theme>/` directory
- A plugin main file with a `Plugin Name:` header comment in `wp-content/plugins/<plugin>/`
- `composer.json` requiring `johnpbloch/wordpress` or `roots/wordpress`
- `wp` CLI usage in scripts (`wp-cli.yml`, `wp plugin`, `wp db`)
## Core Development Rules
| Rule | Detail |
|------|--------|
| Never edit WordPress core | Core files are overwritten on update. Extend via plugins/themes/hooks only. |
| Use hooks, not core patches | `add_action()` / `add_filter()` for all behavior changes. Never modify `wp-includes/` or `wp-admin/`. |
| Prefix everything | Functions, classes, options, hooks, transients, and DB tables must use a unique prefix (`myplugin_`) — avoids collisions with other plugins/themes. |
| Enqueue, never hardcode | Load CSS/JS via `wp_enqueue_script()` / `wp_enqueue_style()` with proper dependency arrays and version strings (cache busting). Never `<script>`/`<link>` directly in templates. |
| Use the `$wpdb` API or WP_Query | Avoid raw `mysql_*`/PDO connections outside WordPress's abstraction — breaks multisite/table-prefix portability. |
| Internationalize strings | Wrap user-facing text in `__()`/`_e()`/`esc_html__()` with a text domain matching the plugin/theme slug. |
| Child themes for customization | Never edit a parent/purchased theme directly — create a child theme (`style.css` with `Template:` header) so updates don't wipe changes. |
| Autoloader / namespaces for larger plugins | PSR-4 via Composer autoload for plugins beyond a few files; avoid giant single-file plugins. |
## Security — Non-Negotiable Rules
WordPress's attack surface is dominated by plugins/themes, not core. Apply these on every piece of code touching user input, output, or the database.
| Vulnerability class | Rule |
|---|---|
| **SQL Injection** | Never concatenate variables into SQL. Use `$wpdb->prepare()` for every query with a variable, always with placeholders (`%s`, `%d`, `%f`) — never manual string interpolation. |
| **XSS (stored/reflected)** | Escape ALL output at the point of echo: `esc_html()`, `esc_attr()`, `esc_url()`, `esc_js()`, `wp_kses_post()` for rich content. Never trust `$_GET`/`$_POST`/`$_REQUEST` directly in HTML. |
| **CSRF** | Every state-changing action (form, AJAX, admin-post) must verify a nonce: `wp_nonce_field()` / `wp_verify_nonce()` or `check_admin_referer()`. Never rely on `is_admin()` alone as protection. |
| **Broken access control** | Gate every privileged action with `current_user_can( 'capability' )` — never trust `is_admin()`, role names, or hidden UI as the only barrier. Check capability on every AJAX/REST callback too, not just page load. |
| **Unauthenticated AJAX/REST exposure** | `wp_ajax_nopriv_*` hooks and public REST routes (`permission_callback`) must independently validate input and, if privileged, must not exist — don't assume "no menu link" is protection. |
| **File upload / arbitrary file write** | Validate MIME type and extension against an allowlist (`wp_check_filetype`), never trust the client `Content-Type`. Disable PHP execution in the uploads directory when possible. Never let user input build a filesystem path unsanitized (path traversal via `../`). |
| **Insecure deserialization** | Never `unserialize()` user-controlled data — use `maybe_unserialize()` only on trusted stored data, prefer `json_decode()` for anything from the client. |
| **SSRF via `wp_remote_*`** | Validate/allowlist destination hosts before calling `wp_remote_get/post()` with user-supplied URLs. |
| **Hardcoded secrets** | API keys, DB credentials, salts belong in `wp-config.php` (outside webroot when possible) or environment variables — never committed in plugin/theme code. |
| **Object injection via magic methods** | Avoid `__wakeup()`/`__destruct()` gadget chains — audit any class that could be unserialized from user input. |
| **Directory listing / info disclosure** | Ensure `index.php` stub files exist in plugin/theme subdirectories; never expose `debug.log`, `.env`, or `wp-config.php.bak` in the webroot. |
| **User enumeration** | Don't build custom endpoints that leak usernames/emails via ID iteration (`?author=1`) beyond what core already restricts. |
| **Outdated dependencies** | Flag plugins/themes with no updates in 2+ years or below the "tested up to" WP version — these are the top real-world compromise vector, not custom code. |
**Review checklist to run before shipping any plugin/theme code:**
1. Every SQL query with a variable uses `$wpdb->prepare()`
2. Every echoed value is escaped with the correct `esc_*()` function for its context
3. Every form/AJAX/REST write action has nonce verification AND a capability check
4. Every file upload validates type/extension server-side
5. No `eval()`, `create_function()`, `unserialize()` on untrusted input, or dynamic `include`/`require` built from user input
## Plugin/Theme Structure Baseline
```
wp-content/plugins/my-plugin/
├── my-plugin.php ← main file with Plugin Name header, minimal bootstrap only
├── includes/ ← classes, hooks, business logic
├── admin/ ← admin-only screens, enqueued only on admin
├── assets/ ← css/js/images, enqueued via wp_enqueue_*
├── languages/ ← .pot/.po/.mo for i18n
└── uninstall.php ← cleans up options/tables on deletion (not deactivation)
```
## Performance
- Use transients (`set_transient()`/`get_transient()`) to cache expensive queries/API calls, not custom cron-based caching unless transients don't fit.
- Avoid `WP_Query` with `posts_per_page => -1` on large sites — paginate.
- Load scripts/styles conditionally (`is_page()`, `is_singular()`) — don't enqueue plugin assets site-wide.
- Prefer REST API / `admin-ajax.php` with proper caching headers over polling.
## Multisite Awareness
If `is_multisite()` is relevant: use `switch_to_blog()`/`restore_current_blog()` correctly (always paired), site-specific options via `get_blog_option()`, and never assume a single `wp_options` table.
## When Not to Use WordPress Idioms
If the project only borrows WordPress data (headless via REST/GraphQL, e.g. WPGraphQL) and the actual application is a separate frontend, apply this skill only to the WordPress backend/plugin code — the frontend consuming the API follows its own framework's skill (e.g. `skills/ui-libraries/*`).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!