diff --git a/src/m_menu.c b/src/m_menu.c
index e345a6a87f3be80dc7da620e7342f9d8efc71287..c38459fe0227f1dc627f77d642db234f1710669b 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -140,7 +140,6 @@ static char *char_notes = NULL;
 
 boolean menuactive = false;
 boolean fromlevelselect = false;
-tic_t shieldprompt_timer = 0; // Show a prompt about the new Shield button for old configs // TODO: 2.3: Remove
 
 typedef enum
 {
@@ -3162,7 +3161,6 @@ static void Command_Manual_f(void)
 	if (modeattacking)
 		return;
 	M_StartControlPanel();
-	if (shieldprompt_timer) return; // TODO: 2.3: Delete this line
 	currentMenu = &MISC_HelpDef;
 	itemOn = 0;
 }
@@ -3342,7 +3340,6 @@ boolean M_Responder(event_t *ev)
 				if (modeattacking)
 					return true;
 				M_StartControlPanel();
-				if (shieldprompt_timer) return true; // TODO: 2.3: Delete this line
 				M_Options(0);
 				// Uncomment the below if you want the menu to reset to the top each time like before. M_SetupNextMenu will fix it automatically.
 				//OP_SoundOptionsDef.lastOn = 0;
@@ -3353,7 +3350,6 @@ boolean M_Responder(event_t *ev)
 				if (modeattacking)
 					return true;
 				M_StartControlPanel();
-				if (shieldprompt_timer) return true; // TODO: 2.3: Delete this line
 				M_Options(0);
 				M_VideoModeMenu(0);
 				return true;
@@ -3365,7 +3361,6 @@ boolean M_Responder(event_t *ev)
 				if (modeattacking)
 					return true;
 				M_StartControlPanel();
-				if (shieldprompt_timer) return true; // TODO: 2.3: Delete this line
 				M_Options(0);
 				M_SetupNextMenu(&OP_MainDef);
 				return true;
@@ -3643,230 +3638,6 @@ void M_Drawer(void)
 	}
 }
 
