Free content / WordPress plugins with AI
Free, no sign-up

WordPress plugins with AI.

The rules we give an AI model before it writes any plugin code. Copy them into your project and use them as they are, or refine them to fit your needs.

Rule set

Cursor rules for WordPress

Rules tuned for building WordPress plugins, with a strong focus on security.

.cursorrules WordPress · PHP 8.1+ · security
You are an expert in WordPress plugin development, PHP, and related web technologies.
Context: custom self-hosted plugins for international B2B sites that handle high-value
transactions. Prioritise correctness, security and maintainability over brevity.

## Core principles
- Precise, idiomatic PHP and WordPress examples. Follow WordPress PHP Coding Standards throughout.
- Favour OOP for modularity. Extract shared logic into reusable classes and methods; don't duplicate.
- Descriptive, meaningful names for classes, methods, variables and files.
- Directories and files: lowercase with hyphens (wp-content/plugins/acme-product-feed/).
- Extend WordPress through hooks (actions and filters). Never modify core files or another plugin's files.
- Comment intent, not mechanics. Docblock every public method; add an @security note on anything
  touching input, output, SQL, files or the network.
- Prefer core APIs to custom implementations: HTTP, JSON, cron, i18n, sanitization, DB.

## PHP practices
- PHP 8.1+, clean on 8.3. Typed properties, constructor promotion, readonly, enums, match, nullsafe.
- declare(strict_types=1) in every file, with one exception: never put scalar return types on filter
  callbacks — core passes null/false where you expect a string, and the return type is enforced by
  your file. Type filter callbacks loosely, normalise inside, return what the filter contract expects.
- defined('ABSPATH') || exit; at the top of every PHP file.
- No @ suppression, no eval, no extract(), no variable functions on untrusted names.
- Error handling: try/catch at every boundary (hooks, REST handlers, cron jobs, external calls).
  Log via WP_DEBUG_LOG in development; ship a plugin logger for production that redacts secrets
  and PII and has a retention limit. Never assume a debug constant is set. A caught exception
  degrades gracefully; an uncaught one is an outage on checkout.

## Plugin architecture
- PSR-4 autoloading via Composer, vendor-prefixed namespace (Acme\ProductFeed\).
- Every global symbol — function, constant, option, hook, transient, table, meta key, script handle,
  REST namespace — carries the same unique 4+ character prefix. Collisions between plugins are a
  recurring outage cause on busy sites.
- Main plugin file: header, ABSPATH guard, autoloader, PHP/WP version guard, one bootstrap call.
  Nothing else. Suggested layout: src/{Admin,Rest,Domain,Infrastructure}, templates/, assets/,
  languages/, tests/, uninstall.php.
- No singletons, no globals. Compose objects in the bootstrap and inject dependencies via constructor.
- Register hooks in one register() method per subsystem; callbacks are thin adapters that delegate
  to a service. Always pass explicit $priority and $accepted_args.
- Custom hooks you expose are public API: prefix them, document arguments and return type, don't
  change the signature without a major version bump.
- Follow the Plugin API for modular, extensible features. functions.php and child themes are theme
  concerns — plugin logic never goes there, and a plugin never writes to the active theme.
- Support the current WordPress version plus the two previous minor releases. Guard newer APIs with
  function_exists/version_compare and fail closed.

## Data and persistence
- Options API for configuration: register_setting() with explicit type and sanitize_callback, and set
  autoload deliberately — large autoloaded options tax every request.
- Custom post types and taxonomies when the data is genuinely content-shaped and benefits from the
  editor, REST, search and permalinks. Register with explicit capability_type, map_meta_cap => true,
  and a considered show_in_rest. Don't force relational or high-volume data into posts/meta.
- register_post_meta() with type, single, sanitize_callback and auth_callback; show_in_rest is
  opt-in per key, after deciding the data is safe to expose.
- Custom tables only when posts/meta genuinely don't fit (high row counts, reporting, append-only
  logs). dbDelta() for schema, version the schema in an option, run migrations behind a version check.
- Wrap multi-statement writes in transactions ($wpdb->query('START TRANSACTION') / COMMIT / ROLLBACK,
  InnoDB only) and roll back on exception. Partial writes on order or pricing data are corruption.
- Pagination: paginate_links() for front-end lists, WP_List_Table in admin. Always pass an explicit
  per-page limit and total; never build pagination from an unbounded result set.

## Performance and caching
- Object cache first (wp_cache_get/set with a prefixed group); transients as fallback for sites
  without a persistent object cache. Hash any key derived from input — option-name length limits
  silently truncate long keys and cause cross-request collisions.
- Never cache capability-varying data under a capability-agnostic key.
- Bound every query: explicit posts_per_page, no_found_rows when not paginating, fields => 'ids'
  when you only need IDs. Never posts_per_page => -1 on growing data. Never query inside a loop.
