If you ever asked an agent to fix an image’s alt text and got back a flat “permission denied,” the honest answer is that the error was lying to you. The refusal itself was real, the write really was blocked. The reason it gave was not. Your agent read that message as a privilege problem and stopped looking for another way in, when the actual answer was one call away the whole time. Version 1.7.0 fixes the message. Here is what was actually going on.
Alt text lives behind a wall WordPress itself built
Image alt text is not stored as ordinary post meta. WordPress keeps it under a key called _wp_attachment_image_alt, and any meta key starting with an underscore is protected meta in core WordPress, a rule is_protected_meta() enforces for every plugin, every theme, and every caller. _thumbnail_id, the key WordPress uses for a post’s featured image, is protected the same way.
That protection has nothing to do with roles or capabilities. An administrator asking to write _wp_attachment_image_alt through a generic post-meta tool gets refused exactly like a subscriber would.
It is not a permission gap this plugin left open, and it is not something 1.7.0 changes. The wall was always supposed to be there.
Why the refusal pointed at the wrong problem
Agent Abilities for MCP exposes three generic post-meta tools: aafm-get-post-meta, aafm-update-post-meta, and aafm-delete-post-meta. All three refuse a protected key like _wp_attachment_image_alt for every caller, which is correct. The part that was not correct sat one layer up, in how that refusal reached the agent.
Before 1.7.0, the MCP adapter this plugin runs on turns any non-true permission verdict into one literal string: “Permission denied.” That is fine when the cause really is a missing capability. It is misleading when the cause is a key nobody, at any privilege level, was ever going to be allowed to touch through that particular tool.
An agent asked to fix alt text would try the post-meta tool, hit that message, and conclude it lacked the rights to do the job. It had no way to know a different tool had the alt field the whole time.
What 1.7.0 changed, and what it did not
The fix is entirely in the wording. When one of the three post-meta tools is called with _wp_attachment_image_alt or _thumbnail_id, the refusal now names the tool that actually reaches that key, instead of stopping at a bare “permission denied”:
- Writing alt text now points to
aafm-update-media, which takes analtfield. - Reading alt text now points to
aafm-get-media-item, which returns it as a plain field. - Writing a featured image now points to
aafm-set-featured-image, or thefeatured_mediafield on a post or page write.
The three post-meta tools also name the alt-text alternative in their own input schema, on the meta_key parameter, so a client that reads its tools carefully can learn that route before it ever calls the wrong one. The featured-image route is in the refusal only.
Nothing about who can do what changed. No permission callback moved, no capability got looser or stricter, and the catalog is still the same 153 governed abilities.
The check that decides whether a key is protected runs exactly as it always did. What changed is that the sentence explaining a refusal now tells the truth about where to go, instead of just stating what did not work.
The same fix answered a second wrong “permission denied”
Alt text was not the only refusal wearing the wrong label in 1.7.0. A call that simply left out an argument its schema requires used to come back as “Permission denied” too, over MCP, for the same underlying reason: the adapter checks permission before it checks whether the call is even complete, and it flattens anything short of a strict “yes” into that one string.
The plugin declares 117 abilities that require at least one argument, and 52 of them resolve the object they are asked about before doing anything else. Ask one of those for a post you never named, and it looked up post ID 0, found nothing, and answered as though you were not allowed to see it. The refusal now names what is missing instead, so a refusal naming post_id as a missing required property tells an agent to fix the call and retry, where “permission denied” told it to stop and fetch a human.
This was only ever a problem on the MCP path. A direct call or a REST request already validates input before checking permissions, in that order, so those paths answered correctly the whole time; the fix changes only what an MCP caller sees.
Why this message is safe to give, and a capability refusal is not
The plugin can name the correct tool here but does not do the same for an ordinary permission failure, and it is worth explaining why: the two refusals are not the same kind of thing.
A protected-key refusal never depends on who is asking or how the site is set up. _wp_attachment_image_alt is out of reach for an administrator and a subscriber alike, on every WordPress install, so naming the tool that does reach it discloses nothing an agent could not already read in that tool’s own published description.
A capability refusal is the opposite. It depends on the caller’s role and on which abilities the operator switched on, and describing a workaround there would leak information about the site’s own configuration. So the plugin only ever names an alternative for the small, fixed set of keys WordPress itself protects, and every other refusal keeps its plain wording.
Try it yourself
If you want to see the corrected path, ask a connected agent something like:
Set the alt text on the last image I uploaded so it describes what's actuallyin the picture.With the Update media ability switched on, a well-behaved agent should call aafm-update-media directly and succeed. If it tries the generic post-meta tool first out of habit, the refusal it gets back now names aafm-update-media instead of leaving it stuck on a permission it was never going to get.
What this is really about
A refusal that names the wrong cause is a real defect in a plugin built around clear governance, not a minor wording issue. The point of an audited, capability-checked tool catalog is that a caller can trust what a “no” means, and a “no” that blames the wrong thing wastes the one chance an agent has to try the actual answer.
1.7.0 also carries a run of other correctness fixes, from WooCommerce refund counting to ACF writes that were destroying content while reporting failure. The full list is on the changelog. If you run the plugin, this update is worth taking, and the earlier 1.6.2 hardening release is worth reading alongside it for the same reason: a governance product only means what it says when it is willing to correct itself out loud.