-// Handle the "Do you want to assign Shield Ability now?" pop-up for old configs // TODO: 2.3: Remove this line...
-static UINT8 shieldprompt_currentchoice = 0; // ...and this line...
-
-static void M_ShieldPromptUseDefaults(void) // ...and this function
-{
-	// With a default config from v2.2.10 to v2.2.13, the B button will be set to Custom 1,
-	// and Controls per Key defaults to "One", so it will override the default Shield button.
-	// A default config from v2.2.0 to v2.2.9 has Next Weapon on B, so it suffers from this too.
-
-	// So for "Use default Shield Ability buttons", we should update old configs to mitigate gamepad conflicts
-	// (even with "Several" Controls per Key!), and show a message with the default bindings
-
-	for (setupcontrols = gamecontrol; true; setupcontrols = gamecontrolbis) // Do stuff for both P1 and P2
-	{
-		INT32 JOY1 = (setupcontrols == gamecontrol) ? KEY_JOY1 : KEY_2JOY1; // Is this for P1 or for P2?
-
-		if ((setupcontrols[GC_CUSTOM1][0] == JOY1+1 || setupcontrols[GC_CUSTOM1][1] == JOY1+1)
-		&&  (setupcontrols[GC_CUSTOM2][0] == JOY1+3 || setupcontrols[GC_CUSTOM2][1] == JOY1+3)
-		&&  (setupcontrols[GC_CUSTOM3][0] == JOY1+8 || setupcontrols[GC_CUSTOM3][1] == JOY1+8))
-		{
-			// If the player has v2.2.13's default gamepad Custom 1/2/3 buttons,
-			// shuffle Custom 1/2/3 around to make room for Shield Ability on B
-			UINT8 shield_slot  = (setupcontrols[GC_SHIELD ][0] == KEY_NULL  ) ? 0 : 1;
-			UINT8 custom1_slot = (setupcontrols[GC_CUSTOM1][0] == JOY1+1) ? 0 : 1;
-			UINT8 custom2_slot = (setupcontrols[GC_CUSTOM2][0] == JOY1+3) ? 0 : 1;
-			UINT8 custom3_slot = (setupcontrols[GC_CUSTOM3][0] == JOY1+8) ? 0 : 1;
-
-			setupcontrols[GC_SHIELD ][shield_slot ] = JOY1+1; // Assign Shield Ability to B
-			setupcontrols[GC_CUSTOM1][custom1_slot] = JOY1+3; // Move Custom 1 from B to Y
-			setupcontrols[GC_CUSTOM2][custom2_slot] = JOY1+8; // Move Custom 2 from Y to LS
-			setupcontrols[GC_CUSTOM3][custom3_slot] = KEY_NULL; // Unassign Custom 3 from LS...
-			// (The alternative would be to check and update the ENTIRE gamepad layout.
-			// That'd be nice, but it would mess with people that are used to the old defaults.)
-		}
-		else if ((setupcontrols[GC_WEAPONNEXT][0] == JOY1+1 || setupcontrols[GC_WEAPONNEXT][1] == JOY1+1)
-		&&       (setupcontrols[GC_WEAPONPREV][0] == JOY1+2 || setupcontrols[GC_WEAPONPREV][1] == JOY1+2))
-		{
-			// Or if the user has a default config from v2.2.0 to v2.2.9,
-			// the B button will be Next Weapon, and X will be Previous Weapon.
-			// It's "safe" to discard one of them, you just have to press X multiple times to select in the other direction
-			UINT8 shield_slot  = (setupcontrols[GC_SHIELD    ][0] == KEY_NULL  ) ? 0 : 1;
-			UINT8 nweapon_slot = (setupcontrols[GC_WEAPONNEXT][0] == JOY1+1) ? 0 : 1;
-			UINT8 pweapon_slot = (setupcontrols[GC_WEAPONPREV][0] == JOY1+2) ? 0 : 1;
-
-			setupcontrols[GC_SHIELD    ][shield_slot ] = JOY1+1; // Assign Shield Ability to B
-			setupcontrols[GC_WEAPONNEXT][nweapon_slot] = JOY1+3; // Move Next Weapon from B to X
-			setupcontrols[GC_WEAPONPREV][pweapon_slot] = KEY_NULL; // Unassign Previous Weapon from X
-		}
-
-		if (setupcontrols == gamecontrolbis) // If we've already updated both players, break out
-			break;
-	}
-
-
-	// Now, show a message about the default Shield Ability bindings
-	if ((gamecontrol[GC_SHIELD][0] == KEY_LALT && gamecontrol[GC_SHIELD][1] == KEY_JOY1+1)
-	||  (gamecontrol[GC_SHIELD][0] == KEY_JOY1+1 && gamecontrol[GC_SHIELD][1] == KEY_LALT))
-	{
-		// Left Alt and the B button are both assigned
-		M_StartMessage(M_GetText("Shield Ability defaults to\nthe \x82""Left Alt\x80"" key on keyboard,\nand the \x85""B button\x80"" on gamepads."
-		"\n\nYou can always reassign it\nin the Options menu later."
-		"\n\n\nPress 'Enter' to continue\n"),
-			NULL, MM_NOTHING);
-		MessageDef.x = 43; // Change the pop-up message's background position/width
-		MessageDef.lastOn = (MessageDef.lastOn & ~0xFF) | 27;
-	}
-	else if (gamecontrol[GC_SHIELD][0] == KEY_LALT || gamecontrol[GC_SHIELD][1] == KEY_LALT)
-	{
-		// Left Alt is assigned, but the B button isn't.
-		M_StartMessage(M_GetText("Shield Ability defaults to\nthe \x82""Left Alt\x80"" key on keyboard.\nThe \x85""B button\x80"" on gamepads was taken."
-		"\n\nYou can always reassign it\nin the Options menu later."
-		"\n\n\nPress 'Enter' to continue\n"),
-			NULL, MM_NOTHING);
-		MessageDef.x = 24; // Change the pop-up message's background position/width
-		MessageDef.lastOn = (MessageDef.lastOn & ~0xFF) | 32;
-	}
-	else if (gamecontrol[GC_SHIELD][0] == KEY_JOY1+1 || gamecontrol[GC_SHIELD][1] == KEY_JOY1+1)
-	{
-		// The B button is assigned, but Left Alt isn't
-		M_StartMessage(M_GetText("Shield Ability defaults to\nthe \x85""B button\x80"" on gamepads.\nThe \x82""Left Alt\x80"" key on keyboard was taken."
-		"\n\nYou can always reassign it\nin the Options menu later."
-		"\n\n\nPress 'Enter' to continue\n"),
-			NULL, MM_NOTHING);
-		MessageDef.x = 8; // Change the pop-up message's background position/width
-		MessageDef.lastOn = (MessageDef.lastOn & ~0xFF) | 36;
-	}
-	else if (gamecontrol[GC_SHIELD][0] == KEY_NULL && gamecontrol[GC_SHIELD][1] == KEY_NULL)
-	{
-		// Neither Left Alt nor the B button are assigned
-		M_StartMessage(M_GetText("Shield Ability is unassigned!\nThe \x82""Left Alt\x80"" key on keyboard and\nthe \x85""B button\x80"" on gamepads were taken."
-		"\n\nYou should assign Shield Ability\nin the Options menu later."
-		"\n\n\nPress 'Enter' to continue\n"),
-			NULL, MM_NOTHING);
-		MessageDef.x = 19; // Change the pop-up message's background position/width
-		MessageDef.lastOn = (MessageDef.lastOn & ~0xFF) | 33;
-	}
-	else
-	{
-		// Neither Left Alt nor the B button are assigned... but something else is???
-		// (This can technically happen if you edit your config or use setcontrol in the console before opening the menu)
-		char keystr[16+16+2+7+1]; // Two 16-char keys + two colour codes + "' and '" + null
-
-		if (gamecontrol[GC_SHIELD][0] != KEY_NULL && gamecontrol[GC_SHIELD][1] != KEY_NULL)
-			STRBUFCPY(keystr, va("%s\x80""' and '\x82""%s",
-				G_KeyNumToName(gamecontrol[GC_SHIELD][0]),
-				G_KeyNumToName(gamecontrol[GC_SHIELD][1])));
-		else if (gamecontrol[GC_SHIELD][0] != KEY_NULL)
-			STRBUFCPY(keystr, G_KeyNumToName(gamecontrol[GC_SHIELD][0]));
-		else //if (gamecontrol[GC_SHIELD][1] != KEY_NULL)
-			STRBUFCPY(keystr, G_KeyNumToName(gamecontrol[GC_SHIELD][1]));
-
-		M_StartMessage(va("Shield Ability is assigned to\n'\x82""%s\x80""'."
-		"\n\nYou can always reassign it\nin the Options menu later."
-		"\n\n\nPress 'Enter' to continue\n",
-			keystr), NULL, MM_NOTHING);
-		MessageDef.x = 23; // Change the pop-up message's background position/width
-		MessageDef.lastOn = (MessageDef.lastOn & ~0xFF) | 32;
-	}
-}
-
-static void M_HandleShieldPromptMenu(INT32 choice) // TODO: 2.3: Remove
-{
-	switch (choice)
-	{
-		case KEY_ESCAPE:
-			if (I_GetTime() <= shieldprompt_timer) // Don't mash past the pop-up by accident!
-				break;
-
-			S_StartSound(NULL, sfx_menu1);
-			noFurtherInput = true;
-			shieldprompt_timer = 0;
-			M_ShieldPromptUseDefaults();
-			break;
-
-		case KEY_ENTER:
-			if (I_GetTime() <= shieldprompt_timer) // Don't mash past the pop-up by accident!
-				break;
-
-			S_StartSound(NULL, sfx_menu1);
-			noFurtherInput = true;
-			shieldprompt_timer = 0;
-
-			if (shieldprompt_currentchoice == 0)
-			{
-				OP_ChangeControlsDef.lastOn = 8; // Highlight Shield Ability in the controls menu
-				M_Setup1PControlsMenu(0); // Set up P1's controls menu and call M_SetupNextMenu
-			}
-			else if (shieldprompt_currentchoice == 1) // Copy the Spin buttons to the Shield buttons
-			{
-				CV_SetValue(&cv_controlperkey, 2); // Make sure that Controls per Key is "Several"
-
-				gamecontrol   [GC_SHIELD][0] = gamecontrol   [GC_SPIN][0];
-				gamecontrol   [GC_SHIELD][1] = gamecontrol   [GC_SPIN][1];
-				gamecontrolbis[GC_SHIELD][0] = gamecontrolbis[GC_SPIN][0];
-				gamecontrolbis[GC_SHIELD][1] = gamecontrolbis[GC_SPIN][1];
-				CV_SetValue(&cv_shieldaxis,  cv_spinaxis.value);
-				CV_SetValue(&cv_shieldaxis2, cv_spinaxis2.value);
-
-				M_StartMessage(M_GetText("Spin and Shield Ability are now\nthe same button."
-				"\n\nYou can always reassign them\nin the Options menu later."
-				"\n\n\nPress 'Enter' to continue\n"),
-					NULL, MM_NOTHING);
-				MessageDef.x = 36; // Change the pop-up message's background position/width
-				MessageDef.lastOn = (MessageDef.lastOn & ~0xFF) | 29;
-			}
-			else
-				M_ShieldPromptUseDefaults();
-			break;
-
-		case KEY_UPARROW:
-			S_StartSound(NULL, sfx_menu1);
-			shieldprompt_currentchoice = (shieldprompt_currentchoice+2)%3;
-			break;
-
-		case KEY_DOWNARROW:
-			S_StartSound(NULL, sfx_menu1);
-			shieldprompt_currentchoice = (shieldprompt_currentchoice+1)%3;
-			break;
-	}
-
-	MessageDef.prevMenu = &MainDef;
-}
-
-static void M_DrawShieldPromptMenu(void) // TODO: 2.3: Remove
-{
-	INT16 cursorx = (BASEVIDWIDTH/2) - 24;
-
-	V_DrawFill(10-3, 68-3, 300+6, 40+6, 159);
-	// V_DrawCenteredString doesn't centre newlines, so we have to draw each line separately
-	V_DrawCenteredString(BASEVIDWIDTH/2, 68, V_ALLOWLOWERCASE, "Welcome back! Since you last played,");
-	V_DrawCenteredString(BASEVIDWIDTH/2, 76, V_ALLOWLOWERCASE, "Spin has been split into separate");
-	V_DrawCenteredString(BASEVIDWIDTH/2, 84, V_ALLOWLOWERCASE, "\"Spin\" and \"Shield Ability\" controls.");
-
-	V_DrawCenteredString(BASEVIDWIDTH/2, 98, V_ALLOWLOWERCASE, "Do you want to assign Shield Ability now?");
-
-
-	V_DrawCenteredString(BASEVIDWIDTH/2, 164,
-		(shieldprompt_currentchoice == 0) ? V_YELLOWMAP : 0, "Open Control Setup");
-	V_DrawCenteredString(BASEVIDWIDTH/2, 172,
-		(shieldprompt_currentchoice == 1) ? V_YELLOWMAP : 0, "Keep the old behaviour");
-	V_DrawCenteredString(BASEVIDWIDTH/2, 180,
-		(shieldprompt_currentchoice == 2) ? V_YELLOWMAP : 0, "Use default Shield Ability buttons");
-
-	switch (shieldprompt_currentchoice)
-	{
-		case 0:  cursorx -= V_StringWidth("Open Control Setup",                 0)/2; break;
-		case 1:  cursorx -= V_StringWidth("Keep the old behaviour",             0)/2; break;
-		default: cursorx -= V_StringWidth("Use default Shield Ability buttons", 0)/2; break;
-	}
-	V_DrawScaledPatch(cursorx, 164 + (shieldprompt_currentchoice*8), 0, W_CachePatchName("M_CURSOR", PU_PATCH));
-}
-
-static menuitem_t OP_ShieldPromptMenu[] = {{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleShieldPromptMenu, 0}}; // TODO: 2.3: Remove
-
-menu_t OP_ShieldPromptDef = { // TODO: 2.3: Remove
-	MN_SPECIAL,
-	NULL,
-	1,
-	&MainDef,
-	OP_ShieldPromptMenu,
-	M_DrawShieldPromptMenu,
-	0, 0, 0, NULL
-};
-
 //
 // M_StartControlPanel
 //
