diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx
index e038790e476b6924e01ecfe1ece0526297f49768..7c757f392e94a8db2108ecea0f4cfc29e4ad0980 100644
--- a/src/containers/blocks.jsx
+++ b/src/containers/blocks.jsx
@@ -328,7 +328,10 @@ class Blocks extends React.Component {
             error.message = `Workspace Update Error: ${error.message}`;
             log.error(error);
         }
-        this.workspace.addChangeListener(this.props.vm.blockListener);
+        // All of the changes that happened during the load above are queued with
+        // timeouts, so re-enable the listener in the next tick, so it happens after
+        // the events are already fired.
+        setTimeout(() => this.workspace.addChangeListener(this.props.vm.blockListener));
 
         if (this.props.vm.editingTarget && this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
             const {scrollX, scrollY, scale} = this.state.workspaceMetrics[this.props.vm.editingTarget.id];
diff --git a/src/lib/project-fetcher-hoc.jsx b/src/lib/project-fetcher-hoc.jsx
index ca847fe3fdd58e7df3d417d6a59217779da4511c..1879d69e582468e3af8ffa5377f7a80c1e5e544a 100644
--- a/src/lib/project-fetcher-hoc.jsx
+++ b/src/lib/project-fetcher-hoc.jsx
@@ -4,10 +4,12 @@ import {intlShape, injectIntl} from 'react-intl';
 import bindAll from 'lodash.bindall';
 import {connect} from 'react-redux';
 
+import {setProjectUnchanged} from '../reducers/project-changed';
 import {
     LoadingStates,
     defaultProjectId,
     getIsFetchingWithId,
+    getIsShowingProject,
     onFetchedProjectData,
     projectError,
     setProjectId
@@ -54,6 +56,9 @@ const ProjectFetcherHOC = function (WrappedComponent) {
             if (this.props.isFetchingWithId && !prevProps.isFetchingWithId) {
                 this.fetchProject(this.props.reduxProjectId, this.props.loadingState);
             }
+            if (this.props.isShowingProject && !prevProps.isShowingProject) {
+                this.props.onProjectLoaded();
+            }
         }
         fetchProject (projectId, loadingState) {
             return storage
@@ -123,6 +128,7 @@ const ProjectFetcherHOC = function (WrappedComponent) {
 
     const mapStateToProps = state => ({
         isFetchingWithId: getIsFetchingWithId(state.scratchGui.projectState.loadingState),
+        isShowingProject: getIsShowingProject(state.scratchGui.projectState.loadingState),
         loadingState: state.scratchGui.projectState.loadingState,
         reduxProjectId: state.scratchGui.projectState.projectId
     });
@@ -130,7 +136,8 @@ const ProjectFetcherHOC = function (WrappedComponent) {
         onError: error => dispatch(projectError(error)),
         onFetchedProjectData: (projectData, loadingState) =>
             dispatch(onFetchedProjectData(projectData, loadingState)),
-        setProjectId: projectId => dispatch(setProjectId(projectId))
+        setProjectId: projectId => dispatch(setProjectId(projectId)),
+        onProjectLoaded: () => dispatch(setProjectUnchanged())
     });
     // Allow incoming props to override redux-provided props. Used to mock in tests.
     const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
diff --git a/src/lib/project-saver-hoc.jsx b/src/lib/project-saver-hoc.jsx
index 3fbbad36129765a87910a5048600007f4064a668..df0d6fa6f2753fc9236296ff79745987bc6355db 100644
--- a/src/lib/project-saver-hoc.jsx
+++ b/src/lib/project-saver-hoc.jsx
@@ -9,6 +9,7 @@ import {
     showAlertWithTimeout,
     showStandardAlert
 } from '../reducers/alerts';
+import {setProjectUnchanged} from '../reducers/project-changed';
 import {
     LoadingStates,
     autoUpdateProject,
@@ -167,7 +168,10 @@ const ProjectSaverHOC = function (WrappedComponent) {
                     storage.DataFormat.JSON,
                     body,
                     projectId
-                );
+                ).then(response => {
+                    this.props.onSetProjectUnchanged();
+                    return response;
+                });
             })
                 .catch(err => {
                     log.error(err);
@@ -255,6 +259,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
         onCreatedProject: (projectId, loadingState) => dispatch(doneCreatingProject(projectId, loadingState)),
         onCreateProject: () => dispatch(createProject()),
         onProjectError: error => dispatch(projectError(error)),
+        onSetProjectUnchanged: () => dispatch(setProjectUnchanged()),
         onShowAlert: alertType => dispatch(showStandardAlert(alertType)),
         onShowCreateSuccessAlert: () => showAlertWithTimeout(dispatch, 'createSuccess'),
         onShowCreatingAlert: () => showAlertWithTimeout(dispatch, 'creating'),
diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx
index 675deb04cd2458b180396b28503fc8c885fddaee..3e36865719974ecd995b1a30a5654abb13f101a5 100644
--- a/src/lib/vm-listener-hoc.jsx
+++ b/src/lib/vm-listener-hoc.jsx
@@ -8,6 +8,7 @@ import {connect} from 'react-redux';
 import {updateTargets} from '../reducers/targets';
 import {updateBlockDrag} from '../reducers/block-drag';
 import {updateMonitors} from '../reducers/monitors';
+import {setProjectChanged, setProjectUnchanged} from '../reducers/project-changed';
 import {setRunningState, setTurboState, setStartedState} from '../reducers/vm-status';
 import {showExtensionAlert} from '../reducers/alerts';
 import {updateMicIndicator} from '../reducers/mic-indicator';
@@ -24,6 +25,7 @@ const vmListenerHOC = function (WrappedComponent) {
             bindAll(this, [
                 'handleKeyDown',
                 'handleKeyUp',
+                'handleProjectChanged',
                 'handleTargetsUpdate'
             ]);
             // We have to start listening to the vm here rather than in
@@ -39,6 +41,7 @@ const vmListenerHOC = function (WrappedComponent) {
             this.props.vm.on('TURBO_MODE_OFF', this.props.onTurboModeOff);
             this.props.vm.on('PROJECT_RUN_START', this.props.onProjectRunStart);
             this.props.vm.on('PROJECT_RUN_STOP', this.props.onProjectRunStop);
+            this.props.vm.on('PROJECT_CHANGED', this.handleProjectChanged);
             this.props.vm.on('RUNTIME_STARTED', this.props.onRuntimeStarted);
             this.props.vm.on('PERIPHERAL_DISCONNECT_ERROR', this.props.onShowExtensionAlert);
             this.props.vm.on('MIC_LISTENING', this.props.onMicListeningUpdate);
@@ -69,6 +72,9 @@ const vmListenerHOC = function (WrappedComponent) {
                 document.removeEventListener('keyup', this.handleKeyUp);
             }
         }
+        handleProjectChanged () {
+            this.props.onProjectChanged();
+        }
         handleTargetsUpdate (data) {
             if (this.props.shouldEmitTargetsUpdate) {
                 this.props.onTargetsUpdate(data);
@@ -167,6 +173,8 @@ const vmListenerHOC = function (WrappedComponent) {
         },
         onProjectRunStart: () => dispatch(setRunningState(true)),
         onProjectRunStop: () => dispatch(setRunningState(false)),
+        onProjectChanged: () => dispatch(setProjectChanged()),
+        onProjectSaved: () => dispatch(setProjectUnchanged()),
         onRuntimeStarted: () => dispatch(setStartedState(true)),
         onTurboModeOn: () => dispatch(setTurboState(true)),
         onTurboModeOff: () => dispatch(setTurboState(false)),
diff --git a/src/lib/vm-manager-hoc.jsx b/src/lib/vm-manager-hoc.jsx
index 981fe8326f73baecc3576e24a47e55cc85b61fa2..d9efcd4de33900abfd8870d3d54a70a9cc155473 100644
--- a/src/lib/vm-manager-hoc.jsx
+++ b/src/lib/vm-manager-hoc.jsx
@@ -6,6 +6,7 @@ import {connect} from 'react-redux';
 import VM from 'scratch-vm';
 import AudioEngine from 'scratch-audio';
 
+import {setProjectUnchanged} from '../reducers/project-changed';
 import {
     LoadingStates,
     getIsLoadingWithId,
@@ -53,6 +54,7 @@ const vmManagerHOC = function (WrappedComponent) {
             return this.props.vm.loadProject(this.props.projectData)
                 .then(() => {
                     this.props.onLoadedProject(this.props.loadingState, this.props.canSave);
+                    this.props.onSetProjectUnchanged();
 
                     // If the vm is not running, call draw on the renderer manually
                     // This draws the state of the loaded project with no blocks running
@@ -123,7 +125,8 @@ const vmManagerHOC = function (WrappedComponent) {
     const mapDispatchToProps = dispatch => ({
         onError: error => dispatch(projectError(error)),
         onLoadedProject: (loadingState, canSave) =>
-            dispatch(onLoadedProject(loadingState, canSave))
+            dispatch(onLoadedProject(loadingState, canSave)),
+        onSetProjectUnchanged: () => dispatch(setProjectUnchanged())
     });
 
     // Allow incoming props to override redux-provided props. Used to mock in tests.
diff --git a/src/reducers/gui.js b/src/reducers/gui.js
index 6ae91b95a6d986e1630a879c9f17d54e3e565c29..edffa1cf363316d0a38a705eb20edc2f064b9566 100644
--- a/src/reducers/gui.js
+++ b/src/reducers/gui.js
@@ -14,6 +14,7 @@ import modalReducer, {modalsInitialState} from './modals';
 import modeReducer, {modeInitialState} from './mode';
 import monitorReducer, {monitorsInitialState} from './monitors';
 import monitorLayoutReducer, {monitorLayoutInitialState} from './monitor-layout';
+import projectChangedReducer, {projectChangedInitialState} from './project-changed';
 import projectStateReducer, {projectStateInitialState} from './project-state';
 import projectTitleReducer, {projectTitleInitialState} from './project-title';
 import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion';
@@ -45,6 +46,7 @@ const guiInitialState = {
     modals: modalsInitialState,
     monitors: monitorsInitialState,
     monitorLayout: monitorLayoutInitialState,
+    projectChanged: projectChangedInitialState,
     projectState: projectStateInitialState,
     projectTitle: projectTitleInitialState,
     restoreDeletion: restoreDeletionInitialState,
@@ -122,6 +124,7 @@ const guiReducer = combineReducers({
     modals: modalReducer,
     monitors: monitorReducer,
     monitorLayout: monitorLayoutReducer,
+    projectChanged: projectChangedReducer,
     projectState: projectStateReducer,
     projectTitle: projectTitleReducer,
     restoreDeletion: restoreDeletionReducer,
diff --git a/src/reducers/project-changed.js b/src/reducers/project-changed.js
new file mode 100644
index 0000000000000000000000000000000000000000..c59f6a107ea4cc9b7ec6cb0c4c403fb29a41fbf5
--- /dev/null
+++ b/src/reducers/project-changed.js
@@ -0,0 +1,28 @@
+const SET_PROJECT_CHANGED = 'scratch-gui/project-changed/SET_PROJECT_CHANGED';
+
+const initialState = false;
+
+const reducer = function (state, action) {
+    if (typeof state === 'undefined') state = initialState;
+    switch (action.type) {
+    case SET_PROJECT_CHANGED:
+        return action.changed;
+    default:
+        return state;
+    }
+};
+const setProjectChanged = () => ({
+    type: SET_PROJECT_CHANGED,
+    changed: true
+});
+const setProjectUnchanged = () => ({
+    type: SET_PROJECT_CHANGED,
+    changed: false
+});
+
+export {
+    reducer as default,
+    initialState as projectChangedInitialState,
+    setProjectChanged,
+    setProjectUnchanged
+};