agentsclimarketplace

Wp metadata api

Skill Lonsdale201/wp-agent-skills/wordpress/wp-metadata-api

Implements and audits WordPress post, user, term, comment, and generic metadata code, including slashing contracts, revision redirection, multi-row keys, exact meta_id operations, return-value ambiguity, cache and hook behavior, registration/auth schemas, scalar typing, and safe handling of serialized or double-serialized values. Use when code calls get_*_meta, add_*_meta, update_*_meta, delete_*_meta, register_meta, update_metadata_by_mid, get_metadata_by_mid, maybe_serialize, maybe_unserialize, or queries a *_meta table directly.From its SKILL.md

Install
npx -y skills add Lonsdale201/wp-agent-skills --skill wp-metadata-api

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 21 stars21 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

9.5 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it

WordPress Metadata API

Choose the correct metadata surface and preserve its historical contracts. This skill covers correctness and data integrity, not whether a meta table is the right storage primitive for a new feature.

Choose the operation by identity

IntentAPI
Read one key's first valueget_post_meta( $id, $key, true )
Read all rows for one keyget_post_meta( $id, $key, false )
Add another rowadd_post_meta() / add_metadata()
Update every row matching object + keyupdate_post_meta() / update_metadata() without $prev_value
Update only rows matching an old valuepass $prev_value
Update one physical rowupdate_metadata_by_mid()
Delete every row matching object + keydelete_post_meta() / delete_metadata() without value
Delete one physical rowdelete_metadata_by_mid()

Do not use a key-level update when preserving multiple distinct rows matters. Do not use $single = true when duplicate keys are meaningful.

Critical contracts

1. Slashing differs by API

add_metadata(), update_metadata(), delete_metadata() and the normal post/user/term/comment wrappers historically expect slashed key/value input and call wp_unslash() internally. If a value has already been unslashed and must retain literal backslashes, slash it for that boundary:

$value = wp_unslash( $_POST['json'] ?? '' );
// Validate the domain value here.
update_post_meta( $post_id, $key, wp_slash( $value ) );

Do not blindly double-slash a superglobal value that has not been normalized. Trace the value's state.

update_metadata_by_mid() does not call wp_unslash() on the new value or key. Pass the already-normalized domain value directly:

update_metadata_by_mid( 'post', $meta_id, $normalized_value );

Flag code that applies the wrapper contract to the by-mid API or vice versa.

2. Post wrappers redirect revisions

add_post_meta(), update_post_meta(), and delete_post_meta() call wp_is_post_revision() and operate on the parent post. If the intent is to change metadata physically stored on a revision, use the generic API with the exact revision ID:

update_metadata( 'post', $revision_id, wp_slash( $key ), wp_slash( $value ) );

Read paths do not perform the same parent redirect. Audit read/write symmetry.

3. Multi-row cardinality

Without $prev_value, update_metadata() updates all rows sharing object ID and key. delete_metadata() without a value deletes all of them. A delete-then-add sequence collapses multiple rows to one and is not atomic.

When transforming each row independently, select stable meta_id values and use by-mid operations. Detect key-renaming collisions before writing. PHP array keys such as "1" can coerce to integer 1 and collide even when source strings look different.

4. Return values are not a simple success boolean

update_metadata() can return a meta ID when it inserts, true when it updates, and false for failure or an unchanged single value. By-mid update returns false when the row is missing, blocked, unchanged at SQL level, or failed.

Do not increment processed counters unconditionally. On ambiguous false, inspect the exact function contract and verify the final value/row existence. For direct SQL also inspect $wpdb->last_error; never expose it to untrusted clients.

5. Scalar types and missing values

Normal meta reads return non-serialized scalars as strings: false becomes '', true becomes '1', and numbers become strings. Arrays and objects retain type. Use metadata_exists() when '' can mean either missing or an explicitly empty value.

get_post_meta( $id ) without a key returns the cache-shaped map and its stored values are not passed through the per-key maybe_unserialize() branch. Do not assume it has the same shape as repeated keyed reads.

6. Registration and authorization

Use register_post_meta() / register_meta() when exposing or validating a known schema. Verify type, single, default, sanitize_callback, auth_callback, object_subtype, and show_in_rest. Registration does not replace endpoint/object capability checks.

Do not sanitize an opaque migration value with sanitize_text_field() merely to satisfy a generic checklist. Preserve exact values when the feature requires it, but validate type, size, encoding, and allowed operation; use prepared SQL or the meta API; escape only at output.

Serialization safety

WordPress automatically serializes arrays/objects. Do not call serialize() or maybe_serialize() before a normal meta write unless preserving a deliberate legacy storage layer. A string that already looks serialized is double-serialized by maybe_serialize() for backward compatibility.

Audit transformations by layer

Never run a blind str_replace() over serialized text. Serialized strings contain byte lengths:

s:3:"foo";

Changing foo to a longer value without decode/re-encode corrupts the payload. This also occurs after one decode of double-serialized data, or inside a nested string that itself contains serialized data. Record the original layer count, decode only the intended trusted layers, transform the domain value, then re-encode the same storage contract.

Do not use is_serialized() as a full integrity check. It recognizes the serialized shape and can accept a string whose embedded byte lengths make an actual unserialize() fail. Validate by safely decoding the expected type in a bounded test path; distinguish a decode failure from the legitimate serialized boolean b:0;.

Before rewriting array keys, preflight transformed keys with type-aware collision detection. Do not partially save a row after a collision.

Treat object construction and recursion as a trust boundary

maybe_unserialize() eventually calls PHP unserialize() with classes allowed. __wakeup() / __unserialize() runs before later code can decide to skip the object. Rate severity by who can write the raw stored payload; a database read alone is not proof of an exploitable object-injection path.

For untrusted serialized input, prefer rejecting it or using JSON. If legacy data must be inspected, use allowed_classes => false, a supported max_depth, byte/node/depth limits, and cycle-aware traversal. allowed_classes => false blocks class instantiation but does not make arbitrary graphs safe to recurse or rewrite.

Cache, hooks, and direct SQL

Metadata APIs run sanitize filters, pre/post hooks, and clear the corresponding {$meta_type}_meta object cache. Direct SQL bypasses these contracts. Use direct SQL only when set-based performance is necessary and the product explicitly decides how to reproduce cache invalidation and hook semantics.

For read loops, prefer priming with update_meta_cache() or a query API that primes meta caches instead of calling keyed getters across unprimed objects.

False-positive guards

  • Do not flag every maybe_unserialize( get_option(...) ) as object injection; trace who can write the raw serialized payload and which gadget classes exist.
  • Do not demand text sanitization for an exact-preservation migration. Demand a defined type/size/encoding contract and safe sinks instead.
  • Do not flag generic metadata APIs merely because wrappers exist; exact revision or meta-row identity can require the generic/by-mid surface.
  • Do not interpret false as definite failure without checking unchanged-state semantics.

Report format

Report the API used, intended row identity/cardinality, slash state, stored and returned type, serialization layers, hooks/caches affected, observed failure, and the corrected contract. Mark speculative object-injection chains as conditional and state the required write primitive.

Cross-references

  • Use wp-batch-mutation-audit for multi-request or concurrent meta changes.
  • Use wp-security-deep for exploit-focused object-injection analysis.
  • Use wp-plugin-options-storage when deciding whether meta is the right storage primitive.

What this skill does NOT cover

  • General nonce, capability, REST, and SQL-injection review.
  • Custom-table schema design or query-plan optimization.
  • A universal parser for hostile PHP serialization.

References

What ships with it: 1 file

277 B alongside SKILL.md

agents/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.