WordPress stores user roles and capabilities in the wp_usermeta table under the key wp_capabilities. Misunderstanding how caps are checked lets attackers escalate privileges — so knowing the exact API is essential for any plugin that grants or checks permissions.
Problem: WordPress ships with a fixed set of roles, and permission checks scattered through plugins often check role names ($user->roles[0] === 'editor') rather than specific capabilities — making it fragile when roles are customised.
Solution: Use add_role() to create custom roles with a precise capability set, get_role()->add_cap() to extend existing roles, and always gate features with current_user_can('specific_cap') — never by role name. Register custom capabilities in a plugin activation hook so they are set once and persist.
The examples below create a custom role with a safe capability set, add individual caps to existing users, and show the correct way to check capabilities in REST API endpoints.
// Register a custom role on plugin activation (run once via register_activation_hook)
function myplugin_create_roles() {
add_role(
'content_manager',
__( 'Content Manager', 'myplugin' ),
[
'read' => true,
'edit_posts' => true,
'edit_others_posts' => true,
'edit_published_posts' => true,
'publish_posts' => true,
'delete_posts' => false, // explicitly deny
'upload_files' => true,
'manage_categories' => true,
]
);
}
register_activation_hook( __FILE__, 'myplugin_create_roles' );
// Remove the role on deactivation
register_deactivation_hook( __FILE__, function() {
remove_role( 'content_manager' );
} );
Grant or revoke individual capabilities on a per-user basis without changing their role:
// Add a specific cap to a user
$user = get_user_by( 'login', 'jane' );
$user->add_cap( 'manage_woocommerce' ); // stored in wp_usermeta
// Remove a cap
$user->remove_cap( 'manage_woocommerce' );
// Check capability — ALWAYS use current_user_can(), never inspect the meta directly
if ( current_user_can( 'manage_woocommerce' ) ) {
// safe to proceed
}
// Protect a REST endpoint
add_action( 'rest_api_init', function() {
register_rest_route( 'myplugin/v1', '/report', [
'methods' => 'GET',
'callback' => 'myplugin_report_handler',
'permission_callback' => function() {
// Never use __return_true() in production
return current_user_can( 'manage_options' );
},
] );
} );
// DANGER: Never store raw capability names from user input
// BAD: $user->add_cap( sanitize_text_field( $_POST['cap'] ) );
// GOOD: Use an allowlist
$allowed_caps = [ 'edit_posts', 'upload_files' ];
$requested = sanitize_text_field( $_POST['cap'] ?? '' );
if ( in_array( $requested, $allowed_caps, true ) ) {
$user->add_cap( $requested );
}
NOTE: Never rely on role names for permission checks — always check the specific capability. Roles can be modified by other plugins; current_user_can( 'edit_posts' ) is always safer than $user->roles[0] === 'editor'.