We’ve all had that dreaded phone call. A client was poking around in Appearance, clicked the wrong button, and accidentally wiped out a key template. Or even worse: they decided to play developer, hopped into the Plugin Editor, hit save on some broken PHP, and white-screened the entire site.

It’s the classic handoff nightmare every developer runs into eventually. The good news is, the fix is pretty straightforward: just strip out the menus they have no business touching in the first place. Let’s walk through how to do this cleanly, covering a few sneaky quirks that most quick tutorials gloss over.

  1. Wrap your remove_menu_page() or remove_submenu_page() calls inside a dedicated function

  2. Hook that function into admin_menu (not admin_init - that’s the trap most people fall into)

  3. Set your hook priority to 999 for submenus like the Theme Editor so your code fires after core finishes registering everything

  4. Layer in current_user_can() checks whenever you want actual security, not just visual cleanup

Why Hiding Menus Isn’t the Same as Locking Them Down

Hiding a menu item simply sweeps it out of sight on the sidebar - it does not stop someone from typing the direct URL into their browser. If a client happens to know or bookmark wp-admin/theme-editor.php, they can still pull it right up.

If you’re after genuine security, you need to pair menu removal with proper capability checks via current_user_can(). What we’re tackling here is mostly the UX angle: decluttering the dashboard so clients only see the exact screens they need to do their everyday work.

Shutting Down the Theme and Plugin Editors

Nine times out of ten, the main reason we do this is to keep clients from touching raw code files. You can drop this snippet right into your functions.php:

PHP

code5 lines
function client_remove_editor_menus() {
    remove_submenu_page( 'themes.php', 'theme-editor.php' );
    remove_submenu_page( 'plugins.php', 'plugin-editor.php' );
}
add_action( 'admin_menu', 'client_remove_editor_menus', 999 );

Pay close attention to that 999 priority. WordPress registers these built-in editor submenus quite late in the boot cycle. If your hook runs too early at the default priority, it tries to remove something that doesn't exist yet, fails silently, and leaves the menu sitting right there.

On production sites, though, an even cleaner route is adding a single constant to wp-config.php:

PHP

code1 line
define( 'DISALLOW_FILE_EDIT', true );

This approach has the added bonus of cutting off REST API access to theme files, and it won't vanish if a theme gets updated. Go with DISALLOW_FILE_EDIT for bulletproof site-wide lockdown. Stick to the functions.php method when you want nuanced rules - like keeping the editor accessible for you while shielding it from other roles.

Stripping Down Top-Level Sidebar Menus

If you want to declutter the main sidebar down to the bare essentials, reach for remove_menu_page() alongside each top-level slug:

PHP

code12 lines
function client_clean_admin_sidebar() {
    remove_menu_page( 'edit.php' );                     // Posts
    remove_menu_page( 'upload.php' );                   // Media
    remove_menu_page( 'edit.php?post_type=page' );      // Pages
    remove_menu_page( 'edit-comments.php' );            // Comments
    remove_menu_page( 'themes.php' );                   // Appearance
    remove_menu_page( 'plugins.php' );                  // Plugins
    remove_menu_page( 'users.php' );                    // Users
    remove_menu_page( 'tools.php' );                    // Tools
    remove_menu_page( 'options-general.php' );          // Settings
}
add_action( 'admin_menu', 'client_clean_admin_sidebar' );

Naturally, you’ll want to leave the items your client actually relies on. If their day-to-day work is just drafting articles, keep edit.php and upload.php intact and yank out the rest.

A quick deployment tip: If you paste this straight into an active parent theme’s functions.php, the next theme update will wipe it clean out. Always place it inside a child theme or, even better, a small custom site-specific plugin managed in version control. Skipping that step is the number one reason clients message you months down the road asking why all their hidden menus suddenly came back.

Targeting Menus by User Role

Nuking menus across the board means you end up locking yourself out, too. The practical pattern here is checking who is currently logged in before ditching any menu screens:

PHP

code12 lines
function client_hide_menus_by_role() {
    if ( current_user_can( 'administrator' ) ) {
        return;
    }

    remove_menu_page( 'plugins.php' );
    remove_menu_page( 'themes.php' );
    remove_menu_page( 'tools.php' );
    remove_menu_page( 'options-general.php' );
    remove_menu_page( 'users.php' );
}
add_action( 'admin_menu', 'client_hide_menus_by_role' );

If you’re running WooCommerce, chances are you want the shop_manager to jump into store orders without letting them tamper with core settings. Inspecting specific capabilities is always cleaner than hardcoding role names:

PHP

code10 lines
function client_hide_menus_for_shop_managers() {
    if ( current_user_can( 'manage_woocommerce' ) && ! current_user_can( 'manage_options' ) ) {
        remove_menu_page( 'themes.php' );
        remove_menu_page( 'plugins.php' );
        remove_menu_page( 'users.php' );
        remove_menu_page( 'tools.php' );
        remove_menu_page( 'options-general.php' );
    }
}
add_action( 'admin_menu', 'client_hide_menus_for_shop_managers' );

By default, manage_options belongs strictly to administrators. Editors, Authors, and Shop Managers won't have it, making this check a foolproof way to draw the line between developer administration and day-to-day store management.