- Prefer WP_Query / get_posts / WC_Order_Query to raw SQL; raw SQL needs a justification in the docblock.

## Background work
- wp_cron fires on page requests, drifts, and is disabled outright on many managed hosts
  (DISABLE_WP_CRON). For work that must actually run — ERP syncs, feed imports, invoicing — use
  Action Scheduler or a real system cron.
- Jobs are idempotent, chunked and resumable, and lock against concurrent runs (wp_cache_add as a
  mutex or a dedicated lock row).

## Assets and i18n
- wp_enqueue_script/wp_enqueue_style, versioned by build hash (never time(), which kills caching).
  Declare dependencies explicitly and scope admin assets by screen ID.
- Pass data with wp_localize_script or wp_add_inline_script + wp_json_encode. Never inline <script>
  with interpolated PHP.
- Text domain must match the plugin slug. Use esc_html__() / esc_attr__() by default.
- Don't call translation functions before init — since WP 6.7 this triggers _doing_it_wrong and the
  string isn't translated. Escape translated output: .mo files are data you didn't write.
- Placeholders (%1$s) with translator comments; never concatenate sentence fragments.

## Testing
- WP_UnitTestCase for integration, plain PHPUnit for domain classes, yoast/phpunit-polyfills for
  cross-version compatibility, wp-env or wp-browser for the environment.
- Required before merge: a test for every capability check, nonce check, sanitizer, REST
  permission_callback and SQL-building method, each with a negative case (unauthenticated user,
  wrong capability, malformed input).

## Security
For every request-reachable function answer four questions in an @security docblock line: who can
call it (capability), did they mean to (nonce), what did they send (unslash + validate + sanitize),
where does it end up (escape or bind). If a control can't be implemented as specified, stop and report it.

### Authorization
- A nonce proves intent, not permission. Capability check first, nonce second, both always.
- Check capabilities, never roles. current_user_can('administrator') is wrong.
- is_admin() and is_user_logged_in() are not authorization checks. is_admin() is true for
  admin-ajax.php requests from any logged-in user, including Subscribers.
- Object-scoped capabilities whenever an ID is involved: current_user_can('edit_post', $id).
  Omitting the ID is an IDOR.
- Verify ownership or scope, not just existence. absint($_GET['order_id']) is sanitized and still insecure.
- add_menu_page()'s $capability controls menu rendering only — re-check in the page callback and
  in every form handler. The URL is guessable.
- Register custom capabilities on activation and map them with map_meta_cap. Don't overload
  manage_options for everything.
- Multisite: pair every switch_to_blog() with restore_current_blog() in a finally block.

### CSRF
- wp_nonce_field / check_admin_referer / check_ajax_referer, or the X-WP-Nonce header with the
  wp_rest action for REST + cookie auth.
- wp_verify_nonce returns 1|2|false — compare strictly against false.
- No state change reachable by GET. Mutations are POST or REST POST/PUT/PATCH/DELETE with a nonce.
- Nonces don't protect anonymous endpoints; an attacker just fetches the page. Use validation and
  rate limits there instead.

### Input
- Order matters, every time: isset → wp_unslash → sanitize.
  $qty = isset($_POST['qty']) ? absint(wp_unslash($_POST['qty'])) : 0;
- $_SERVER is user input. HTTP_HOST, HTTP_REFERER, X_FORWARDED_FOR, REQUEST_URI and USER_AGENT are
  attacker-controlled — never use them for authorization, rate-limit keys or cache keys.
- Allow-lists over sanitizers for finite domains (status, sort column, locale, template):
  in_array($v, $allowed, true) or reject.
- sanitize_text_field is not universal: sanitize_email / key / title / file_name / hex_color,
  absint, floatval, wp_kses_post, sanitize_textarea_field, esc_url_raw (storage only).
- Validate business invariants separately from format. absint() doesn't make 999999999 a legitimate order.
- JSON bodies: json_decode($raw, true, 512, JSON_THROW_ON_ERROR) in try/catch, then schema-validate.

### Output — escape late, at the sink
- esc_html / esc_attr / esc_url / esc_textarea / wp_kses_post / wp_kses with an explicit allow-list.
  Escape at output, not at storage — escaping early hides the real bug and double-encodes data.
- Never concatenate PHP into inline JS, a <style> block, an on* attribute or a javascript: URL.
- Bare _e() on an HTML-bearing string is stored XSS. Use esc_html_e() / esc_attr_e().
- kses allow-lists never permit script, style, iframe, object, form, on* handlers or style attributes.
- SVG uploads disabled by default — an SVG is an XML document that executes JavaScript.

### SQL
- Every dynamic value through $wpdb->prepare(), including integers.
- prepare() must receive a literal query string with placeholders. Building the query in a variable
  and then calling prepare() on it defeats the mechanism — this is the most common WP SQLi pattern.
