Applies to admin.client-mode, admin.capability-boundaries.

Covers admin.client-mode, admin.capability-boundaries. Related: security.submission-store-private, security.rest-route-authorisation.

A tidy admin and a locked admin look identical from the sidebar, and only one of them is a boundary. Both rules here are house rules: they came out of auditing two WordPress themes we had already delivered, no client asked for them, and no published specification requires them.

Hiding is not a boundary

remove_menu_page() takes a row out of the sidebar and changes no permission, so the screen still answers at its own URL.

The measured split is what makes that concrete. Both delivered builds deny one thing by capability: creating posts in the inbox types, with create_posts => do_not_allow and map_meta_cap. Users, Plugins, Tools and Themes are menu-hidden in both and carry no capability change at all - a grep across either theme for add_role, add_cap, remove_cap or user_has_cap returns three hits and all three are the same map_meta_cap line. So the third row of the table below is failing in delivered work, and it looks exactly like the second row from the sidebar.

One of the two builds says so in its own theme docs, in its words: "Hiding is UI-only, not a hard capability block - the pages are still reachable by direct URL ... not a security boundary". The other ships the identical mechanism and says nothing; a grep of its PHP, docs and README for cosmetic, security boundary and UI-only returns zero matches. That split is why admin.capability-boundaries carries a second sentence in its accept, and why it scores contested against our own work. The mechanism was never the disagreement. Writing it down was.

Clutter or boundary

What it is Which What fixes it
A plugin's upsell menu, a dashboard widget, an unused post type Clutter Hide it, and record in the repo that hiding is cosmetic
Creating a record in a log the site writes for itself Boundary Deny the capability where the type is registered
Users, Plugins, Themes, Tools Boundary, if you mean it A role with the capability removed. Hidden, these read most like a lock

The boundary both builds drew one edge of

Both deny exactly one thing, and they deny it properly:

register_post_type( 'message', [
	'map_meta_cap' => true,                                // without this the next line is inert
	'capabilities' => [ 'create_posts' => 'do_not_allow' ],
] );

map_meta_cap => true is the load-bearing half. Without it WordPress never maps the meta capability, the denial is never consulted, and the screen you believed you closed opens.

What neither build sets is capability_type. It defaults to post, so every verb except create maps onto the ordinary post capabilities, and any role that can edit a post can list and read every stored message and consent record inside wp-admin. security.submission-store-private is the cleanest verdict in this dimension, verified live on both sites with the permalink and all three REST routes returning 404 anonymously, and none of it says anything about a logged-in editor. Denying create_posts stops the client fabricating a consent record, not an author reading the lot.

The gap costs more elsewhere. Both builds put the theme's settings screen, which holds an SMTP password and a reCAPTCHA secret, at edit_posts, so every editor can read both. practices.no-secrets misses it by design: it grades git history and the web root, not wp-admin.

Why the trim has to be reversible

Both builds keep the hide list as a PHP array in inc/admin-cleanup.php, and each part of it earns its place.

The trim that applied to nobody

admin.client-mode originally asked for the site-wide toggle both builds ship, and a live project had removed that exact toggle the day before the rule was written. WPML will not let an editor open a translated post at all, so the agency's own people were given administrator accounts, and the toggle exempted administrators by design, which is right for exactly as long as the only administrators are developers. The moment the client's own people held the role it applied to nobody it existed to help, and nothing reported it. The accept names the account that settles it: one the client actually uses. Not yours.

Checking it

# Ask WordPress rather than the sidebar, as the client's own account.
wp eval --user=editor@example.com '$t = get_post_type_object( "message" );
  var_dump( current_user_can( $t->cap->create_posts ) );'   # bool(false)

# Then open the screen as that account. A hidden menu still returns 200.
curl -sI -b client-cookies.txt 'https://example.com/wp-admin/post-new.php?post_type=message' | head -1

The second has to refuse rather than render the editor, and that same account has to reach the control that undoes the trim. The audit behind these rules never ran the refusal: it read the registration and inferred it. Inferring it is how a hide ships as a boundary, so run it.