Generated banner for Wp Abilities Verify
Generated logo for Wp Abilities Verify

Wp Abilities Verify

WordPressOfficialPublished by WordPress

  • Skill
  • Review and audit
  • Software development
  • GPL-2.0-or-later
  • No issues found

From the author

Verify a WordPress plugin's Abilities API registrations: enumerate abilities, check that callback behavior matches each annotation's claim (the adversarial readonly-but-writes detection), validate permissions and schemas, and validate audit documents produced by wp-abilities-audit.
— the description by WordPress

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

wp-abilities-verify is a tool for auditing WordPress plugin Abilities API registrations. It performs static analysis and optional runtime verification to ensure that the code's behavior matches its declared annotations. The skill identifies discrepancies where a readonly-annotated ability performs write operations, validates input schemas, checks permission gates, and verifies audit documentation produced by related tools.

What it does

  • Performs adversarial annotation correctness checks to detect hidden write operations in readonly abilities.
  • Validates plugin audit documents against canonical schemas.
  • Enforces schema hygiene and permission gate consistency for registered abilities.
  • Executes abilities in a runtime environment to confirm permission roundtrips and idempotency.
  • Generates a structured markdown report detailing status, inventory, and identified issues.
  • Supports suppression of false positives via inline code comments.

When to use it

  • Before merging pull requests for plugins using the Abilities API.
  • To perform health checks on existing plugins to catch regressions.
  • To validate audit documentation before sharing it with developers.
  • When you need to confirm that ability annotations accurately reflect the underlying code behavior.

Advantages

  • Prevents security and UX issues caused by misleading ability annotations.
  • Provides both static and runtime verification modes for flexible analysis.
  • Ensures consistency between plugin source code and audit documentation.
  • Automates the detection of common write patterns that violate readonly claims.

Usage examples

“Verify the abilities in the plugin located at /var/www/html/wp-content/plugins/my-plugin using static mode.”
The agent inspects the plugin source code, checks annotation correctness, validates schemas, and outputs a markdown report to the specified path.
“Run a runtime verification for the plugin in /app/plugins/example-plugin using the environment command npm run wp-env start.”
The agent brings up the environment, performs runtime enumeration, executes abilities to check permission roundtrips, and generates a comprehensive verification report.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/wordpress/wp-abilities-verify/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip wp-abilities-verify.zip -d .agents/skills

Documentation by WordPress

WP Abilities Verify

Verify a WordPress plugin's Abilities API registrations. The centerpiece is the adversarial annotation correctness check: a readonly: true ability that actually writes (via $wpdb->update, update_option, a non-GET delegate, etc.) is a security and UX disaster because agents plan actions on the basis of the annotations they introspect. This skill catches those lies by reading the callback body and comparing what it does against what the annotation claims.

The skill also validates audit docs produced by wp-abilities-audit, checks permission gates and schema hygiene, and optionally executes each ability against a live environment.

When to use

  • After abilities have been registered in a plugin but before a PR lands.

  • As a health-check on an already-shipped plugin (catch regressions where a refactor turned a readonly ability into a writing one).

  • To validate an audit document before handing it to an implementer.

Two modes

  • Static mode — runs from the plugin checkout. No env. Enumerates via source inspection, runs the adversarial correctness check, runs schema and permission lints, and validates audit docs.

  • Runtime mode — requires a running env. Does everything static does PLUS: wp_get_abilities() for authoritative enumeration, executes each ability with curated inputs, confirms permission roundtrip against real users, and runs a twin-invocation heuristic on idempotent: true abilities to flag candidates for review (return-value equality is a signal, not a verdict — core defines idempotent as "no additional effect on the environment").

Both modes produce the same structured report format.

A static-mode PASS means "no obvious-shape violations," not "verified write-free." For high-stakes plugins, run runtime mode before landing — it catches bootstrap-order, permission-roundtrip, and idempotency issues that static can't. See references/annotation-correctness.md for the static blind spots.

