diff --git a/crates/dwg/src/lib.rs b/crates/dwg/src/lib.rs
--- a/crates/dwg/src/lib.rs
+++ b/crates/dwg/src/lib.rs
@@ -113,8 +113,11 @@ mod native {
             notes.push(format!("DWG: {orphans} DGN line style component{s} that no line style definition uses {were} left out"));
         }
         let mut out = CappedWriter { buf: Vec::new(), limit: limits.max_dxf_bytes, exceeded: false };
-        let written = std::panic::catch_unwind(AssertUnwindSafe(|| acadrust::DxfWriter::new(&doc).write_to_writer(&mut out)))
-            .map_err(|_| "the DWG→DXF conversion failed on this file".to_string())?;
+        let written = std::panic::catch_unwind(AssertUnwindSafe(|| {
+            acadrust::DxfWriter::new(&doc).write_to_writer(&mut out)?;
+            mtext_contents(&doc, &mut out)
+        }))
+        .map_err(|_| "the DWG→DXF conversion failed on this file".to_string())?;
         if out.exceeded {
             return Err(format!(
                 "DWG: converting this {} file produced more than {} of drawing data (the limit); \
@@ -127,6 +130,94 @@ mod native {
         Ok((add_header_vars(out.buf, &doc.header), notes))
     }
 
+    /// acadrust 0.6.6's ASCII writer treats a caret followed by a literal space as already
+    /// escaped, and turns line feeds into paragraph codes. Serialize affected MTEXT contents
+    /// from the native values instead, using its lossless string writer. All other groups,
+    /// including embedded-object data, remain byte-for-byte as the DXF writer emitted them.
+    fn mtext_contents(doc: &acadrust::CadDocument, out: &mut CappedWriter) -> acadrust::Result<()> {
+        use acadrust::io::dxf::{DxfStreamWriter, DxfTextWriter};
+        let contents: std::collections::HashMap<_, _> = doc
+            .entities()
+            .filter_map(|e| match e {
+                acadrust::EntityType::MText(t) if t.value.contains(['^', '\t', '\n']) => Some((t.common.handle.value(), t.value.as_str())),
+                _ => None,
+            })
+            .collect();
+        if contents.is_empty() {
+            return Ok(());
+        }
+        let dxf = std::mem::take(&mut out.buf);
+        let mut lines = dxf.split_inclusive(|b| *b == b'\n');
+        let (mut mtext, mut text, mut written) = (false, None, false);
+        while let Some(code) = lines.next() {
+            let value = lines.next().ok_or_else(|| std::io::Error::other("incomplete DWG bridge DXF pair"))?;
+            match code.trim_ascii() {
+                b"0" => {
+                    mtext = value.trim_ascii() == b"MTEXT";
+                    text = None;
+                    written = false;
+                }
+                b"5" if mtext => {
+                    text = std::str::from_utf8(value.trim_ascii()).ok().and_then(|s| u64::from_str_radix(s, 16).ok()).and_then(|h| contents.get(&h));
+                }
+                b"101" => mtext = false,
+                b"1" | b"3" if mtext && text.is_some() => {
+                    if let Some(&text) = text
+                        && !written
+                    {
+                        let mut writer = DxfTextWriter::new(&mut *out);
+                        if doc.version < acadrust::DxfVersion::AC1021
+                            && let Some(encoding) = acadrust::io::dxf::code_page::encoding_from_code_page(&doc.header.code_page)
+                        {
+                            writer.set_encoding(encoding);
+                        }
+                        // Each character can expand to two caret-escape characters. Keep
+                        // those pairs together and each group within 250 characters.
+                        let mut rest = text;
+                        while let Some((end, _)) = rest.char_indices().nth(125) {
+                            let (chunk, tail) = rest.split_at(end);
+                            writer.write_xrecord_string(3, chunk)?;
+                            rest = tail;
+                        }
+                        writer.write_xrecord_string(1, rest)?;
+                        written = true;
+                    }
+                    continue;
+                }
+                _ => {}
+            }
+            out.write_all(code)?;
+            out.write_all(value)?;
+        }
+        Ok(())
+    }
+
+    #[cfg(test)]
+    #[test]
+    fn mtext_contents_keeps_other_groups_and_the_output_limit() -> acadrust::Result<()> {
+        for version in [acadrust::DxfVersion::AC1015, acadrust::DxfVersion::AC1032] {
+            let mut doc = acadrust::CadDocument::new();
+            doc.version = version;
+            doc.header.code_page = "ANSI_1252".into();
+            let mut t = acadrust::MText::with_value("é ^ C", acadrust::Vector3::ZERO);
+            t.common.handle = acadrust::Handle::new(0x10);
+            doc.add_entity(acadrust::EntityType::MText(Box::new(t)))?;
+            // Only the MTEXT's own 1/3 groups change. The embedded object's groups and the
+            // following TEXT record must survive byte-for-byte, including their spaces.
+            let prefix = b"  0\r\nMTEXT\r\n  5\r\n10\r\n  8\r\n0\r\n";
+            let suffix = b"101\r\nEmbedded Object\r\n  1\r\n^ embedded\r\n  3\r\n^ tail\r\n  0\r\nTEXT\r\n  1\r\n^ plain\r\n";
+            let raw = [prefix.as_slice(), b"  3\r\nold\r\n  1\r\ntext\r\n", suffix.as_slice()].concat();
+            let mut out = CappedWriter { buf: raw.clone(), limit: 4096, exceeded: false };
+            mtext_contents(&doc, &mut out)?;
+            let value: &[u8] = if version == acadrust::DxfVersion::AC1015 { b"\xe9 ^  C" } else { "é ^  C".as_bytes() };
+            assert_eq!(out.buf, [prefix.as_slice(), b"  1\r\n", value, b"\r\n", suffix.as_slice()].concat());
+            let mut limited = CappedWriter { buf: raw, limit: 1, exceeded: false };
+            assert!(mtext_contents(&doc, &mut limited).is_err());
+            assert!(limited.exceeded);
+        }
+        Ok(())
+    }
+
     /// Most record types named in the skipped-records note.
     const NOTE_TYPES: usize = 6;
 
diff --git a/crates/io/tests/it/main.rs b/crates/io/tests/it/main.rs
--- a/crates/io/tests/it/main.rs
+++ b/crates/io/tests/it/main.rs
@@ -38,6 +38,7 @@ mod mleader_roundtrip;
 mod mline_roundtrip;
 #[cfg(not(target_arch = "wasm32"))]
 mod model_layout_plot_type;
+mod mtext_caret_spaces;
 mod mtext_columns_roundtrip;
 mod mtext_embedded_object;
 mod mtext_line_spacing;
diff --git a/crates/io/tests/it/mtext_caret_spaces.rs b/crates/io/tests/it/mtext_caret_spaces.rs
new file mode 100644
--- /dev/null
+++ b/crates/io/tests/it/mtext_caret_spaces.rs
@@ -0,0 +1,118 @@
+//! Literal MTEXT carets and their adjacent whitespace survive DXF and DWG saves (#2250).
+
+use cadcraft_doc::{Common, Drawing, EntityKind, Handle, MText, Space};
+use cadcraft_geom::Vec3;
+use cadcraft_io::{SaveOptions, Version, read, write_with};
+
+type TestResult = Result<(), Box<dyn std::error::Error>>;
+
+fn drawing() -> Result<(Drawing, Vec<(Handle, String)>), Box<dyn std::error::Error>> {
+    let mut d = Drawing::new_metric();
+    let mut expected = Vec::new();
+    let mut cases: Vec<String> = [
+        "ordinary text",
+        "^ C",
+        "A ^ C B",
+        "^  C",
+        "A ^   C  B",
+        "^ ",
+        "^^ C ^ ^  D^",
+        "^I ^J ^ C",
+        "\t^ C\nD\tE",
+        "^\t ^\n C",
+        r"A\P^ C\P^I ^J",
+        r"\S+0.1^-0.1; ^ C",
+    ]
+    .into_iter()
+    .map(str::to_string)
+    .collect();
+    // Exercise multiple group-3 chunks and a caret at the output chunk boundary.
+    cases.push(format!("{}^ C", "A".repeat(124)));
+    cases.push(format!("{}^ C B", "A".repeat(260)));
+    for contents in cases {
+        let kind = EntityKind::MText(MText {
+            insert: Vec3::new(1.0, 2.0, 0.0),
+            height: 0.5,
+            width: 12.5,
+            attach: 1,
+            rotation: 0.0,
+            style: "Standard".into(),
+            contents: contents.clone(),
+            line_spacing: 1.0,
+            line_spacing_exact: false,
+            columns: None,
+        });
+        let handle = d.add(&Space::Model, Common::default(), kind)?;
+        expected.push((handle, contents));
+    }
+    Ok((d, expected))
+}
+
+fn assert_contents(d: &Drawing, expected: &[(Handle, String)], context: &str) -> TestResult {
+    assert_eq!(d.model.len(), expected.len(), "{context}: entity count");
+    for (handle, contents) in expected {
+        let e = d.entity(*handle).ok_or("MTEXT handle lost")?;
+        let EntityKind::MText(t) = &e.kind else { return Err("MTEXT entity type changed".into()) };
+        assert_eq!(&t.contents, contents, "{context}: {handle:?}");
+    }
+    Ok(())
+}
+
+#[test]
+fn dxf_mtext_caret_spaces_round_trip() -> TestResult {
+    let (original, expected) = drawing()?;
+    for version in Version::ALL {
+        for binary in [false, true] {
+            let mut d = original.clone();
+            for pass in 1..=2 {
+                let bytes = write_with(&d, "carets.dxf", &SaveOptions { version: Some(version), binary })?;
+                d = read(&bytes, "carets.dxf")?;
+                assert_contents(&d, &expected, &format!("{version:?}, binary={binary}, pass={pass}"))?;
+            }
+        }
+    }
+    Ok(())
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+#[test]
+fn dwg_mtext_caret_spaces_round_trip() -> TestResult {
+    let (original, expected) = drawing()?;
+    for version in Version::ALL {
+        let mut d = original.clone();
+        for pass in 1..=2 {
+            let bytes = write_with(&d, "carets.dwg", &SaveOptions { version: Some(version), binary: false })?;
+            assert_eq!(cadcraft_dwg::version(&bytes).as_deref(), Some(version.code()));
+            // Inspect the stored DWG text before its DXF rendition: a compensating reader
+            // must not hide corruption on save. This uses the same codec, not an external oracle.
+            let native = acadrust::DwgReader::from_stream(std::io::Cursor::new(&bytes)).read()?;
+            for (handle, contents) in &expected {
+                let text = native.entities().find_map(|e| match e {
+                    acadrust::EntityType::MText(t) if t.common.handle.value() == handle.0 => Some(&t.value),
+                    _ => None,
+                });
+                assert_eq!(text, Some(contents), "native DWG {version:?}, pass={pass}, {handle:?}");
+            }
+            let dxf = cadcraft_dwg::dwg_to_dxf(&bytes)?;
+            let converted = read(&dxf, "bridge.dxf")?;
+            assert_contents(&converted, &expected, &format!("DWG bridge DXF {version:?}, pass={pass}"))?;
+            d = read(&bytes, "carets.dwg")?;
+            assert_contents(&d, &expected, &format!("DWG {version:?}, pass={pass}"))?;
+        }
+    }
+    Ok(())
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+#[test]
+fn bridge_caret_space_is_escaped_once_in_each_direction() -> TestResult {
+    let (d, _) = drawing()?;
+    let dxf = cadcraft_io::write_dxf(&d);
+    assert!(dxf.contains("  1\r\n^  C\r\n"), "literal caret plus authored space needs two wire spaces");
+    let native = acadrust::DxfReader::from_reader(std::io::Cursor::new(dxf.into_bytes()))?.read()?;
+    assert!(native.entities().any(|e| matches!(e, acadrust::EntityType::MText(t) if t.value == "^ C")));
+    let dwg = write_with(&d, "carets.dwg", &SaveOptions { version: Some(Version::R2018), binary: false })?;
+    let returned = String::from_utf8(cadcraft_dwg::dwg_to_dxf(&dwg)?)?;
+    assert!(returned.contains("  1\r\n^  C\r\n"), "the return bridge must encode the separator independently of authored whitespace");
+    Ok(())
+}
