diff --git a/src/assets/avatars/bot-recording.json b/scripts/bot/bot-recording.json
similarity index 100%
rename from src/assets/avatars/bot-recording.json
rename to scripts/bot/bot-recording.json
diff --git a/src/assets/avatars/bot-recording.mp3 b/scripts/bot/bot-recording.mp3
similarity index 100%
rename from src/assets/avatars/bot-recording.mp3
rename to scripts/bot/bot-recording.mp3
diff --git a/scripts/bot/run-bot.js b/scripts/bot/run-bot.js
index dc3cb0160099bb1e437af9c8ea3065e7eb84c0aa..2af6b846214fb4b18f0154b845009f9635efd219 100755
--- a/scripts/bot/run-bot.js
+++ b/scripts/bot/run-bot.js
@@ -3,10 +3,12 @@ const doc = `
 Usage:
     ./run-bot.js [options]
 Options:
+    -h --help         Show this screen
     -u --url=<url>    URL
     -o --host=<host>  Hubs host if URL is not specified [default: localhost:8080]
     -r --room=<room>  Room id
-    -h --help         Show this screen
+    -a --audio=<file> File to replay for the bot's outgoing audio
+    -d --data=<file>  File to replay for the bot's data channel
 `;
 
 const docopt = require("docopt").docopt;