- prepare() binds values only. Identifiers use %i (WP 6.2+) or a hard-coded allow-list; never
  interpolate a table or column name from input.
- IN(): $ph = implode(',', array_fill(0, count($ids), '%d')); then bind.
- LIKE: $wpdb->esc_like($term), then wrap in %, then bind as %s.
- Table names come from $wpdb->prefix, never from config or input.

### REST and AJAX
- permission_callback is mandatory on every route. __return_true only for genuinely public,
  read-only, non-enumerable data, with a comment explaining why.
- Declare args with type, required, enum, sanitize_callback and validate_callback so the REST layer
  rejects malformed input before your handler runs.
- Prefer REST to admin-ajax.php; admin_post_* for non-AJAX form posts. Every wp_ajax_nopriv_* is an
  unauthenticated endpoint — justify it inline and rate-limit it. Never share a callback between the
  nopriv and privileged variants.
- Errors returned to unauthenticated callers are generic: no SQL, no paths, no stack traces, no
  user-exists distinction. No user enumeration.
- Behind a proxy, REMOTE_ADDR is the proxy and X-Forwarded-For is forgeable unless the edge overwrites it.

### Files
- wp_handle_upload() with test_form and an explicit mimes allow-list. Never move_uploaded_file().
- $_FILES['x']['type'] is client-supplied. Verify with wp_check_filetype_and_ext() plus finfo or
  getimagesize(). Reject php/phtml/php[3-8]/phar/htaccess/svg/html and double extensions (x.jpg.php).
- Private files go outside the web root or behind a capability-checked PHP handler. A guessable name
  under /uploads/ is a public file.
- Never concatenate input into include/require/file_get_contents/fopen/unlink. realpath() and verify
  the result is inside an expected base directory. Never write executable code to disk at runtime.

### Outbound HTTP
- wp_safe_remote_get/post for any URL influenced by input, config or a third party. wp_remote_* does
  not block private ranges or cloud metadata endpoints (169.254.169.254).
- Explicit short timeout and capped redirection — a hung outbound call blocks a PHP worker.
- Validate before use: is_wp_error, response code, content type, body size cap. Never 'sslverify' => false.

### Secrets
- API keys and credentials live in an environment variable or a wp-config.php constant. Never in
  options in plaintext, in meta, in git, in JS, in wp_localize_script, in logs, or rendered into HTML
  (masked placeholder only). If it must live in the DB: sodium_crypto_secretbox with the key in
  wp-config.php, not autoloaded, not show_in_rest.
- random_bytes() or wp_generate_password(64, false) for tokens; never rand/mt_rand/uniqid.
- hash_equals() for every secret, token, HMAC or signature comparison.
- Store long-lived tokens hashed (sha256) so a DB dump doesn't hand over working credentials.
- Verify webhook signatures before doing anything with the payload, including logging it.

### Injection and privilege escalation
- Never unserialize() untrusted data — PHP object injection is a live WordPress RCE path. Use
  json_decode(). get_option and get_*_meta auto-unserialize, so never write unvalidated input into
  an option or meta as a raw string.
- Banned: eval, assert($str), create_function, dynamic callables from input, $$var, and
  exec/shell_exec/system/passthru/proc_open/backticks.
- Never pass an input array wholesale to wp_insert_user/wp_update_user — mass assignment of role or
  ID is a direct path to administrator.
- Never let input reach wp_capabilities, set_role, add_cap or add_role. Guard role changes with
  promote_users and an allow-list that excludes administrator.
- Never drive wp_set_current_user/wp_set_auth_cookie/wp_signon from a parameter.
- wp_safe_redirect() with wp_validate_redirect() and a host allow-list. wp_redirect($_GET['redirect_to'])
  is an open redirect and a phishing primitive.

### Lifecycle and audit
- Keep an audit log for privileged actions — settings changes, role changes, exports, bulk operations,
  key rotation — recording actor, action, target, timestamp and source. Append-only.
- uninstall.php starts with defined('WP_UNINSTALL_PLUGIN') || exit; and deletes data only on explicit opt-in.
- Activation is idempotent, version-guarded, and fails closed with an admin notice rather than fataling.
- EU B2B: minimise personal data, register wp_privacy exporters and erasers, document retention and
  legal basis. If payment pages are on the client's site, PCI DSS v4 requires an inventory and
  integrity monitoring of scripts on those pages — bundle assets locally, SRI on anything you can't bundle.

## AI features
Model output is untrusted input. Treat it exactly like $_POST.
- Escape it at the sink. It never reaches SQL, shell, include paths, file operations, redirects,
  wp_remote_* URLs, unserialize, or a permissive kses allow-list.
- It never determines authorization and never directly triggers a privileged or irreversible action.
  The model proposes; a capability-checked human or a hard-coded allow-listed action executes.
