Unbranded Handoff
White-label handoff mu-plugin for WordPress client sites: agency-branded login, quieter admin for clients
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/blademan/unbranded-handoff/archive/refs/heads/main.zipA small must-use plugin that makes a WordPress site feel like the agency's own product when it goes to a client.
v1.0.0. Tested locally on WordPress 7.1.2 (SQLite, PHP 8.5.11) with PHPCS clean. Run the checklist below on staging before using it on a production site.
What it does
For everyone (login screen):
- Agency logo, link and link text instead of the WordPress ones.
For client users (any logged-in user who is not agency staff):
- Removes risky capabilities: updates, plugin and theme install/edit/delete/activate, file editors.
- Hides configured menus (default: Tools).
- Removes the WordPress logo and update nodes from the admin bar.
- Replaces the admin footer text with "Site by Agency" and hides the WordPress version.
- Replaces the WordPress news dashboard widgets with a "Need help?" widget (name, support email, URL).
Agency staff see a normal WordPress admin.
Screenshots
Captured on a local test site with the sample config (agency "Example Studio").
Login screen (everyone)

Agency user (unchanged admin) and client user (restricted admin)
| Agency | Client |
|---|---|
![]() |
![]() |
Direct URL as a client (for example plugin-editor.php) is blocked, not just hidden:

Requirements
- WordPress 6.4+
- PHP 8.2+
Install
- Copy
unbranded-handoff.phpand theunbranded-handoff/folder intowp-content/mu-plugins/. - Copy
unbranded-handoff/config-sample.phptounbranded-handoff/config.php. - Edit
config.php: set your agency details and at least one ofagency_email_domainsoragency_user_ids.
Configuration
| Key | Type | Purpose |
|---|---|---|
agency_name |
string | Shown on the login screen, footer and dashboard widget |
agency_url |
URL | Login logo link, footer link, dashboard widget |
support_email |
Dashboard widget | |
logo_url |
URL | Login logo. Empty keeps the default |
agency_email_domains |
string[] | Users with these email domains are agency staff |
agency_user_ids |
int[] | User IDs that are agency staff |
hidden_menus |
string[] | Menu slugs hidden for clients (cosmetic) |
restricted_caps |
string[] | Capabilities removed from clients (real enforcement) |
Decisions
- Fail-safe. If both agency lists are empty, the plugin applies no restrictions and shows an admin notice. A bad config can never lock everyone out.
- Removes, never grants. Agency status only exempts a user from restrictions. It never adds capabilities, so a user who registers with an agency email domain gains nothing.
- Capabilities do the blocking, menus do not. Hiding a menu does not stop someone opening its URL. Removing the capability does, and also hides the menu and update nags for free.
hidden_menusis only for extra cosmetic cleanup. - User-based, not constant-based. Restrictions use the
user_has_capfilter and check the user being asked about, so cron, WP-CLI and REST requests behave the same as the browser. - Config file, not a settings screen. A settings page would be state-changing UI that needs nonces and its own attack surface. A file in version control per site is simpler and reviewable. A settings UI is a non-goal for v1.
- No client names, no secrets.
config.phpis git-ignored. Do not put passwords or API keys in it.
Non-goals (v1)
Settings UI, multisite, page-builder-specific changes, and replacing the WordPress logo inside the admin.
Testing
Before using on a client site, on staging:
- Log in as an agency user: the admin looks unchanged.
- Log in as a client user: no update nags, no Plugins menu, no Tools menu, no theme/plugin editors (try
/wp-admin/plugin-editor.phpdirectly, expect "Sorry, you are not allowed"). - Log out: the login screen shows your logo and the logo link goes to your site.
- Empty both agency lists: the plugin shows the notice and applies no restrictions.
Development
composer install
composer lint
Code follows the WordPress coding standards, all globals use the ubc_ / UBC_ prefix.