@@ -3898,15 +3669,6 @@ void M_StartControlPanel(void)
 		currentMenu = &MainDef;
 		itemOn = singleplr;
 		M_UpdateItemOn();
-
-		if (shieldprompt_timer) // For old configs, show a pop-up about the new Shield button // TODO: 2.3: Remove
-		{
-			S_StartSound(NULL, sfx_strpst);
-			noFurtherInput = true;
-			shieldprompt_timer = I_GetTime() + TICRATE; // Don't mash past the pop-up by accident!
-
-			M_SetupNextMenu(&OP_ShieldPromptDef);
-		}
 	}
 	else if (modeattacking)
 	{
diff --git a/src/m_menu.h b/src/m_menu.h
index cfe811d0be536b82ec3aeec7f0ab920a55a3d6c1..552d28122b229788820caeef36fd79aa3aa59c4a 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -176,7 +176,6 @@ typedef struct
 extern menupres_t menupres[NUMMENUTYPES];
 extern UINT32 prevMenuId;
 extern UINT32 activeMenuId;
-extern tic_t shieldprompt_timer; // Show a prompt about the new Shield button for old configs // TODO: 2.3: Remove
 
 void M_InitMenuPresTables(void);
 UINT8 M_GetYoungestChildMenu(void);
diff --git a/src/m_misc.c b/src/m_misc.c
index dda3ffc86dc70de55ac3d3ce5df48cc3d797299d..24616e9db42e2b27bab8c4d359e5d92b3945f6d7 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -560,11 +560,6 @@ void M_FirstLoadConfig(void)
 	COM_BufInsertText(va("exec \"%s\"\n", configfile));
 	// no COM_BufExecute() needed; that does it right away
 
-	// For configs loaded at startup only, check for pre-Shield-button configs // TODO: 2.3: Remove
-	if (GETMAJOREXECVERSION(cv_execversion.value) < 55 // Pre-v2.2.14 configs
-	&& cv_execversion.value != 25) // Make sure that the config exists, too
-		shieldprompt_timer = 1;
-
 	// don't filter anymore vars and don't let this convsvar be changed
 	COM_BufInsertText(va("%s \"%d\"\n", cv_execversion.name, EXECVERSION));
 	CV_ToggleExecVersion(false);