All WordPress HTML Templates Forms & Webhooks AI & Tools
WordPress

Building a Client-Proof WordPress Admin: Roles, Guardrails and Handover

A practical WordPress client handover guide: custom roles in code, capability guardrails, block locking with templateLock, and the docs that stop 2am calls.

A real working moment illustrating the theme of an article about wordpress client handover. Wide 16:9 banner, one strong focal point, magazine editorial quality, authentic and unstaged.

Three weeks after a handover, a client’s new marketing hire logged in as Administrator, installed a page builder to “fix a spacing issue”, and deactivated the caching plugin because a tutorial told them to. The site didn’t break loudly. It just got slower, and the homepage hero lost its container width, and nobody noticed until a sales call went badly.

That’s the normal failure mode. Not malice, not incompetence: an admin account with 60-odd capabilities handed to someone who needed maybe eight of them. A good WordPress client handover is mostly the work of deciding, in advance, what the client should be physically unable to do.

This is the version we run on client sites now, after enough post-launch archaeology to know which guardrails hold and which ones just annoy people into asking for admin access anyway.

Key Takeaways

  • Roles are just stored bags of capabilities in the wp_options table. Create a custom role once in a must-use plugin, version it, and never edit roles through a UI plugin you might later deactivate.
  • editthemeoptions is the most dangerous capability you can grant casually: in a block theme it opens the entire Site Editor, including templates and global styles.
  • Hiding an admin menu with removemenupage() is cosmetic. The URL still works. Capabilities are the lock, menus are the signage, and you need both.
  • templateLock: "contentOnly" plus locked patterns gives editors text and image fields inside a layout they can’t dismantle, which removes about 80 percent of “the page looks weird now” tickets.
  • Handover documentation should be task-shaped (“how to add a case study”) and under four minutes per video. Nobody reads a 30 page PDF, and nobody watches a 40 minute screen recording twice.

What actually breaks after handover

Go and look at the last five sites you shipped. The damage is almost always one of five things: a plugin installed on a whim, a theme or plugin update applied without a backup, global styles changed in the Site Editor, a page layout torn apart in the block editor, or a menu item deleted and rebuilt badly.

Notice what’s not on that list. Nobody is writing bad PHP into the file editor, because most of them wouldn’t know how. The risk isn’t code, it’s configuration surface. Every screen you leave visible is a screen someone will eventually poke, usually on a Friday afternoon, usually while you’re offline.

The design goal isn’t “lock them out”. It’s to make the twelve things the client legitimately does per month obvious and safe, and make everything else invisible.

Roles are bags of capabilities, and they live in the database

WordPress stores roles as a serialised array in the wpuserroles option. When you call add_role(), you write to the database once. Calling it on every page load does nothing after the first time, which is why people get confused when they change the capability list and see no effect.

The fix is to version your role definitions and rewrite them only when the version bumps. Put this in wp-content/mu-plugins/client-guardrails.php so it can’t be deactivated from the admin.

