diff --git a/crates/engine/src/cmd/mix.rs b/crates/engine/src/cmd/mix.rs
--- a/crates/engine/src/cmd/mix.rs
+++ b/crates/engine/src/cmd/mix.rs
@@ -40,7 +40,7 @@ pub fn specs() -> Vec<CommandSpec> {
             }
             Ok(json!({}))
         }),
-        cmd!("mix.record_arm", "Record Enable", [], Some("Shift+R"), "{tracks?, value?: bool}", has_selection, |e, p| toggle(
+        cmd!(noundo "mix.record_arm", "Record Enable", [], Some("Shift+R"), "{tracks?, value?: bool}", has_selection, |e, p| toggle_runtime(
             e,
             p,
             "mix.record_arm",
@@ -49,7 +49,7 @@ pub fn specs() -> Vec<CommandSpec> {
             }),
             |tr| tr.mixer.record_arm
         )),
-        cmd!("mix.input_monitor", "Input Monitoring", [], Some("Shift+I"), "{tracks?, value?: bool}", has_selection, |e, p| toggle(
+        cmd!(noundo "mix.input_monitor", "Input Monitoring", [], Some("Shift+I"), "{tracks?, value?: bool}", has_selection, |e, p| toggle_runtime(
             e,
             p,
             "mix.input_monitor",
@@ -216,6 +216,22 @@ fn toggle(e: &mut Engine, p: &Value, cmd: &str, set: fn(&mut Session, TrackId, b
     Ok(json!({"value": v}))
 }
 
+fn toggle_runtime(
+    e: &mut Engine,
+    p: &Value,
+    cmd: &str,
+    set: fn(&mut Session, TrackId, bool),
+    get: fn(&soundcraft_model::Track) -> bool,
+) -> Result<Value> {
+    let tracks = tracks_required(e, cmd, p)?;
+    let s = e.runtime_session_mut();
+    let v = p.get("value").and_then(Value::as_bool).unwrap_or_else(|| !tracks.iter().all(|t| s.track(*t).is_some_and(get)));
+    for t in &tracks {
+        set(s, *t, v);
+    }
+    Ok(json!({"value": v}))
+}
+
 fn volume(e: &mut Engine, p: &Value) -> Result<Value> {
     let tracks = tracks_required(e, "mix.volume", p)?;
     let abs = p.get("db").and_then(Value::as_f64);
@@ -947,6 +963,52 @@ mod tests {
         e
     }
 
+    #[test]
+    fn live_input_states_are_transient_and_survive_document_history() {
+        let mut e = Engine::default();
+        e.execute("track.new", &json!({})).unwrap();
+        let track = e.session().tracks[0].id;
+        e.mark_clean();
+        let history = e.undo_history();
+
+        e.execute("mix.record_arm", &json!({"tracks": [track.0], "value": true})).unwrap();
+        e.execute("track.set_record_to_input_only", &json!({})).unwrap();
+        let mixer = &e.session().track(track).unwrap().mixer;
+        assert!(mixer.record_arm && mixer.input_monitor);
+        assert!(!e.is_dirty(), "live input state is not a document change");
+        assert_eq!(e.undo_history(), history, "live input state has no Undo entry");
+
+        // A later document edit captures a full Session snapshot. Undoing or redoing it must not
+        // resurrect the live state that happened to be present when that snapshot was taken.
+        e.execute("mix.volume", &json!({"tracks": [track.0], "db": -6.0})).unwrap();
+        e.execute("mix.record_arm", &json!({"tracks": [track.0], "value": false})).unwrap();
+        e.execute("mix.input_monitor", &json!({"tracks": [track.0], "value": false})).unwrap();
+        assert!(e.is_dirty());
+
+        assert!(e.undo());
+        let mixer = &e.session().track(track).unwrap().mixer;
+        assert!(!mixer.record_arm && !mixer.input_monitor);
+        assert!(!e.is_dirty(), "undo returned to the saved document");
+
+        assert!(e.redo());
+        let mixer = &e.session().track(track).unwrap().mixer;
+        assert!(!mixer.record_arm && !mixer.input_monitor);
+        assert!(e.is_dirty());
+
+        // Live states that are enabled after another document edit survive both directions too.
+        e.execute("mix.volume", &json!({"tracks": [track.0], "db": -12.0})).unwrap();
+        e.execute("mix.record_arm", &json!({"tracks": [track.0], "value": true})).unwrap();
+        e.execute("mix.input_monitor", &json!({"tracks": [track.0], "value": true})).unwrap();
+
+        assert!(e.undo());
+        let mixer = &e.session().track(track).unwrap().mixer;
+        assert!(mixer.record_arm && mixer.input_monitor);
+
+        assert!(e.redo());
+        let mixer = &e.session().track(track).unwrap().mixer;
+        assert!(mixer.record_arm && mixer.input_monitor);
+    }
+
     fn volumes(e: &Engine) -> Vec<f32> {
         [3, 6].iter().map(|id| e.session().track(TrackId(*id)).unwrap().mixer.volume_db).collect()
     }
diff --git a/crates/engine/src/cmd/track.rs b/crates/engine/src/cmd/track.rs
--- a/crates/engine/src/cmd/track.rs
+++ b/crates/engine/src/cmd/track.rs
@@ -97,8 +97,8 @@ pub fn specs() -> Vec<CommandSpec> {
         cmd!("track.bounce", "Bounce...", ["Track"], Some("Cmd+Alt+Shift+B"), "{tracks?}", has_selection, commit),
         cmd!("track.bypass_inserts", "All", ["Track", "Bypass Inserts"], None, "{tracks?, bypass?: bool}", has_selection, bypass_inserts),
         cmd!("track.mute_sends", "All", ["Track", "Mute Sends"], None, "{tracks?, mute?: bool}", has_selection, mute_sends),
-        cmd!("track.set_record_to_input_only", "Set Record Tracks to Input Only", ["Track"], Some("Alt+K"), "{}", always, |e, _| {
-            let s = e.session_mut();
+        cmd!(noundo "track.set_record_to_input_only", "Set Record Tracks to Input Only", ["Track"], Some("Alt+K"), "{}", always, |e, _| {
+            let s = e.runtime_session_mut();
             let mut n = 0;
             for t in &mut s.tracks {
                 if t.mixer.record_arm {
diff --git a/crates/engine/src/cmd/track_more.rs b/crates/engine/src/cmd/track_more.rs
--- a/crates/engine/src/cmd/track_more.rs
+++ b/crates/engine/src/cmd/track_more.rs
@@ -283,15 +283,17 @@ fn load_preset(e: &mut Engine, p: &Value) -> Result<Value> {
     let s = e.session_mut();
     for t in &tracks {
         if let Some(tr) = s.track_mut(*t) {
-            // Routing is session-specific: keep the track's own input/output, VCA and arm state.
-            let keep = (tr.mixer.input.clone(), tr.mixer.output.clone(), tr.mixer.vca, tr.mixer.record_arm, tr.mixer.pan.clone());
+            // Routing and live input state are track-specific: a preset must not replace them.
+            let keep =
+                (tr.mixer.input.clone(), tr.mixer.output.clone(), tr.mixer.vca, tr.mixer.record_arm, tr.mixer.input_monitor, tr.mixer.pan.clone());
             tr.mixer = mixer.clone();
             tr.mixer.input = keep.0;
             tr.mixer.output = keep.1;
             tr.mixer.vca = keep.2;
             tr.mixer.record_arm = keep.3;
-            if tr.mixer.pan.len() != keep.4.len() {
-                tr.mixer.pan = keep.4;
+            tr.mixer.input_monitor = keep.4;
+            if tr.mixer.pan.len() != keep.5.len() {
+                tr.mixer.pan = keep.5;
             }
             if tr.kind == TrackKind::Instrument && instrument.is_some() {
                 tr.instrument = instrument.clone();
@@ -612,10 +614,13 @@ mod tests {
         e.execute("edit.select", &json!({"tracks": ["Bass"]})).unwrap();
         e.execute("track.save_preset", &json!({"path": path})).unwrap();
         e.execute("edit.select", &json!({"tracks": ["Hats"]})).unwrap();
+        e.execute("mix.record_arm", &json!({"value": true})).unwrap();
+        e.execute("mix.input_monitor", &json!({"value": true})).unwrap();
         e.execute("track.load_preset", &json!({"path": path})).unwrap();
         let hats = e.session().track_by_name("Hats").unwrap();
         assert_eq!(hats.mixer.inserts[0].as_ref().map(|i| i.plugin.as_str()), Some("compressor"));
         assert_eq!(hats.mixer.volume_db, -5.0);
+        assert!(hats.mixer.record_arm && hats.mixer.input_monitor, "loading a preset keeps live input state");
         let _ = std::fs::remove_dir_all(&dir);
     }
 
diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs
--- a/crates/engine/src/lib.rs
+++ b/crates/engine/src/lib.rs
@@ -187,6 +187,13 @@ impl Engine {
         Arc::make_mut(&mut self.doc)
     }
 
+    /// Mutable access for live state that redraws and reaches playback but is not part of the
+    /// saved document. Callers must also keep the fields out of serialization and Undo/Redo.
+    pub(crate) fn runtime_session_mut(&mut self) -> &mut Session {
+        self.revision = self.revision.wrapping_add(1);
+        Arc::make_mut(&mut self.doc)
+    }
+
     /// Like `session_mut`, for `edit.zoom` only and without bumping `revision`.
     pub fn zoom_mut(&mut self) -> &mut ZoomState {
         self.dirty = true;
@@ -246,6 +253,7 @@ impl Engine {
         let Some(h) = self.undo.pop() else { return false };
         let cur = std::mem::replace(&mut self.doc, h.doc);
         self.keep_edit_mode(&cur);
+        self.keep_runtime_track_states(&cur);
         self.redo.push(HistoryEntry { label: h.label, doc: cur });
         self.revision = self.revision.wrapping_add(1);
         self.dirty = true;
@@ -256,6 +264,7 @@ impl Engine {
         let Some(h) = self.redo.pop() else { return false };
         let cur = std::mem::replace(&mut self.doc, h.doc);
         self.keep_edit_mode(&cur);
+        self.keep_runtime_track_states(&cur);
         self.undo.push(HistoryEntry { label: h.label, doc: cur });
         self.revision = self.revision.wrapping_add(1);
         self.dirty = true;
@@ -276,6 +285,17 @@ impl Engine {
         }
     }
 
+    /// Undo and Redo must not restore live input states captured in document snapshots. A track
+    /// absent from the current document comes back safe rather than inheriting a historical arm.
+    fn keep_runtime_track_states(&mut self, from: &Session) {
+        for track in &mut Arc::make_mut(&mut self.doc).tracks {
+            let (record_arm, input_monitor) =
+                from.track(track.id).map_or((false, false), |current| (current.mixer.record_arm, current.mixer.input_monitor));
+            track.mixer.record_arm = record_arm;
+            track.mixer.input_monitor = input_monitor;
+        }
+    }
+
     /// Record an undo point manually (for interactive gestures committed by the UI).
     pub fn push_undo(&mut self, label: &str, before: Arc<Session>) {
         if Arc::ptr_eq(&before, &self.doc) {
@@ -424,7 +444,7 @@ impl Engine {
     }
 }
 
-/// Equal apart from what changes without editing: the selection, the playhead and the zoom.
+/// Equal apart from what changes without editing: the selection, playhead, zoom and live input states.
 fn same_document(saved: &Session, doc: &Session) -> bool {
     let mut doc = doc.clone();
     let (from, to) = (&saved.edit, &mut doc.edit);
@@ -434,6 +454,12 @@ fn same_document(saved: &Session, doc: &Session) -> bool {
     to.selected_clips.clone_from(&from.selected_clips);
     to.playhead = from.playhead;
     to.zoom = from.zoom;
+    for track in &mut doc.tracks {
+        if let Some(saved_track) = saved.track(track.id) {
+            track.mixer.record_arm = saved_track.mixer.record_arm;
+            track.mixer.input_monitor = saved_track.mixer.input_monitor;
+        }
+    }
     *saved == doc
 }
 
diff --git a/crates/model/src/mixer.rs b/crates/model/src/mixer.rs
--- a/crates/model/src/mixer.rs
+++ b/crates/model/src/mixer.rs
@@ -502,9 +502,11 @@ pub struct Mixer {
     pub solo: bool,
     #[serde(default)]
     pub solo_safe: bool,
-    #[serde(default)]
+    /// Live record-enable state. Runtime-only: opening a session must never arm an input.
+    #[serde(skip)]
     pub record_arm: bool,
-    #[serde(default)]
+    /// Live input-monitor state. Runtime-only: opening a session must never enable monitoring.
+    #[serde(skip)]
     pub input_monitor: bool,
     #[serde(default)]
     pub phase_invert: bool,
diff --git a/crates/model/src/session.rs b/crates/model/src/session.rs
--- a/crates/model/src/session.rs
+++ b/crates/model/src/session.rs
@@ -681,6 +681,30 @@ mod tests {
         assert_eq!(back, s);
     }
 
+    #[test]
+    fn live_input_states_are_never_saved_or_restored() {
+        let mut s = Session::default();
+        let t = s.add_track(TrackKind::Audio, ChannelFormat::Mono, None);
+        let track = s.track_mut(t).unwrap();
+        track.mixer.mute = true;
+        track.mixer.record_arm = true;
+        track.mixer.input_monitor = true;
+
+        let mut value = serde_json::to_value(&s).unwrap();
+        let mixer = value["tracks"][0]["mixer"].as_object_mut().unwrap();
+        assert!(!mixer.contains_key("record_arm"));
+        assert!(!mixer.contains_key("input_monitor"));
+
+        // Files written by older versions may still contain these keys; ignore them on input.
+        mixer.insert("record_arm".into(), serde_json::Value::Bool(true));
+        mixer.insert("input_monitor".into(), serde_json::Value::Bool(true));
+        let loaded = Session::from_json(&value.to_string()).unwrap();
+        let track = loaded.track(t).unwrap();
+        assert!(track.mixer.mute, "persistent mixer state still loads");
+        assert!(!track.mixer.record_arm);
+        assert!(!track.mixer.input_monitor);
+    }
+
     #[test]
     fn video_clip_round_trips_and_old_sessions_load() {
         let mut s = Session::default();