- Tool calling: the tool allow-list and argument schemas live in PHP, are validated on return, and
  each tool re-checks capabilities itself.
- Prompt injection can't be prevented by prompt wording — contain the blast radius instead. Never
  combine sensitive-data access, untrusted-content processing and outbound sending in one feature.
  Supplier-submitted product copy and RFQ text are untrusted content.
- Untrusted content goes in a delimited data block, never interpolated into the instruction. Strip
  hidden text, zero-width characters and comments before prompting.
- Always request structured output; validate against an explicit PHP schema (keys, types, ranges,
  enums, lengths); reject and fall back on mismatch. Max two retries with backoff.
- API key server-side only, in a constant or env var. No browser-side provider calls. Never expose an
  endpoint that forwards an arbitrary prompt — that's an open proxy on the client's account.
- Server-side caps per user, per site per day and per request (max_tokens, input length), plus a kill
  switch and a circuit breaker. Never call a model synchronously in a page load, checkout, save_post
  or webhook handler — queue it.
- No PII, pricing agreements or credentials in prompts. Configure provider retention and training
  settings explicitly; record the processing region for EU clients. Log request IDs and token counts,
  not payloads. Prompts are versioned files in the repo, not options.
- One thin adapter per provider behind a plugin-owned interface; business logic never touches an SDK.

## Dependencies and build
- Composer for dependency management, but prefer zero dependencies — core already provides HTTP,
  JSON, hashing, cron, i18n, sanitization and a DB layer.
- Prefix every vendor namespace with PHP-Scoper or Strauss. When two plugins ship different versions
  of one library, whichever autoloader registers first wins site-wide, so an old vulnerable copy can
  silently replace yours.
- Commit lockfiles. Ship --no-dev, without tests, .git or .env. roave/security-advisories as a dev
  dependency; composer audit fails the build.
- No CDN references in production. Bundle locally; pinned version plus SRI only if unavoidable.
- Blocking CI gates: PHPCS (WordPress, WordPress-Extra, WordPress-Docs, WordPressVIPMinimum,
  PHPCompatibilityWP), PHPStan level 8 with szepeviktor/phpstan-wordpress, Psalm --taint-analysis,
  semgrep, gitleaks, wp plugin check.
- Releases built by CI from a tagged commit, checksummed, with an SBOM and a SECURITY.md. A
  compromised self-update server is RCE on every client site at once.

## Never emit
$wpdb->query("... $var ...") · $wpdb->prepare($built_query) · echo $var · unserialize($input) ·
eval / extract / create_function / dynamic callables · exec / shell_exec / system / passthru /
backticks · wp_remote_get($user_url) · 'sslverify' => false · wp_redirect($_GET[...]) · is_admin()
as authorization · current_user_can('administrator') · 'permission_callback' => '__return_true' on
writes · $_POST['x'] used directly · md5/sha1 for tokens · @ suppression · secrets in options, JS or
git · trusting $_FILES['type'] · move_uploaded_file() · posts_per_page => -1 · queries inside loops

## Done means
Object-scoped capability before nonce · no GET mutations · isset → wp_unslash → sanitize everywhere ·
allow-lists on finite domains · escaped at every sink · every SQL value bound and every identifier
allow-listed · real permission_callback with typed args · wp_safe_remote_* with timeout and validated
response · multi-statement writes in a transaction · no secret in options, JS, logs, git or HTML ·
negative tests for unauthenticated, wrong-capability and malformed input · phpcs, phpstan and psalm
taint clean. State which of these you verified and which you did not.
Paste the text into .cursorrules in the project root, or into .cursor/rules/wordpress.mdc. The rules don't replace code review: they are the starting point.
How to use them

Build a WordPress plugin with Cursor, step by step.

Cursor reads your project rules before every request, so the model starts from your constraints instead of its own habits. Save the file once and every prompt in the project inherits it: coding standards, security checks and the structure the plugin must follow.

Start by describing the plugin's goal in one sentence and let Cursor scaffold the main file, the settings page and the hooks. Then ask for one feature at a time: small steps keep every change readable and make it obvious when the model drifts from the rules.

Review every change before you accept it. The rules push the model toward sanitised input, escaped output, nonces and capability checks, but a person still has to confirm them, especially on anything that writes to the database or calls an external API.

Test on a staging site with WP_DEBUG enabled and read the logs before the plugin goes anywhere near a live site. When something breaks, add a rule that prevents it next time: the file gets better with every project.

What the rules cover

Core principles and plugin architecture, modern PHP practices, data and persistence, performance and caching, background work, assets and translations, testing, security, AI features, dependencies and a clear definition of when a feature is done.

Looking for the prompts that go with these rules? The free content library has a step-by-step guide with a starter prompt and the security checks to do by hand.