<?php
/**
  • Plugin Name: Client Guardrails
  • Description: Roles, capabilities and admin cleanup. Site specific, do not reuse blindly.
*/ add_action( 'init', function () { // Roles persist in the DB, so only rewrite them when this version changes. if ( getoption( 'acmeroles_version' ) === '4' ) { return; } removerole( 'sitemanager' ); $editor = get_role( 'editor' ); addrole( 'sitemanager', 'Site Manager', array_merge( $editor->capabilities, array( 'list_users' => true, 'edit_users' => false, 'gformfullaccess' => true, // Gravity Forms entries and form editing 'wpseomanageoptions' => false, // Yoast stays with us ) ) ); updateoption( 'acmeroles_version', '4' ); } );

Two things learned the hard way. First, don’t build roles with a UI plugin like Members or User Role Editor and leave it at that. Those plugins are excellent for inspecting capabilities, but if the plugin gets deactivated during a debugging session six months later, your role definitions are frozen in a state nobody can read. Define roles in code, use the plugin as a viewer.

Second, be paranoid about editthemeoptions. It sounds like “can edit menus”. In a block theme it also grants the Site Editor, which means templates, template parts and global styles. If your client needs menu access only, grant the capability and then block the Site Editor explicitly:

addaction( 'admininit', function () {
	if ( currentusercan( 'sitemanager' ) && ! currentusercan( 'manageoptions' ) ) {
		removemenupage( 'themes.php' );
	}
} );

// Menu removal is cosmetic. This is the actual lock on the Site Editor screen.
add_action( 'load-site-editor.php', function () {
	if ( ! currentusercan( 'manage_options' ) ) {
		wp_die( 'The site design is managed by your development team. Email [email protected].', 403 );
	}
} );

That wp_die() message matters more than it looks. A blank permissions error generates a support email that says “the site is broken”. A message with a contact address generates one that says what they wanted to do.

Building the guardrails before the WordPress client handover

Do this work during the build, not in the last week. Retrofitting restrictions onto a site the client has already explored feels like a demotion to them, and you’ll spend the call defending it.

wp-config constants worth setting

// Kills the theme and plugin file editors. No reason to ever leave these on.
define( 'DISALLOWFILEEDIT', true );

// Blocks ALL plugin/theme installs and updates, including automatic security updates.
// Only use this if you own the update process. Otherwise it is actively harmful.
define( 'DISALLOWFILEMODS', false );

define( 'WPAUTOUPDATE_CORE', 'minor' );

DISALLOWFILEMODS is the one people copy from a listicle and regret. It stops the client installing junk, yes. It also stops WordPress applying its own security patches to plugins. If you’re not running a maintenance retainer with a monthly update window, leave it false and control installs through capabilities instead by removing installplugins and activateplugins from the client role.

Cleaning the dashboard

Every plugin that adds a promotional notice to admin_notices is training your client to ignore notices, including the real ones. Suppress them for non-admins.

addaction( 'adminhead', function () {
	if ( ! currentusercan( 'manage_options' ) ) {
		removeallactions( 'admin_notices' );
		removeallactions( 'alladminnotices' );
	}
}, 1 );

addaction( 'wpdashboard_setup', function () {
	removemetabox( 'dashboard_primary', 'dashboard', 'side' );   // WordPress news
	removemetabox( 'dashboardquickpress', 'dashboard', 'side' );
	removemetabox( 'dashboard_activity', 'dashboard', 'normal' );
} );

Then add one dashboard widget of your own: site contact, hosting provider, where backups go, what the retainer covers. It’s the first thing they see and it deflects a surprising number of emails.

Client friendly WordPress is mostly block locking

The block editor gives editors a nested tree of blocks and a delete key. That combination is why homepage heroes lose their layout. The answer isn’t training, it’s locking.

Three mechanisms, in increasing severity:

  • Per-block lock attributes. Add "lock":{"move":true,"remove":true} to a block’s JSON. The block can still be edited, just not dragged out or deleted.
  • Container templateLock. "templateLock":"contentOnly" on a Group means children can’t be added, removed or reordered, and the block toolbar for styling disappears entirely. Editors see text fields and image replacements. That’s it.
  • Post type templates. Register a fixed block template on a custom post type so every new entry starts in the correct shape.
<!-- wp:group {"templateLock":"contentOnly","className":"hero","layout":{"type":"constrained"}} -->
<div class="wp-block-group hero">
  <!-- wp:heading {"level":1,"metadata":{"name":"Hero heading"}} -->
  <h1 class="wp-block-heading">Replace this headline</h1>
  <!-- /wp:heading -->
  <!-- wp:paragraph {"metadata":{"name":"Hero intro"}} -->
  <p>One or two sentences, no more.</p>
  <!-- /wp:paragraph -->
</div>
<!-- /wp:group -->

That metadata.name field is underused. In contentOnly mode the list view shows those names, so your client sees “Hero heading” instead of “Heading”. Small change, noticeably fewer confused messages.

Ship these as registered patterns rather than asking anyone to paste HTML. We covered the full approach in building a reusable block pattern library for clients. The short version: a pattern library plus contentOnly locking replaces most of what people used to buy a page builder for. If the content model is genuinely structured (team members, case studies, properties), skip blocks and use fields instead. Our notes on ACF and native block bindings cover when that’s the better call.

Also lock the Media Library

Set show_ui restrictions or at minimum limit non-admins to their own uploads. A shared library with 4,000 unsorted files is how clients end up re-uploading a 6 MB PNG that was already there at the right size. Pair that with server side compression so it matters less when they do.

The handover package itself

The technical lockdown is half the job. The other half is making sure that when you disappear, the site is still recoverable by someone else.

  1. Ownership before access. Domain registrar, DNS, hosting and licence keys should be in accounts the client owns, with you added as a collaborator. Not the other way round. An agency-owned domain is a hostage situation nobody planned, and it always surfaces at the worst time.
  2. Credentials in a vault, never in email. A shared 1Password or Bitwarden collection, handed over as a whole item. Include the licence keys for every commercial plugin and note the renewal date.
  3. Task-shaped videos. One recording per job the client actually does. “Add a blog post with a featured image.” “Update the opening hours.” “Export form entries.” Three to four minutes each, named clearly, stored somewhere permanent. Long walkthroughs get watched once and never again.
  4. A one page runbook. What’s hosted where, where backups go, how to restore, who to call and in what order. Our 3-2-1 backup guide is the model we use for the backup section, and the critical line is the one confirming a restore was actually tested, with the date.
  5. A written update policy. Who applies plugin updates, on what cadence, and whether there’s a staging site. If the answer is “nobody”, say so out loud so the client can decide to pay for it or accept the risk.

Add a scheduled email that fires monthly to both you and the client with core, plugin and PHP versions. It’s about twenty lines of code, and it converts silent decay into a visible number.

Where this advice doesn’t apply

Locking down hard is wrong for two kinds of client. The first is a client with a genuinely capable in-house team: a marketing department with a WordPress person will hit your guardrails daily and resent them. Give them a Site Manager role with more room and spend your effort on a staging environment instead.

The second is any project where you’re not the ongoing maintainer. If you’re handing the site to another agency in six weeks, an mu-plugin full of undocumented capability filters is a hostile act. Document every restriction in a README next to the code, including how to remove it.

Be honest about the limit of the whole approach: none of this stops a determined admin. Anyone with manage_options and FTP access can undo every guardrail here in ten minutes. Guardrails prevent accidents, not decisions. If the client insists on an Administrator account, give it to them, write down that you advised against it, and make sure the backups are real.

For projects where the client wants to control layout without you building a locking system from scratch, a theme designed around that constraint is a shortcut worth considering. CanvasWP ships with pre-built sections that behave predictably in the editor, which cuts the amount of custom locking code you need to write.

Frequently Asked Questions

Should the client ever get an Administrator account?

Usually not, but sometimes it’s unavoidable. If the client is the business owner and there is no maintenance retainer, they need a way in when you’re unreachable, so create a genuine admin account with a strong password stored in their vault and tell them it’s for emergencies. Their day-to-day account should be the restricted role. Two accounts, two purposes.

What’s the actual difference between a role and a capability?

A capability is a single permission string like editposts or manageoptions. A role is a named collection of those capabilities stored in the database. You can also grant capabilities directly to an individual user with $user->add_cap(), which is useful for one-off exceptions without inventing a whole new role.

Do I need a plugin like Members or Adminimize?

For inspecting and debugging capabilities, a role plugin earns its place. For defining them, code in an mu-plugin is more reliable because it survives plugin deactivation and lives in version control. Adminimize-style plugins that hide fields with CSS are worth avoiding entirely: hiding a field visually doesn’t stop the data being saved.

How do I stop clients breaking page layouts in the block editor?

Wrap sections in a Group with templateLock: "contentOnly" and ship them as registered patterns. Editors can change text and swap images but cannot delete, reorder or restyle the blocks inside. Name each editable block with the metadata.name attribute so the list view reads in plain language.

Who should own hosting and plugin licences after handover?

The client, with you invited as a collaborator or agency user. Agency-owned hosting accounts and licences create a dependency that becomes a dispute the moment the relationship ends. If you resell hosting as part of a retainer, put the terms in writing including what happens to the site if the retainer stops.

Where to start

Pick your most recently launched site. Log in as the client’s account, not yours, and try to break something: install a plugin, open the Site Editor, delete a block from the homepage hero. Whatever you succeed at is your first guardrail. Write it as an mu-plugin, commit it, and make that file the starting template for every project after this one.

Do that three times and you’ll have a lockdown kit that takes twenty minutes to configure per site. The support inbox gets quieter within a month.

client friendly wordpress admin setup custom wordpress user roles in code disable site editor for non admins mu-plugin client guardrails wordpress restrict wordpress admin for clients templateLock contentOnly block editor wordpress client handover wordpress client handover checklist