Inputs required

  1. Plugin checkout path — working tree to verify.

  2. Mode — static or runtime. Default to static if unspecified.

  3. (Runtime only) Env-up command — read the plugin's AGENTS.md. Common patterns: npm run wp-env start, npx wp-env start, or a composer-based bring-up. Plugin families with their own dev tooling will document their own command. Do NOT assume npm run wp-env works.

  4. (Optional) Audit doc path — enables cross-checks between the audit and the registered abilities, and validates the audit itself.

  5. Report output path — explicit path, typically the user's vault.

Prerequisites

  • wp-project-triage has been run on the plugin.

  • The plugin has at least one registered ability in source. Zero hits on wp_register_ability( → return a clear "no abilities registered" report, not an empty PASS.

Procedure

1. (If audit provided) Validate the audit doc

Read references/audit-schema-validation.md. Validate the audit against the canonical schema owned by wp-abilities-audit. Surface missing required fields, multiple reference_ability: true, and backing: null entries that aren't paired with a surfaced_gaps entry. backing: null alone is WARN (intentional gap output), not FAIL.

2. Enumerate abilities statically

Read references/static-enumeration.md. Find each wp_register_ability( call, extract the name, the annotation block, and the execute-callback location. Use a multi-line tool (rg --multiline --pcre2) — the canonical formatting splits the call across lines. Record each ability's source-file + line + annotations + callback byte range.

3. (Runtime only) Enumerate via REST + wp-cli

Read references/runtime-harness.md. Bring the env up using the command from AGENTS.md, then enumerate via wp_get_abilities() over wp-cli and cross-check against the static inventory. Source-only → FAIL (registration not firing). Runtime-only → WARN (dynamic registration path).

4. Annotation correctness (the adversarial core)

Read references/annotation-correctness.md. Read each callback body and verify it matches the annotation claim:

  • readonly: true → callback must not write to the database, the options table, post / user / term / comment data, the filesystem, cron, or via non-GET HTTP / REST delegates.

  • destructive: false → callback must not delete, refund, void, cancel, or trash.

  • idempotent: true → repeated calls with the same input have no additional effect on the environment (per the idempotent annotation's docblock in class-wp-ability.php). Static catches counter writes and per-call cron schedules; runtime adds a twin-invocation heuristic for visible state changes.

The reference lists common write patterns as a starting set, not a checklist — plugin vocabularies vary, and the agent extends with verbs specific to the plugin under verification.

False positives get suppressed via an inline // verify-ignore: <annotation> -- <reason> comment.

5. Permission roundtrip

Read references/permission-roundtrip.md. Static: classify each permission_callback against the six shapes (preferred Shape A current_user_can(...); FAIL on Shape B-bad WP_REST_Request patterns or Shape E literal true). Runtime: anon and subscriber denied; admin allowed (unless deliberately public). When an audit was provided, cross-check the registered cap against the audit's declared gate.

6. Schema lints

Read references/schema-lints.md. Six small principles applied to each ability's input_schema: object schemas declare additionalProperties; required fields have descriptions; enums non-empty; no $ref; defaults are statically constant (including (object) array()); reference abilities have no required inputs.

Cross-reference ../wp-abilities-api/references/input-schema-gotchas.md for the four runtime gotchas (defaults not injected on the property-level path, pagination key drift, empty() on string IDs, direct vs indirect invocation strictness).

7. Error-code vocabulary

Cross-reference ../wp-abilities-api/references/error-code-vocabulary.md. Inspect each callback's WP_Error returns; non-vocabulary codes → WARN.

Verification

The run produces a structured markdown report at the user-specified path:

---
Last updated: <YYYY-MM-DD HH:MM>
---

# <Plugin> Abilities Verification — <Static|Runtime> Mode

## Status: <PASS|WARN|FAIL>

## Audit doc validation (if provided)

## Static inventory

## Annotation correctness
| Ability | Claim | Result | Evidence |
|---|---|---|---|

## Permission gates

## Schema lints

## Error-code vocabulary

Every ability is OK, WARN, or FAIL. A single FAIL → top-line FAIL; WARNs without FAILs → WARN; otherwise PASS.

Failure modes / debugging

  • Env not reachable (runtime) — env-up failed or Docker isn't running. Re-run wp-project-triage, then fix the env. Don't fall back silently to static without noting it in the report.

  • No abilities in source — return a clear "nothing to verify" report.

  • Audit schema mismatch — point at references/audit-schema-validation.md; don't auto-fix the audit.

  • False positive on readonly-writes — see the // verify-ignore mechanism in references/annotation-correctness.md. Document why each suppression is legitimate.

  • Runtime enumeration smaller than static — registration hook isn't firing. Check init hook timing, activation state, autoloader order.

Escalation

  • Recurring legitimate pattern that trips the adversarial check across multiple plugins → propose adding it to the suppression guidance in annotation-correctness.md. Don't broaden the candidate-pattern list speculatively.

  • Audit-schema validator rejects a legitimate audit → the canonical schema in ../wp-abilities-audit/references/audit-schema.md has evolved. Update references/audit-schema-validation.md to match.

Out of scope

Token-budget measurement is a separate verification axis — an annotation-clean, schema-clean, runtime-passing ability set can still be unshippable if its tools/list form burns through an agent's context budget. That axis is tracked separately. Do not aggregate manual or external measurement into this skill's PASS / FAIL verdict.

Files

references/annotation-correctness.md

Annotation Correctness

The adversarial core of this skill: verify what the annotation claims by reading the callback. A readonly: true ability that actually writes is a security and UX disaster, and unit tests don't catch it because the mock looks just like the real writer.

Why this matters

Agents plan actions on the basis of the annotations they introspect. If an ability is annotated readonly: true, an orchestrator will confidently invoke it in a dry-run, speculative exploration, or multi-agent fan-out without thinking twice — because readonly means "can't break anything".

A readonly: true ability that actually writes is therefore:

  1. A security hazard — agents will invoke it in contexts where side effects are forbidden.

  2. A UX disaster — the agent's mental model of what happened diverges silently from reality.

  3. Undetectable at the annotation layer — the annotation says readonly: true; nothing in the registration forces it to be true.

Unit tests won't catch this class of bug because the mock the test constructs looks just like the real writer. What catches it is reading the execute callback body and comparing what it does against what the annotation says it does.

What each annotation promises

AnnotationWhat it promises (from core)
readonly: trueNo durable writes to user / business state. GET-style side-effect-free.
destructive: falseWon't irreversibly destroy data or forfeit money.
idempotent: trueRepeated calls with the same arguments produce no additional effect on the environment (per the idempotent annotation's docblock in class-wp-ability.php).

readonly: true prohibits durable writes to user or business state. Read-through cache writes (e.g. set_transient) and observability timestamps (e.g. last_read_at) are acceptable when explicitly annotated with verify-ignore — see the "Suppressing legitimate exceptions" section below. The static check treats unannotated writes as FAILs; annotated ones pass with the reason recorded as evidence.

These overlap but are not redundant: readonly is the strictest; destructive: false is weaker (updates that don't destroy are OK); idempotent is orthogonal (a POST that writes the same row twice is both "writes" and "idempotent").

The Abilities REST run controller operationalizes annotations into HTTP method routing (readonly: true → GET, destructive && idempotent → DELETE, otherwise POST — see WP_REST_Abilities_V1_Run_Controller::validate_request_method()). That mapping is the load-bearing semantic; verify checks that each callback's behavior is consistent with how the routing will treat it.

How to verify

For each ability, locate the execute_callback body (see static-enumeration.md step 4), then:

  1. Read the callback end-to-end. Form a model of what it actually does. Don't rely on pattern-matching alone.

  2. Compare to the claim. A readonly: true callback that writes anywhere — the database via $wpdb, options / post / user / term / comment writes, filesystem, cron schedules, or non-GET HTTP/REST delegates — FAILs readonly. A destructive: false callback that deletes, refunds, voids, cancels, or trashes FAILs destructive. An idempotent: true callback whose environmental effect accumulates per call (counters, append-only logs, per-call cron schedules) FAILs idempotent.

  3. Record evidence. Cite file + line of the offending pattern so a reviewer can jump straight to it.

Use grep or ripgrep to surface candidates. Common writes worth looking for:

$wpdb->update / insert / delete / replace
update_option / add_option / delete_option
wp_insert_post / wp_update_post / wp_delete_post
update_post_meta / update_user_meta / update_term_meta
->save / ->delete / ->set_status / ->add_*
wp_remote_post / wp_remote_delete
file_put_contents / wp_upload_bits / unlink / rename
wp_schedule_event / wp_schedule_single_event

Treat the list as a starting set, not a checklist. Plugin vocabularies vary — domain-specific verbs (->markAsPaid, ->commit, ->refund) and framework patterns (Doctrine ->persist, queue ->dispatch) won't appear above. Once you've grepped for candidates, read the callback to confirm whether each hit is actually a write and whether it contradicts the annotation in context.

Known blind spots

Static reading + grep can't reach every write. A static-mode PASS means "no obvious-shape violations," not "verified write-free."

Blind spotWhy static misses itMitigation
Indirected service writes — $repo->persist(), $service->commit(), custom verbs.Any finite verb list drifts; domain vocabulary varies.Inspect callbacks that touch custom services or repositories.
do_action() whose listeners write.Provenance ambiguity: ability looks clean; system mutates state in a listener.Audit listeners on the action. If any writes, downgrade or split.
Implicit core hooks fired by WP API calls — wp_insert_post() fires save_post; update_option() fires updated_option; wp_create_user() fires user_register; etc.The WP API call IS the write; the hooks fire automatically as a side effect. Agents looking for do_action() won't see this.Treat any WP write-API call as a write regardless of whether the callback also calls do_action().
Action Scheduler / deferred writes — as_schedule_single_action(), WC()->queue()->schedule_single(), custom job dispatchers.The callback returns cleanly with no immediately visible DB mutation; the durable write lands later in the AS tables. A static grep for $wpdb->insert won't catch it.Treat scheduler dispatches as writes. The "no additional effect on the environment" promise of idempotent: true is violated by accumulating queued jobs even if the immediate return value is constant.
Variable-built HTTP methods on delegate helpers.Static can't follow runtime values.Treat callers of helpers whose default method isn't GET as suspect.
Tautological capability gates — current_user_can('read') on a "private" ability.The cap looks valid; subscribers happen to hold it.Cross-reference the permission roundtrip — subscribers should be denied.

For high-stakes plugins, run runtime mode (see runtime-harness.md) before landing — it catches some blind spots via twin-invocation diff and live state inspection.

Suppressing legitimate exceptions

When a pattern that looks like a write is semantically a read (e.g. populating a read-through cache via set_transient, updating a last_read_at timestamp for tracking, diagnostic logging), suppress with an inline comment on the offending line:

// verify-ignore: readonly -- writes to read-through cache; semantically a read.
set_transient( $cache_key, $data, HOUR_IN_SECONDS );

Format: // verify-ignore: <annotation> -- <reason>. Legal annotation names: readonly, destructive, idempotent, all. Narrower is better than all.

Runtime check complement

For idempotent: true abilities, runtime mode adds a heuristic: invoke twice with the same input and compare. See runtime-harness.md Check 6. Differing returns are a signal to inspect, not a verdict — under core's definition, the question is whether the environment changed, not whether the return value matches. A response that embeds a per-call timestamp / nonce / random ID is fine; a response that reflects a counter that grew between calls is not.

Report format

Each finding gets one row in the run's "Annotation correctness" table:

| Ability | Claim | Result | Evidence |
|---|---|---|---|
| myplugin/get-things | readonly=true | OK | callback reads only |
| myplugin/get-things-with-counts | readonly=true | FAIL | `src/Abilities/Things.php:142`: `$wpdb->update( $table, ... )` |
| myplugin/submit-thing | destructive=false | OK | no destructive patterns |
| myplugin/submit-thing | idempotent=false | OK | check only applies when idempotent=true; false annotation acknowledged |

The evidence column MUST cite file + line so a reviewer can jump straight to the issue.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Node.js

The author says: Targets WordPress 7.0+ plugins (PHP 7.4.0+). Requires a runnable environment (wp-env, docker-based dev stack, or equivalent) for runtime mode; static mode runs entirely from the plugin checkout with no env. Filesystem-based agent with bash + node.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
WordPress
Publisher
WordPress (Official)
Source
WordPress/agent-skills · skills/wp-abilities-verify · commit 7374e6f
Licence
GNU General Public License v2.0 or later (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by WordPress or WordPress.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download wp-abilities-verify.zip