diff --git a/crates/engine/src/cmd/audiosuite.rs b/crates/engine/src/cmd/audiosuite.rs
--- a/crates/engine/src/cmd/audiosuite.rs
+++ b/crates/engine/src/cmd/audiosuite.rs
@@ -137,6 +137,7 @@ fn process(e: &mut Engine, p: &Value) -> Result<Value> {
         nc.name = new_name;
         nc.content = ClipContent::Audio { source: src, offset: 0 };
         nc.length = i64::try_from(frames).unwrap_or(nc.length);
+        nc.clamp_fades();
         // The render already has the clip's stretch applied; keeping it would stretch the new file again.
         nc.stretch = 1.0;
         // Invert and Duplicate are phase/copy operations. The clip's gain stays a clip gain,
@@ -246,6 +247,21 @@ mod tests {
         assert!((sample - 0.5).abs() < 1e-3, "duplicate keeps the file level and the clip gain, got {sample}");
     }
 
+    #[test]
+    fn shortening_process_clamps_fades_to_the_new_length() {
+        let (mut e, id) = gained_clip();
+        {
+            let c = e.session_mut().find_clip_mut(id).unwrap();
+            c.fade_in.len = 60;
+            c.fade_out.len = 10;
+        }
+        e.execute("audiosuite.process", &json!({"process": "time_stretch", "params": {"ratio": 0.25}, "clips": [id.0]})).unwrap();
+        let c = e.session().tracks[0].clips()[0].clone();
+        assert!(c.length < 64, "{}", c.length);
+        assert!(c.fade_in.len <= c.length, "fade in {} > length {}", c.fade_in.len, c.length);
+        assert_eq!(c.fade_out.len, 10.min(c.length));
+    }
+
     #[test]
     fn processed_clip_does_not_keep_the_stretch_it_baked() {
         for stretch in [0.5, 2.0] {