Cleaning Up Custom Post Type Menus

Whenever a build uses custom post types that your client doesn’t need cluttering their view, target the menu using its registered post type slug:

PHP

code5 lines
function client_hide_cpt_menus() {
    remove_menu_page( 'edit.php?post_type=portfolio' );
    remove_menu_page( 'edit.php?post_type=testimonial' );
}
add_action( 'admin_menu', 'client_hide_cpt_menus' );

Just swap out portfolio and testimonial with the actual slugs you registered in your setup.

Finding the Exact Menu Slug (Especially for Third-Party Plugins)

Here’s the step where most guides leave developers scratching their heads. Third-party plugins almost never use clean, predictable display names for their slugs, and taxonomy submenus frequently introduce weird URL encodings that break your code.

Instead of guessing, print the global menu arrays directly into your debug log. You can drop this tiny snippet in temporarily:

PHP

code5 lines
add_action( 'admin_menu', function() {
    global $menu, $submenu;
    error_log( print_r( $menu, true ) );
    error_log( print_r( $submenu, true ) );
}, 9999 );

Pop open wp-content/debug.log, and you’ll see the exact array structure. The real slug lives at index [2] for each entry. Grab the string you need, then make sure to delete this debugging hook.

Once you know the real slugs, stripping out third-party items looks like this:

PHP

code6 lines
function client_hide_plugin_menus() {
    remove_menu_page( 'wpcf7' );         // Contact Form 7
    remove_menu_page( 'rank-math' );     // Rank Math SEO
    remove_menu_page( 'woocommerce' );   // WooCommerce
}
add_action( 'admin_menu', 'client_hide_plugin_menus', 999 );

Watch out for the ampersand trap: This one has burned almost everyone at least once. Any submenu slug carrying query parameters is stored inside $submenu using HTML-encoded ampersands (&). That means edit-tags.php?taxonomy=category&post_type=product is stored internally as edit-tags.php?taxonomy=category&post_type=product.

If you pass a plain & into remove_submenu_page(), WordPress won't match it, the call silently fails, and the item stubbornly stays put. Check the raw array log before you tear your hair out.

Tidying Up Dashboard Widgets

Cleaning up the sidebar is only half the battle; the main dashboard screen usually needs some love, too. Clients almost never need default distractions like "WordPress News" or "Quick Draft." You can drop them using remove_meta_box() hooked to wp_dashboard_setup:

PHP

code6 lines
function client_clean_dashboard() {
    remove_meta_box( 'dashboard_primary', 'dashboard', 'side' );     // WordPress News
    remove_meta_box( 'dashboard_quick_press', 'dashboard', 'side' ); // Quick Draft
    remove_meta_box( 'dashboard_activity', 'dashboard', 'normal' );  // Activity
}
add_action( 'wp_dashboard_setup', 'client_clean_dashboard' );

Widget IDs can vary quite a bit, particularly with third-party extensions. You can quickly dump $GLOBALS['wp_meta_boxes']['dashboard'] inside a temporary function to inspect every active widget ID on that specific installation.

Troubleshooting: Why Isn't the Menu Disappearing?

If your code is executing but the items refuse to budge, run through this mental checklist:

You used the wrong hook: Hooking to admin_init instead of admin_menu is the most common blunder out there. The menu array hasn't even been constructed during admin_init, so any removal functions you call run entirely in a vacuum.

Your priority fired too early: Plenty of plugins register their admin screens unusually late - sometimes at priority 1020 or beyond. If your code runs at default priority 10, the plugin simply re-adds its menu right after you tried to remove it. Push your hook up to priority 1021 or higher and retest.

Slug encoding mismatch: As mentioned earlier, check if an unencoded ampersand is breaking your string match. Always pull the literal string out of the $submenu global to be 100% sure what WordPress is looking for.

Multisite quirks: When working across a WordPress Multisite network, certain screens belong strictly to the Network Admin area and require targeting via the network_admin_menu hook. Keep in mind that Super Admin privileges operate independently from standard local administrator capabilities.

Going the No-Code Route: Admin Menu Editor

If you’d rather not maintain custom PHP snippets, the Admin Menu Editor plugin is a rock-solid visual alternative. It gives you an intuitive drag-and-drop dashboard to hide, rename, reorder, and restrict access based on roles. The free tier covers everyday use cases, while the Pro add-on brings granular capability handling and individual user targeting.

For client builds where you want an easily auditable backend without writing custom code, it's a completely justifiable plugin dependency. On the other hand, if you’re already structuring custom functions, keeping it in code gives you pinpoint control and keeps overhead down.

Either route slots naturally into a solid maintenance routine: stripping out unused Gutenberg block CSS is another quick win to run during this same phase to lighten frontend bloat for clients who stick to classic layouts.

And if an administrative access mishap happens down the line while fine-tuning permissions, resetting a WordPress admin password through WP-CLI or direct database queries will get you back in fast. If an unexpected wp-config.php tweak takes the environment offline, a classic database connection error from a mistyped string is usually the culprit. Finally, if a client calls right after you hide upload.php panicking about broken thumbnails, don't worry - it’s just a coincidence: troubleshoot why the Media Library shows blank images instead of rolling back your clean new sidebar.