@@ -51,13 +53,20 @@ function error(...objs) {
       await page.evaluate(() => console.log(navigator.userAgent));
       let retryCount = 5;
       let backoff = 1000;
-      const interact = async () => {
+      const loadFiles = async () => {
         try {
           // Interact with the page so that audio can play.
           await page.mouse.click(100, 100);
-          // Signal that the page has been interacted with.
-          await page.evaluate(() => window.interacted());
-          log("Interacted.");
+          if (options["--audio"]) {
+            const audioInput = await page.waitForSelector("#bot-audio-input");
+            audioInput.uploadFile(options["--audio"]);
+            log("Uploaded audio file.");
+          }
+          if (options["--data"]) {
+            const dataInput = await page.waitForSelector("#bot-data-input");
+            dataInput.uploadFile(options["--data"]);
+            log("Uploaded data file.");
+          }
         } catch (e) {
           log("Interaction error", e.message);
           if (retryCount-- < 0) {
@@ -67,10 +76,10 @@ function error(...objs) {
           log("Retrying...");
           backoff *= 2;
           // Retry interaction to start audio playback
-          setTimeout(interact, backoff);
+          setTimeout(loadFiles, backoff);
         }
       };
-      await interact();
+      await loadFiles();
     } catch (e) {
       log("Navigation error", e.message);
       setTimeout(navigate, 1000);
diff --git a/src/assets/stylesheets/loader.scss b/src/assets/stylesheets/loader.scss
index 448cd2203367b6ff5c9afaf24dad51a71c11a5d5..74ccb011d068d9013c78f61817d4c4d1f24053ab 100644
--- a/src/assets/stylesheets/loader.scss
+++ b/src/assets/stylesheets/loader.scss
@@ -1,4 +1,5 @@
 .loader-wrap {
+  pointer-events: none;
   position: relative;
   width: 100px;
   height: 90px;
@@ -6,7 +7,6 @@
 
 .loading-panel {
   @extend %default-font;
-  pointer-events: none;
   color: white;
   position: absolute;
   top: 0;
@@ -23,7 +23,7 @@
 .loading-panel__logo {
   width: 165px;
   height: 33px;
-  margin-top: 20px;
+  margin: 20px 0;
 }
 
 .loader-center,
diff --git a/src/components/avatar-replay.js b/src/components/avatar-replay.js
index b1dd13bf7f20ded46150246d445b2ac611277698..b739d976055f02c14882d8725e0e737baf09b9e1 100644
--- a/src/components/avatar-replay.js
+++ b/src/components/avatar-replay.js
@@ -27,6 +27,10 @@ AFRAME.registerComponent("avatar-replay", {
 
   update: function() {
     const { camera, leftController, rightController, recordingUrl } = this.data;
+    if (!recordingUrl) {
+      return;
+    }
+
     const fetchRecording = fetch(recordingUrl).then(resp => resp.json());
     camera.setAttribute("motion-capture-replayer", { loop: true });
     this._setupController(leftController);
diff --git a/src/hub.js b/src/hub.js
index cf326c1491e276b73ac8437c985d5b1ac4035e50..94a423315a65ad7424ecb8ab77bb69735ac410fa 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -171,6 +171,7 @@ function mountUI(scene, props = {}) {
     <UIRoot
       {...{
         scene,
+        isBotMode,
         concurrentLoadDetector,
         disableAutoExitOnConcurrentLoad,
         forcedVREntryType,
@@ -232,7 +233,9 @@ const onReady = async () => {
 
   const enterScene = async (mediaStream, enterInVR, hubId) => {
     const scene = document.querySelector("a-scene");
-    scene.classList.add("no-cursor");
+    if (!isBotMode) {
+      scene.classList.add("no-cursor");
+    }
     scene.renderer.sortObjects = true;
     const playerRig = document.querySelector("#player-rig");
     document.querySelector("canvas").classList.remove("blurred");
@@ -374,25 +377,24 @@ const onReady = async () => {
         playerRig.setAttribute("avatar-replay", {
           camera: "#player-camera",
           leftController: "#player-left-controller",
-          rightController: "#player-right-controller",
-          recordingUrl: "/assets/avatars/bot-recording.json"
+          rightController: "#player-right-controller"
         });
 
         const audioEl = document.createElement("audio");
-        audioEl.loop = true;
-        audioEl.muted = true;
-        audioEl.crossorigin = "anonymous";
-        audioEl.src = "/assets/avatars/bot-recording.mp3";
-        document.body.appendChild(audioEl);
-
-        // Wait for runner script to interact with the page so that we can play audio.
-        const interacted = new Promise(resolve => {
-          window.interacted = resolve;
-        });
-        const canPlay = new Promise(resolve => {
-          audioEl.addEventListener("canplay", resolve);
-        });
-        await Promise.all([canPlay, interacted]);
+        const audioInput = document.querySelector("#bot-audio-input");
+        audioInput.onchange = () => {
+          audioEl.loop = true;
+          audioEl.muted = true;
+          audioEl.crossorigin = "anonymous";
+          audioEl.src = URL.createObjectURL(audioInput.files[0]);
+          document.body.appendChild(audioEl);
+        };
+        const dataInput = document.querySelector("#bot-data-input");
+        dataInput.onchange = () => {
+          const url = URL.createObjectURL(dataInput.files[0]);
+          playerRig.setAttribute("avatar-replay", { recordingUrl: url });
+        };
+        await new Promise(resolve => audioEl.addEventListener("canplay", resolve));
         mediaStream.addTrack(audioEl.captureStream().getAudioTracks()[0]);
         audioEl.play();
       }
@@ -470,7 +472,6 @@ const onReady = async () => {
       const noop = () => {};
       // Replace renderer with a noop renderer to reduce bot resource usage.
       scene.renderer = { setAnimationLoop: noop, render: noop };
-      document.body.style.display = "none";
     }
   });
   environmentRoot.appendChild(initialEnvironmentEl);
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index b2fab4b9989c9c75ba5b3020e36756a09787c67d..934a8aee455aa3bc34d29f9957d1e61e957c6f42 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -60,6 +60,7 @@ class UIRoot extends Component {
     disableAutoExitOnConcurrentLoad: PropTypes.bool,
     forcedVREntryType: PropTypes.string,
     enableScreenSharing: PropTypes.bool,
+    isBotMode: PropTypes.bool,
     store: PropTypes.object,
     scene: PropTypes.object,
     linkChannel: PropTypes.object,
@@ -589,6 +590,16 @@ class UIRoot extends Component {
       );
     }
 
+    if (this.props.isBotMode) {
+      return (
+        <div className="loading-panel">
+          <img className="loading-panel__logo" src="../assets/images/logo.svg" />
+          <input type="file" id="bot-audio-input" accept="audio/*" />
+          <input type="file" id="bot-data-input" accept="application/json" />
+        </div>
+      );
+    }
+
     if (!this.props.initialEnvironmentLoaded || !this.props.availableVREntryTypes || !this.props.hubId) {
       return (
         <IntlProvider locale={lang} messages={messages}>
diff --git a/webpack.config.js b/webpack.config.js
index b4de849de503cee902cdc366972cc0a519d2f05e..bb9be202ded2fca453410c448f25d551a918b05b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -229,18 +229,6 @@ module.exports = (env, argv) => ({
         to: "hub-preview.png"
       }
     ]),
-    new CopyWebpackPlugin([
-      {
-        from: "src/assets/avatars/bot-recording.json",
-        to: "assets/avatars/bot-recording.json"
-      }
-    ]),
-    new CopyWebpackPlugin([
-      {
-        from: "src/assets/avatars/bot-recording.mp3",
-        to: "assets/avatars/bot-recording.mp3"
-      }
-    ]),
     // Extract required css and add a content hash.
     new ExtractTextPlugin({
       filename: "assets/stylesheets/[name]-[md5:contenthash:hex:20].css",