diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js
index 4217b6e6bca71b76bcf156c75d014cef051e3d4d..3443e05d2c0b5f5b763f28a5a146c10940251c5b 100644
--- a/src/lib/make-toolbox-xml.js
+++ b/src/lib/make-toolbox-xml.js
@@ -138,6 +138,18 @@ const motion = function (isStage, targetId) {
     `;
 };
 
+const xmlEscape = function (unsafe) {
+    return unsafe.replace(/[<>&'"]/g, c => {
+        switch (c) {
+        case '<': return '&lt;';
+        case '>': return '&gt;';
+        case '&': return '&amp;';
+        case '\'': return '&apos;';
+        case '"': return '&quot;';
+        }
+    });
+};
+
 const looks = function (isStage, targetId, costumeName, backdropName) {
     const hello = ScratchBlocks.ScratchMsgs.translate('LOOKS_HELLO', 'Hello!');
     const hmm = ScratchBlocks.ScratchMsgs.translate('LOOKS_HMM', 'Hmm...');
@@ -714,6 +726,10 @@ const makeToolboxXML = function (isStage, targetId, categoriesXML,
     costumeName = '', backdropName = '', soundName = '') {
     const gap = [categorySeparator];
 
+    costumeName = xmlEscape(costumeName);
+    backdropName = xmlEscape(backdropName);
+    soundName = xmlEscape(soundName);
+
     const everything = [
         xmlOpen,
         motion(isStage, targetId), gap,
diff --git a/test/integration/blocks.test.js b/test/integration/blocks.test.js
index d70396032cc5944054819f0482d018febc753d13..b39f05a79d469942eba11f4a9376a788a6c7365e 100644
--- a/test/integration/blocks.test.js
+++ b/test/integration/blocks.test.js
@@ -191,6 +191,24 @@ describe('Working with the blocks', () => {
         await clickText('newname', scope.blocksTab);
     });
 
+    test('Renaming costume with a special character should not break toolbox', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+
+        // Rename the costume
+        await clickText('Costumes');
+        const el = await findByXpath("//input[@value='costume1']");
+        await el.sendKeys('<NewCostume>');
+
+        // Make sure it is updated in the block menu
+        await clickText('Code');
+        await clickText('Looks', scope.blocksTab);
+        await driver.sleep(500); // Wait for scroll to finish
+        await clickText('<NewCostume>', scope.blocksTab);
+
+        await clickText('Sound', scope.blocksTab);
+    });
+
     // NOTE: This test describes the current behavior so that changes are not
     // introduced inadvertly, but I know this is not the desired behavior
     test('Adding costumes DOES NOT update the default costume name in the toolbox', async () => {