Permissions

Permission Inheritance: How WordPress Roles Map to Agent Access

⏱ 11 min

Lesson 1 established that every MCP connection resolves to a real WordPress user. This lesson makes the practical consequence of that fact explicit: an AI agent’s access is never defined by “being an agent,” it’s defined entirely by whatever role and capabilities that underlying user account already has. There is no separate agent permission system to configure. You are configuring WordPress’s existing one.

What you'll learn in this lesson
Why there is no separate agent permission system
The account behind the connection is the entire permission boundary.
How roles and capabilities actually work in WordPress
A quick, precise refresher, since this course leans on it constantly from here.
What current_user_can() sees when called from an ability
It resolves against the exact user the Application Password belongs to.
Why the built-in roles are usually the wrong fit for an agent
Setting up the case for the next lesson's dedicated role.
Prerequisites

Lesson 1, and a basic familiarity with WordPress’s built-in roles (Administrator, Editor, Author, Contributor, Subscriber). No new tools needed yet.

Step 1: The connection is the user, permanently

When Claude, Cursor, or any other MCP client sends a request using an Application Password, WordPress resolves that request to the exact WP_User who generated the password, no more, no less. If that user is an Administrator, the agent has Administrator-level capabilities everywhere a permission_callback checks them. If that user is a Contributor, the agent is bounded by Contributor-level capabilities. This sounds obvious stated plainly, but it’s the single most common misconfiguration in real deployments: someone generates an Application Password from their own Administrator account “just to get it working,” and every AI client using that credential inherits full site control indefinitely.

Never issue an agent credential from an Administrator account

This is the most important sentence in this course. Every Application Password an AI client uses should belong to a dedicated, purpose-built account with the minimum capabilities that agent actually needs, never your own admin login. The next lesson builds exactly that account.

Step 2: A precise refresher on roles and capabilities

WordPress’s permission model has two layers, and both matter here:

Capabilities are the actual permission unit
Strings like edit_posts, publish_posts, delete_others_posts, manage_options. These are what code actually checks.
Roles are named bundles of capabilities
Administrator, Editor, Author, Contributor, Subscriber, each just a preset list of capabilities assigned to that role.
current_user_can() is the function that checks them
It looks at the current user's role(s) and any capabilities added individually, and returns true or false.

An ability’s permission_callback is almost always, directly or indirectly, a call to current_user_can():

// File: my-plugin.php
'permission_callback' => function () {
	return current_user_can( 'edit_posts' );
},

There is nothing MCP-specific about this check. It’s the same function, checking the same capability, that WordPress core itself uses to decide whether to show an “Edit” button in wp-admin. An AI agent invoking this ability is subject to exactly the same rule as a human clicking that button, because it’s authenticating as the same kind of account.

Step 3: Object-level checks resolve against the same user too

Some capability checks aren’t just role-based, they’re checked against a specific object, most commonly via WordPress’s meta-capability mapping:

current_user_can( 'edit_post', $post_id );

This resolves differently depending on who owns $post_id. A Contributor can edit their own draft but not someone else’s. An agent authenticated as a Contributor-role account inherits exactly that same restriction, it can act on posts that account owns, and gets correctly denied on posts it doesn’t, with zero extra code from you. This is a real, useful property: WordPress’s existing ownership rules apply to agent traffic automatically, as long as your permission_callback actually uses them rather than a blanket true.

A permission_callback that always returns true defeats all of this

It’s tempting, especially while debugging a new ability, to write 'permission_callback' => '__return_true' and move on. Ship that, and every one of WordPress’s built-in ownership and capability checks stops applying to that ability entirely, regardless of which account is connected. Always route through current_user_can(), even during development.

Step 4: Why the built-in roles are usually the wrong fit

The five default roles were designed for humans managing content through wp-admin, not for scoping a programmatic AI connection. Editor, for instance, bundles edit_others_posts, delete_posts, manage_categories, and more, likely far more than a single agent workflow (say, “summarize new comments”) actually needs. Reaching for the nearest built-in role as a shortcut tends to over-grant. The next lesson covers building a dedicated role or a small set of custom capabilities scoped to exactly what a given agent connection is meant to do.

Test it: confirm which capabilities a connected account actually has

Before trusting any existing connection, check exactly what its underlying user can do:

wp-env run cli wp eval '
$user = get_user_by( "login", "ai-agent-readonly" );
print_r( $user->allcaps );
'

Review that list against what the agent’s workflow actually requires. Anything present that the workflow doesn’t need is unused, unnecessary risk.

Recap

An MCP connection has no identity beyond the WordPress user it authenticated as. That user’s role and capabilities are the entire access boundary, checked through the same current_user_can() function WordPress core uses everywhere else, including object-level ownership checks. Never issue an agent credential from an Administrator account, and don’t assume a built-in role is well-scoped just because it’s convenient. The next lesson builds a dedicated, minimally-scoped role instead.

Resources & further reading

← Generating and Rotating Application Passwords for AI Clients Building Role-Based Access Control on Top of permission_callback →