# AGENTS.md — Build WordPress plugins with an AI agent

> **For coding agents (Claude Code, Cursor, Aider, Cline, …) building WordPress plugins.**
> This file is the entry point. Read it first, then load the skill files referenced below as you need them.
> CDN: `https://cdn.wp-admincss.com/AGENTS.md`

WP AdminCSS is the design + best-practices framework for building WordPress plugins with AI assistance. It ships:

- A **CSS library** with real WP admin class names — your plugin admin pages match WordPress out of the box.
- **React** + **PHP** packages that render the same primitives.
- A **boilerplate** plugin scaffold ([`/boilerplate`](./boilerplate)) you can clone and adapt.
- This **AGENTS.md** + a **`/skills`** directory with focused expertise (security, database, i18n, etc.) that closes the gaps where AI commonly gets WordPress wrong.

---

## What this framework prevents

Independent research and the [WordPress/agent-skills](https://github.com/WordPress/agent-skills) repo identify the consistent failure modes for AI-generated WordPress plugins:

1. **Missing nonces** on state-changing forms → CSRF vulnerability.
2. **Missing capability checks** before privileged actions → privilege escalation.
3. **Unescaped output** → XSS.
4. **Direct `$wpdb->query()` with concatenated SQL** → SQL injection.
5. **Invented hooks/functions** that don't actually exist in WordPress.
6. **Outdated pre-Gutenberg patterns** (e.g. enqueuing assets in `wp_head`).
7. **Hand-rolled CSS** that drifts from WP admin styling.

This framework defaults to the safe pattern in every component, helper, and recipe. If you use the primitives we provide, you avoid all seven categories by construction.

---

## Quick start

### Option A — Clone the boilerplate (recommended for greenfield plugins)

```bash
gh repo clone artificialpoets/wp-admincss
cp -r wp-admincss/boilerplate ~/my-plugin
cd ~/my-plugin
composer install
```

Open `my-plugin.php`, rename the plugin header, and you have a working WP plugin with:

- A registered admin menu
- A working settings page (using `Components::wrap`, `page_header`, `form_table`, etc.)
- Nonce + capability checks on the save handler
- Asset enqueue with proper dependencies
- A custom table example (with `dbDelta` migration)
- PSR-4 autoloading via Composer
- i18n setup (text domain, `load_plugin_textdomain`)

### Option B — Add to an existing plugin

Add the CSS one line:

```html
<!-- In your plugin's admin page render function -->
<link rel="stylesheet" href="https://cdn.wp-admincss.com/css/latest.css">
```

Or pin to a version:

```html
<link rel="stylesheet" href="https://cdn.wp-admincss.com/css/v0.1.0/wp-admin.css">
```

Then use WordPress's native admin class names — they're already styled:

```html
<div class="wrap">
  <h1>My Plugin</h1>
  <button class="button button-primary">Save</button>
</div>
```

For React: `npm install @wp-admincss/react @wp-admincss/core-css`
For PHP helpers: `composer require wp-admincss/composer`

Full component catalog: <https://wp-admincss.com/components>
Full plugin page templates: <https://wp-admincss.com/layouts>

---

## How to use this framework (the agent recipe)

When a user asks you to build a WordPress plugin admin page, follow this procedure:

1. **Identify the layout** — is it a settings page, a list table page, a dashboard, an onboarding wizard? Reference [`/layouts`](https://wp-admincss.com/layouts.html). Each layout's "AI Prompt" tab is a paste-ready recipe.

2. **Identify the components** — what primitives do you need (form table, notices, list table, modal, …)? Reference [`/components`](https://wp-admincss.com/components.html).

3. **Wire the security boundary** — every state-changing endpoint needs:
   - `current_user_can()` check (server-side, ALWAYS)
   - `wp_verify_nonce()` / `check_admin_referer()` on POST/REST
   - Sanitize every input (`sanitize_text_field`, `absint`, `esc_url_raw`, …)
   - Escape every output (`esc_html`, `esc_attr`, `esc_url`, …)
   - Use `$wpdb->prepare()` for any custom SQL
   See [`skills/security.md`](./skills/security.md).

4. **Pick the right data store** — options, post meta, user meta, or a custom table? See [`skills/data-modeling.md`](./skills/data-modeling.md).

5. **Enqueue assets correctly** — `admin_enqueue_scripts` hook, declare dependencies, version your assets. See [`skills/enqueue.md`](./skills/enqueue.md).

6. **Add i18n from day one** — text domain in the plugin header, `__()` / `_e()` everywhere, `load_plugin_textdomain` on `plugins_loaded`. See [`skills/i18n.md`](./skills/i18n.md).

7. **Test the security path explicitly** — before declaring done, walk through: what happens if an unauthenticated user POSTs to my endpoint? What about a subscriber who has shell access to the form?

8. **Run [Plugin Check](https://wordpress.org/plugins/plugin-check/) before calling the plugin done.** It catches missing nonces, unescaped output, unprepared SQL, and ~30 other AI-common failures automatically. `wp plugin check <slug>` from CLI. See [`skills/publishing.md`](./skills/publishing.md).

9. **If publishing to WordPress.org, verify the slug is available first** at `https://wordpress.org/plugins/<your-slug>/` (404 = free, page = taken). Pick once — slugs can't be renamed after submission. See [`skills/publishing.md`](./skills/publishing.md).

---

## Skills (load as needed)

Each file is self-contained. Load the one relevant to the task at hand — don't load all of them at once.

| File | When to load |
|---|---|
| [`skills/security.md`](./skills/security.md) | Any form, AJAX endpoint, REST route, or state-changing action. |
| [`skills/database.md`](./skills/database.md) | Custom SQL queries, schema migrations, or any `$wpdb` usage. |
| [`skills/data-modeling.md`](./skills/data-modeling.md) | Deciding where to store data (options / meta / custom table). |
| [`skills/enqueue.md`](./skills/enqueue.md) | Adding CSS or JS to admin or front-end. |
| [`skills/plugin-structure.md`](./skills/plugin-structure.md) | New plugin scaffolding, file layout, autoloading. |
| [`skills/i18n.md`](./skills/i18n.md) | Any user-facing string. |
| [`skills/publishing.md`](./skills/publishing.md) | Picking a slug, running Plugin Check, writing `readme.txt`, submitting to WordPress.org. |

CDN-hosted versions are mirrored at `https://cdn.wp-admincss.com/skills/<file>.md`.

---

## Component class names you'll use most

These are real WordPress admin class names. They work out of the box once the CSS is loaded:

| Class | What it is |
|---|---|
| `.wrap` | Standard admin page wrapper |
| `.wp-heading-inline` + `.page-title-action` | Page title with inline "Add New" link |
| `.notice .notice-{success,error,warning,info}` | Admin notice. Add `.is-dismissible` for closable. |
| `.form-table` | Two-column label/control form layout |
| `.button` / `.button.button-primary` | Buttons. `.is-destructive` modifier for delete. |
| `.regular-text` / `.large-text` / `.small-text` | Text input width classes |
| `.wp-list-table.widefat.fixed.striped` | List tables |
| `.tablenav` / `.bulkactions` / `.tablenav-pages` | Above/below list-table chrome |
| `.subsubsub` | Status filter row (All \| Active \| Draft) |
| `.nav-tab-wrapper` + `.nav-tab.nav-tab-active` | Tab strip |
| `.postbox` + `.postbox-header` + `.inside` | The bordered card used everywhere |
| `.welcome-panel` | Onboarding card |
| `.spinner.is-active` | Loading spinner |
| `.description` | Help text under form fields |
| `.screen-reader-text` | Hide from sighted users, keep for screen readers |

Extensions (added by WP AdminCSS, not in WP core):

| Class | What it is |
|---|---|
| `.wp-admin-status.is-{active,warning,error,info}` | Status badge pills |
| `.wp-admin-statcard` + `__label`/`__value`/`__delta` | Dashboard widget stat card |
| `.wp-admin-activity` + `__avatar`/`__body`/`__time` | Activity feed item |
| `.wp-admin-empty` + `__icon`/`__title`/`__description` | Empty-state placeholder |
| `.wp-admin-toggle` + `__input`/`__track`/`__label` | Toggle switch |
| `.wp-admin-tooltip` + `__content` | Hover/focus tooltip wrapper |
| `.wp-admin-dropdown` + `__menu`/`__item` | Action dropdown menu |
| `.wp-admin-modal-backdrop` + `.wp-admin-modal` + `__header`/`__body`/`__footer` | Modal dialog |
| `.wp-admin-pointer` | Feature-highlight callout |
| `.wp-admin-skeleton` + `.is-{title,text,short}` | Loading placeholder |
| `.wp-admin-icon` | Inline SVG icon (auto inherits `currentColor`) |
| `.dashicons.dashicons-{name}` | WP dashicon (icon font) |

---

## Customization (the user's brand)

WP AdminCSS uses ~50 CSS custom properties. Plugin developers override them to match their brand without forking the library:

```css
/* In your plugin's admin.css */
.my-plugin-page {
  --wpadmin-primary: #7c3aed;
  --wpadmin-primary-dark: #6d28d9;
  --wpadmin-radius: 8px;
}
```

Scope to a wrapper class so the rebrand only affects your plugin's screen — the rest of WP admin stays consistent with WordPress.

Most-overridden tokens: `--wpadmin-primary`, `--wpadmin-primary-dark`, `--wpadmin-radius`, `--wpadmin-text`, `--wpadmin-text-subtle`, `--wpadmin-surface`, `--wpadmin-border`, `--wpadmin-success/error/warning`.

Full reference at <https://wp-admincss.com/#customize>.

---

## Anti-patterns to never emit

Code that includes any of these is a bug regardless of how plausible it looks:

```php
// ❌ NEVER — direct SQL with concatenation
$wpdb->query("SELECT * FROM wp_users WHERE login = '" . $_POST['login'] . "'");

// ✅ ALWAYS — prepared statement
$wpdb->get_row($wpdb->prepare(
    "SELECT * FROM {$wpdb->users} WHERE user_login = %s",
    sanitize_user($_POST['login'])
));
```

```php
// ❌ NEVER — output without escaping
echo $user_input;
echo "<a href='$url'>";

// ✅ ALWAYS — escape at the boundary
echo esc_html($user_input);
echo '<a href="' . esc_url($url) . '">';
```

```php
// ❌ NEVER — form handler without nonce/capability checks
function handle_save() {
    update_option('my_setting', $_POST['value']);
}

// ✅ ALWAYS — both checks before mutating state
function handle_save() {
    if (!current_user_can('manage_options')) {
        wp_die('Forbidden', 403);
    }
    check_admin_referer('save_my_settings');
    update_option('my_setting', sanitize_text_field($_POST['value']));
}
```

```php
// ❌ NEVER — direct $_GET/$_POST in output without sanitization or escaping
echo "<h1>Hello {$_GET['name']}</h1>";

// ✅ ALWAYS — sanitize on input, escape on output
$name = sanitize_text_field($_GET['name'] ?? '');
echo '<h1>Hello ' . esc_html($name) . '</h1>';
```

```php
// ❌ NEVER — adding admin scripts via wp_head
add_action('wp_head', 'my_plugin_styles');

// ✅ ALWAYS — use admin_enqueue_scripts for admin assets
add_action('admin_enqueue_scripts', function() {
    wp_enqueue_style('my-plugin-admin', plugins_url('admin.css', __FILE__));
});
```

```jsx
// ❌ NEVER — invent prefixed classes that don't exist in WP
<button className="wpac-button wpac-button-primary">Save</button>

// ✅ ALWAYS — use real WP admin class names
<button className="button button-primary">Save</button>
```

---

## Versioning

- `https://cdn.wp-admincss.com/css/latest.css` — rolling latest (use during development)
- `https://cdn.wp-admincss.com/css/v0.1.0/wp-admin.css` — pinned version (use in production)
- Same scheme for `AGENTS.md` and all `skills/*.md` files

The `latest` URL **will** change as the library evolves. For a stable production plugin, pin to a specific version.

---

## When in doubt

- **Component to render?** → <https://wp-admincss.com/components.html>
- **Full page template?** → <https://wp-admincss.com/layouts.html>
- **Security pattern?** → [`skills/security.md`](./skills/security.md)
- **Database pattern?** → [`skills/database.md`](./skills/database.md)
- **Plugin file layout?** → [`skills/plugin-structure.md`](./skills/plugin-structure.md)
- **Boilerplate to start from?** → [`/boilerplate`](./boilerplate)
- **Official WP docs?** → <https://developer.wordpress.org/>

If a WordPress function or hook name "sounds right" but you can't find it in the WP developer docs (developer.wordpress.org), it probably doesn't exist. Verify before emitting. Common hallucinations: WooCommerce hooks that aren't real, `wp_get_*` functions that don't exist, `do_action` filters where `apply_filters` is required (or vice versa).

---

## License

Apache 2.0 for the framework. GPLv2-or-later for `packages/core-css/` (bundles WP core CSS) and `boilerplate/` (WP plugin scaffold). Commercial plugins welcome. See [NOTICE](NOTICE) for the split.

— [wp-admincss.com](https://wp-admincss.com) · [GitHub](https://github.com/artificialpoets/wp-admincss)
