diff --git a/crates/buzz-media/src/validation.rs b/crates/buzz-media/src/validation.rs
index 0d03429e06..5f16beb695 100644
--- a/crates/buzz-media/src/validation.rs
+++ b/crates/buzz-media/src/validation.rs
@@ -571,6 +571,24 @@ fn validate_jpeg_metadata_free(bytes: &[u8]) -> Result<(), MediaError> {
     Err(MediaError::InvalidImage)
 }
 
+/// tEXt keywords that carry Buzz snapshot manifests (`.agent.png` /
+/// `.team.png`). These are deliberate product payloads — agent/team sharing
+/// embeds a manifest in a single tEXt chunk — so they are exempt from the
+/// metadata ban. Exactly one snapshot chunk is permitted per file; every
+/// other textual/metadata chunk remains forbidden.
+const PNG_SNAPSHOT_KEYWORDS: [&[u8]; 2] = [b"buzz_agent_snapshot", b"buzz_team_snapshot"];
+
+/// Returns true when a raw tEXt chunk payload is a Buzz snapshot manifest:
+/// the payload must start with an allowlisted keyword followed by the
+/// keyword/text NUL separator.
+fn is_snapshot_text_chunk(payload: &[u8]) -> bool {
+    PNG_SNAPSHOT_KEYWORDS.iter().any(|keyword| {
+        payload.len() > keyword.len()
+            && &payload[..keyword.len()] == *keyword
+            && payload[keyword.len()] == 0
+    })
+}
+
 fn validate_png_metadata_free(bytes: &[u8]) -> Result<(), MediaError> {
     const SIG: &[u8] = b"\x89PNG\r\n\x1a\n";
     if !bytes.starts_with(SIG) {
@@ -578,6 +596,7 @@ fn validate_png_metadata_free(bytes: &[u8]) -> Result<(), MediaError> {
     }
     let mut i = SIG.len();
     let mut saw_iend = false;
+    let mut saw_snapshot_chunk = false;
     while i < bytes.len() {
         if i + 12 > bytes.len() {
             return Err(MediaError::InvalidImage);
@@ -589,7 +608,19 @@ fn validate_png_metadata_free(bytes: &[u8]) -> Result<(), MediaError> {
             .and_then(|v| v.checked_add(len))
             .filter(|&v| v <= bytes.len())
             .ok_or(MediaError::InvalidImage)?;
-        if matches!(&kind, b"eXIf" | b"tEXt" | b"zTXt" | b"iTXt" | b"iCCP") {
+        if &kind == b"tEXt" {
+            // Buzz agent/team snapshot manifests ride in a single tEXt chunk
+            // with an allowlisted keyword. Anything else — other keywords, or
+            // a second snapshot chunk — is a forbidden metadata channel.
+            let payload = &bytes[i + 8..end - 4];
+            if saw_snapshot_chunk || !is_snapshot_text_chunk(payload) {
+                return Err(MediaError::MetadataForbidden);
+            }
+            saw_snapshot_chunk = true;
+            i = end;
+            continue;
+        }
+        if matches!(&kind, b"eXIf" | b"zTXt" | b"iTXt" | b"iCCP") {
             return Err(MediaError::MetadataForbidden);
         }
         // Unknown ancillary chunks are private metadata channels. Keep only
@@ -1230,6 +1261,67 @@ mod tests {
         ));
     }
 
+    #[test]
+    fn test_png_snapshot_text_chunks_are_allowed() {
+        // Agent/team snapshot manifests ride in an allowlisted tEXt chunk;
+        // the relay must accept exactly one such chunk per file.
+        let config = test_config();
+        for keyword in [b"buzz_agent_snapshot".as_slice(), b"buzz_team_snapshot"] {
+            let mut payload = keyword.to_vec();
+            payload.push(0);
+            payload.extend_from_slice(b"eyJmb3JtYXQiOiJidXp6In0=");
+            let mut png = TINY_PNG[..TINY_PNG.len() - 12].to_vec();
+            png.extend_from_slice(&png_chunk(b"tEXt", &payload));
+            png.extend_from_slice(&TINY_PNG[TINY_PNG.len() - 12..]);
+            assert_eq!(
+                validate_content(&png, &config).unwrap_or_else(|error| panic!(
+                    "rejected snapshot tEXt keyword {}: {error}",
+                    String::from_utf8_lossy(keyword)
+                )),
+                "image/png"
+            );
+        }
+    }
+
+    #[test]
+    fn test_png_snapshot_text_chunk_rejected_when_duplicated_or_spoofed() {
+        let config = test_config();
+
+        // Two snapshot chunks: the second is a covert channel.
+        let mut payload = b"buzz_agent_snapshot".to_vec();
+        payload.push(0);
+        payload.extend_from_slice(b"data");
+        let mut png = TINY_PNG[..TINY_PNG.len() - 12].to_vec();
+        png.extend_from_slice(&png_chunk(b"tEXt", &payload));
+        png.extend_from_slice(&png_chunk(b"tEXt", &payload));
+        png.extend_from_slice(&TINY_PNG[TINY_PNG.len() - 12..]);
+        assert!(matches!(
+            validate_content(&png, &config),
+            Err(MediaError::MetadataForbidden)
+        ));
+
+        // Keyword prefix without the NUL separator, and near-miss keywords,
+        // stay forbidden.
+        for payload in [
+            b"buzz_agent_snapshotX\0data".as_slice(),
+            b"buzz_agent_snapshot_extra\0data",
+            b"buzz_agent_snapshot", // no separator at all
+            b"Comment\0GPS=37.7,-122.4",
+        ] {
+            let mut png = TINY_PNG[..TINY_PNG.len() - 12].to_vec();
+            png.extend_from_slice(&png_chunk(b"tEXt", payload));
+            png.extend_from_slice(&TINY_PNG[TINY_PNG.len() - 12..]);
+            assert!(
+                matches!(
+                    validate_content(&png, &config),
+                    Err(MediaError::MetadataForbidden)
+                ),
+                "accepted non-snapshot tEXt payload {:?}",
+                String::from_utf8_lossy(payload)
+            );
+        }
+    }
+
     #[test]
     fn test_rejects_jpeg_app_metadata_comments_and_trailing_payload() {
         let config = test_config();
diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock
index 6af56ab4f6..810f921769 100644
--- a/desktop/src-tauri/Cargo.lock
+++ b/desktop/src-tauri/Cargo.lock
@@ -499,7 +499,11 @@ dependencies = [
  "base64 0.22.1",
  "http",
  "log",
+ "rustls",
+ "serde",
+ "serde_json",
  "url",
+ "webpki-roots 1.0.8",
 ]
 
 [[package]]
@@ -556,6 +560,23 @@ version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
 
+[[package]]
+name = "aws-creds"
+version = "0.39.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3b85155d265df828f84e53886ed9e427aed979dd8a39f5b8b2162c77e142d7"
+dependencies = [
+ "attohttpc",
+ "home",
+ "log",
+ "quick-xml 0.38.4",
+ "rust-ini",
+ "serde",
+ "thiserror 2.0.18",
+ "time",
+ "url",
+]
+
 [[package]]
 name = "aws-lc-rs"
 version = "1.17.1"
@@ -579,6 +600,15 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "aws-region"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "838b36c8dc927b6db1b6c6b8f5d05865f2213550b9e83bf92fa99ed6525472c0"
+dependencies = [
+ "thiserror 2.0.18",
+]
+
 [[package]]
 name = "axum"
 version = "0.8.9"
@@ -854,6 +884,12 @@ dependencies = [
  "piper",
 ]
 
+[[package]]
+name = "blurhash"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc"
+
 [[package]]
 name = "bon"
 version = "3.9.3"
@@ -982,6 +1018,7 @@ dependencies = [
  "base64 0.22.1",
  "buzz-agent",
  "buzz-core",
+ "buzz-media",
  "buzz-persona",
  "buzz-sdk",
  "bytes",
@@ -1051,6 +1088,36 @@ dependencies = [
  "zip 8.6.0",
 ]
 
+[[package]]
+name = "buzz-media"
+version = "0.1.0"
+dependencies = [
+ "axum",
+ "blurhash",
+ "buzz-core",
+ "bytes",
+ "chrono",
+ "futures-core",
+ "futures-util",
+ "hex",
+ "image",
+ "imagesize",
+ "infer",
+ "mp4",
+ "nostr",
+ "rust-s3",
+ "serde",
+ "serde_json",
+ "sha2 0.11.0",
+ "tempfile",
+ "thiserror 2.0.18",
+ "tokio",
+ "tokio-util",
+ "tracing",
+ "ulid",
+ "uuid",
+]
+
 [[package]]
 name = "buzz-persona"
 version = "0.1.0"
@@ -1451,6 +1518,19 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "compact_str"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
+dependencies = [
+ "castaway",
+ "cfg-if 1.0.4",
+ "itoa",
+ "ryu",
+ "static_assertions",
+]
+
 [[package]]
 name = "compact_str"
 version = "0.9.1"
@@ -2066,7 +2146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
 dependencies = [
  "data-encoding",
- "syn 1.0.109",
+ "syn 2.0.118",
 ]
 
 [[package]]
@@ -3588,6 +3668,15 @@ dependencies = [
  "digest 0.11.3",
 ]
 
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "html5ever"
 version = "0.38.0"
@@ -3693,6 +3782,7 @@ dependencies = [
  "tokio",
  "tokio-rustls",
  "tower-service",
+ "webpki-roots 1.0.8",
 ]
 
 [[package]]
@@ -3957,6 +4047,12 @@ dependencies = [
  "quick-error",
 ]
 
+[[package]]
+name = "imagesize"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c"
+
 [[package]]
 name = "indexmap"
 version = "1.9.3"
@@ -4776,6 +4872,23 @@ version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
 
+[[package]]
+name = "maybe-async"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "746873a384ad60adc5db74471dfaba74bd278afbdcfd81db93fafcdfc8b5ca0c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "md5"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c"
+
 [[package]]
 name = "mdns-sd"
 version = "0.19.2"
@@ -5252,6 +5365,15 @@ dependencies = [
  "unicase",
 ]
 
+[[package]]
+name = "minidom"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e394a0e3c7ccc2daea3dffabe82f09857b6b510cb25af87d54bf3e910ac1642d"
+dependencies = [
+ "rxml",
+]
+
 [[package]]
 name = "minimal-lexical"
 version = "0.2.1"
@@ -5388,6 +5510,20 @@ dependencies = [
  "pxfm",
 ]
 
+[[package]]
+name = "mp4"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9ef834d5ed55e494a2ae350220314dc4aacd1c43a9498b00e320e0ea352a5c3"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "num-rational",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
 [[package]]
 name = "muda"
 version = "0.19.3"
@@ -5948,6 +6084,7 @@ dependencies = [
  "num-bigint",
  "num-integer",
  "num-traits",
+ "serde",
 ]
 
 [[package]]
@@ -6941,7 +7078,7 @@ checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85"
 dependencies = [
  "base64 0.22.1",
  "indexmap 2.14.0",
- "quick-xml",
+ "quick-xml 0.41.0",
  "serde",
  "time",
 ]
@@ -7355,6 +7492,16 @@ version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
 
+[[package]]
+name = "quick-xml"
+version = "0.38.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
 [[package]]
 name = "quick-xml"
 version = "0.41.0"
@@ -7569,7 +7716,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c"
 dependencies = [
  "bitflags 2.13.0",
- "compact_str",
+ "compact_str 0.9.1",
  "critical-section",
  "hashbrown 0.17.1",
  "itertools",
@@ -7772,6 +7919,8 @@ dependencies = [
  "native-tls",
  "percent-encoding",
  "pin-project-lite",
+ "quinn",
+ "rustls",
  "rustls-pki-types",
  "serde",
  "serde_json",
@@ -7779,6 +7928,7 @@ dependencies = [
  "sync_wrapper",
  "tokio",
  "tokio-native-tls",
+ "tokio-rustls",
  "tokio-util",
  "tower",
  "tower-http",
@@ -7788,6 +7938,7 @@ dependencies = [
  "wasm-bindgen-futures",
  "wasm-streams 0.4.2",
  "web-sys",
+ "webpki-roots 1.0.8",
 ]
 
 [[package]]
@@ -8021,6 +8172,41 @@ dependencies = [
  "ordered-multimap",
 ]
 
+[[package]]
+name = "rust-s3"
+version = "0.37.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aeedb13abdaa7e48d391de05b0569b37fa0a7a64a668dff6ffb2141ad0c2527e"
+dependencies = [
+ "async-trait",
+ "aws-creds",
+ "aws-region",
+ "base64 0.22.1",
+ "bytes",
+ "cfg-if 1.0.4",
+ "futures-util",
+ "hex",
+ "hmac 0.12.1",
+ "http",
+ "log",
+ "maybe-async",
+ "md5",
+ "minidom",
+ "percent-encoding",
+ "quick-xml 0.38.4",
+ "reqwest 0.12.28",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha2 0.10.9",
+ "sysinfo 0.37.2",
+ "thiserror 2.0.18",
+ "time",
+ "tokio",
+ "tokio-stream",
+ "url",
+]
+
 [[package]]
 name = "rustc-hash"
 version = "2.1.3"
@@ -8159,6 +8345,25 @@ version = "1.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
 
+[[package]]
+name = "rxml"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc94b580d0f5a6b7a2d604e597513d3c673154b52ddeccd1d5c32360d945ee"
+dependencies = [
+ "bytes",
+ "rxml_validation",
+]
+
+[[package]]
+name = "rxml_validation"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "826e80413b9a35e9d33217b3dcac04cf95f6559d15944b93887a08be5496c4a4"
+dependencies = [
+ "compact_str 0.7.1",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.23"
@@ -9332,6 +9537,20 @@ dependencies = [
  "syn 2.0.118",
 ]
 
+[[package]]
+name = "sysinfo"
+version = "0.37.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f"
+dependencies = [
+ "libc",
+ "memchr",
+ "ntapi",
+ "objc2-core-foundation",
+ "objc2-io-kit",
+ "windows 0.61.3",
+]
+
 [[package]]
 name = "sysinfo"
 version = "0.38.4"
@@ -9903,7 +10122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
 dependencies = [
  "fastrand",
- "getrandom 0.3.4",
+ "getrandom 0.4.3",
  "once_cell",
  "rustix 1.1.4",
  "windows-sys 0.61.2",
@@ -10720,6 +10939,16 @@ dependencies = [
  "windows-sys 0.61.2",
 ]
 
+[[package]]
+name = "ulid"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe"
+dependencies = [
+ "rand 0.9.4",
+ "web-time",
+]
+
 [[package]]
 name = "unarray"
 version = "0.1.4"
@@ -12322,7 +12551,7 @@ dependencies = [
  "serde",
  "serde_json",
  "shellexpand",
- "sysinfo",
+ "sysinfo 0.38.4",
  "thiserror 2.0.18",
  "tokio",
  "tokio-util",
diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml
index dcb432b736..7826f44efd 100644
--- a/desktop/src-tauri/Cargo.toml
+++ b/desktop/src-tauri/Cargo.toml
@@ -126,3 +126,6 @@ strip-ansi-escapes = "0.2"
 # `test-util` enables tokio's paused-clock (`start_paused`) so the relay
 # admission gate tests can assert exact wait durations without real sleeps.
 tokio = { version = "1", features = ["test-util"] }
+# The relay's media validation, so the snapshot-sharing tests can prove the
+# full export → sanitize → relay-accept → import contract end to end.
+buzz_media_pkg = { package = "buzz-media", path = "../../crates/buzz-media" }
diff --git a/desktop/src-tauri/src/commands/media.rs b/desktop/src-tauri/src/commands/media.rs
index 5aaa45b4a3..11a4e6d3bc 100644
--- a/desktop/src-tauri/src/commands/media.rs
+++ b/desktop/src-tauri/src/commands/media.rs
@@ -231,6 +231,16 @@ pub(crate) fn sanitize_image_for_upload(body: Vec<u8>, mime: &str) -> Result<Vec
         return Ok(body);
     }
 
+    // Agent/team snapshot PNGs carry their manifest in a tEXt chunk that the
+    // re-encode below would destroy. Pull it out first and re-inject it after
+    // sanitizing — all other metadata is still stripped, and the relay
+    // allowlists exactly this chunk.
+    let snapshot_chunk = if format == image::ImageFormat::Png {
+        super::media_snapshot_png::extract_snapshot_text_chunk(&body)
+    } else {
+        None
+    };
+
     use image::ImageDecoder;
     let reader = image::ImageReader::with_format(std::io::Cursor::new(&body), format);
     let mut decoder = reader
@@ -249,7 +259,11 @@ pub(crate) fn sanitize_image_for_upload(body: Vec<u8>, mime: &str) -> Result<Vec
     image
         .write_to(&mut output, format)
         .map_err(|_| "failed to encode image without metadata".to_string())?;
-    Ok(output.into_inner())
+    let sanitized = output.into_inner();
+    match snapshot_chunk {
+        Some(chunk) => super::media_snapshot_png::inject_snapshot_text_chunk(sanitized, &chunk),
+        None => Ok(sanitized),
+    }
 }
 
 pub(crate) fn detect_and_validate_mime(body: &[u8]) -> Result<String, String> {
diff --git a/desktop/src-tauri/src/commands/media_snapshot_png.rs b/desktop/src-tauri/src/commands/media_snapshot_png.rs
new file mode 100644
index 0000000000..f2593ff9e0
--- /dev/null
+++ b/desktop/src-tauri/src/commands/media_snapshot_png.rs
@@ -0,0 +1,226 @@
+//! Preservation of Buzz snapshot tEXt chunks through the upload sanitizer.
+//!
+//! Split out of `media.rs` to keep that file under the desktop line-size
+//! limit. Agent/team sharing embeds a manifest in a PNG `tEXt` chunk
+//! (`buzz_agent_snapshot` / `buzz_team_snapshot`); the sanitizer's re-encode
+//! would destroy it and the relay would previously reject it. These helpers
+//! extract the chunk before the re-encode and re-inject it afterwards. The
+//! relay allowlists exactly these keywords in `buzz-media::validation` — the
+//! two lists must stay in sync.
+
+/// tEXt keywords that carry Buzz snapshot manifests (`.agent.png` /
+/// `.team.png`). These chunks are the product payload of agent/team sharing —
+/// they must survive the metadata strip.
+const PNG_SNAPSHOT_KEYWORDS: [&[u8]; 2] = [b"buzz_agent_snapshot", b"buzz_team_snapshot"];
+
+/// Extract the raw bytes of the first Buzz snapshot tEXt chunk (length + type
+/// + payload + CRC) from a PNG, or `None` when absent/not a PNG.
+///
+/// Walks the chunk structure directly instead of decoding the image so a
+/// malformed file simply yields `None` and falls through to the normal
+/// sanitize path.
+pub(crate) fn extract_snapshot_text_chunk(bytes: &[u8]) -> Option<Vec<u8>> {
+    const SIG: &[u8] = b"\x89PNG\r\n\x1a\n";
+    if !bytes.starts_with(SIG) {
+        return None;
+    }
+    let mut i = SIG.len();
+    while i + 12 <= bytes.len() {
+        let len = u32::from_be_bytes(bytes[i..i + 4].try_into().ok()?) as usize;
+        let end = i.checked_add(12)?.checked_add(len)?;
+        if end > bytes.len() {
+            return None;
+        }
+        let kind = &bytes[i + 4..i + 8];
+        if kind == b"tEXt" {
+            let payload = &bytes[i + 8..i + 8 + len];
+            let is_snapshot = PNG_SNAPSHOT_KEYWORDS.iter().any(|keyword| {
+                payload.len() > keyword.len()
+                    && &payload[..keyword.len()] == *keyword
+                    && payload[keyword.len()] == 0
+            });
+            if is_snapshot {
+                return Some(bytes[i..end].to_vec());
+            }
+        }
+        if kind == b"IEND" {
+            return None;
+        }
+        i = end;
+    }
+    None
+}
+
+/// Re-insert a raw snapshot tEXt chunk into a sanitized PNG, immediately
+/// after the IHDR chunk. Placement matters: the `png` crate decoder used by
+/// the import path only exposes text chunks encountered before IDAT via
+/// `read_info()`. The chunk bytes carry their own CRC, which remains valid
+/// because the chunk content is unchanged.
+pub(crate) fn inject_snapshot_text_chunk(png: Vec<u8>, chunk: &[u8]) -> Result<Vec<u8>, String> {
+    const SIG_LEN: usize = 8;
+    // IHDR is always the first chunk of a well-formed PNG.
+    if png.len() < SIG_LEN + 12 || &png[SIG_LEN + 4..SIG_LEN + 8] != b"IHDR" {
+        return Err("sanitized PNG is missing IHDR chunk".to_string());
+    }
+    let ihdr_len = u32::from_be_bytes(
+        png[SIG_LEN..SIG_LEN + 4]
+            .try_into()
+            .map_err(|_| "sanitized PNG has malformed IHDR length".to_string())?,
+    ) as usize;
+    let ihdr_end = SIG_LEN
+        .checked_add(12)
+        .and_then(|v| v.checked_add(ihdr_len))
+        .filter(|&v| v <= png.len())
+        .ok_or_else(|| "sanitized PNG has malformed IHDR chunk".to_string())?;
+    let mut out = Vec::with_capacity(png.len() + chunk.len());
+    out.extend_from_slice(&png[..ihdr_end]);
+    out.extend_from_slice(chunk);
+    out.extend_from_slice(&png[ihdr_end..]);
+    Ok(out)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::super::media::sanitize_image_for_upload;
+
+    #[test]
+    fn test_sanitizer_preserves_agent_snapshot_text_chunk() {
+        // Build a real 2×2 PNG carrying an agent-snapshot manifest chunk plus
+        // a mundane metadata chunk that must NOT survive.
+        for keyword in ["buzz_agent_snapshot", "buzz_team_snapshot"] {
+            let manifest = "eyJmb3JtYXQiOiJidXp6LWFnZW50LXNuYXBzaG90In0=";
+            let mut source = Vec::new();
+            {
+                let mut enc = png::Encoder::new(std::io::Cursor::new(&mut source), 2, 2);
+                enc.set_color(png::ColorType::Rgba);
+                enc.set_depth(png::BitDepth::Eight);
+                enc.add_text_chunk(keyword.to_string(), manifest.to_string())
+                    .unwrap();
+                enc.add_text_chunk("Comment".to_string(), "GPS=37.7,-122.4".to_string())
+                    .unwrap();
+                let mut writer = enc.write_header().unwrap();
+                writer.write_image_data(&[0u8; 16]).unwrap();
+            }
+
+            let sanitized = sanitize_image_for_upload(source, "image/png").unwrap();
+
+            // The snapshot manifest survives, readable by the same decoder the
+            // import path uses.
+            let decoder = png::Decoder::new(std::io::Cursor::new(&sanitized));
+            let reader = decoder.read_info().unwrap();
+            let texts = &reader.info().uncompressed_latin1_text;
+            let snapshot = texts
+                .iter()
+                .find(|c| c.keyword == keyword)
+                .unwrap_or_else(|| panic!("sanitized PNG lost the {keyword} tEXt chunk"));
+            assert_eq!(snapshot.text, manifest);
+
+            // The mundane metadata chunk is stripped.
+            assert!(
+                !texts.iter().any(|c| c.keyword == "Comment"),
+                "sanitizer kept a non-snapshot tEXt chunk"
+            );
+        }
+    }
+
+    #[test]
+    fn test_sanitizer_still_strips_all_text_from_regular_pngs() {
+        let mut source = Vec::new();
+        {
+            let mut enc = png::Encoder::new(std::io::Cursor::new(&mut source), 2, 2);
+            enc.set_color(png::ColorType::Rgba);
+            enc.set_depth(png::BitDepth::Eight);
+            enc.add_text_chunk("Comment".to_string(), "GPS=37.7,-122.4".to_string())
+                .unwrap();
+            let mut writer = enc.write_header().unwrap();
+            writer.write_image_data(&[0u8; 16]).unwrap();
+        }
+
+        let sanitized = sanitize_image_for_upload(source, "image/png").unwrap();
+        let decoder = png::Decoder::new(std::io::Cursor::new(&sanitized));
+        let reader = decoder.read_info().unwrap();
+        assert!(reader.info().uncompressed_latin1_text.is_empty());
+    }
+
+    #[test]
+    fn test_agent_snapshot_survives_full_share_pipeline() {
+        // Cross-contract regression for the production failure: a real
+        // encoded .agent.png must survive export → client sanitize →
+        // relay validation → import decode. Each seam is also covered by
+        // unit tests, but this proves the exact pipeline that broke.
+        use crate::managed_agents::agent_snapshot::{
+            decode_snapshot_png, encode_snapshot_png, AgentSnapshot, AgentSnapshotDefinition,
+            AgentSnapshotMemory, AgentSnapshotProfile, MemoryLevel,
+        };
+
+        let snapshot = AgentSnapshot {
+            format: "buzz-agent-snapshot".to_string(),
+            version: 1,
+            definition: AgentSnapshotDefinition {
+                name: "Tree Trunks".to_string(),
+                system_prompt: Some("You are a helpful agent.".to_string()),
+                runtime: Some("goose".to_string()),
+                model: None,
+                provider: None,
+                parallelism: Some(1),
+                respond_to: None,
+                respond_to_allowlist: vec![],
+                idle_timeout_seconds: None,
+                max_turn_duration_seconds: None,
+                name_pool: vec![],
+            },
+            profile: AgentSnapshotProfile {
+                display_name: "Tree Trunks".to_string(),
+                about: Some("Shared agent".to_string()),
+                avatar_data_url: None,
+                avatar_url: None,
+            },
+            memory: AgentSnapshotMemory {
+                level: MemoryLevel::None,
+                entries: vec![],
+            },
+        };
+
+        // Export with a real avatar body (what the share flow uploads).
+        let avatar = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(
+            4,
+            4,
+            image::Rgba([200, 120, 40, 255]),
+        ));
+        let mut avatar_png = std::io::Cursor::new(Vec::new());
+        avatar
+            .write_to(&mut avatar_png, image::ImageFormat::Png)
+            .unwrap();
+        let exported = encode_snapshot_png(&snapshot, Some(avatar_png.get_ref())).unwrap();
+
+        // Client upload path.
+        let sanitized = sanitize_image_for_upload(exported, "image/png").unwrap();
+
+        // Relay ingest path.
+        let relay_config = buzz_media_pkg::MediaConfig {
+            s3_endpoint: String::new(),
+            s3_access_key: String::new(),
+            s3_secret_key: String::new(),
+            s3_bucket: String::new(),
+            s3_region: "us-east-1".to_string(),
+            max_image_bytes: 50 * 1024 * 1024,
+            max_gif_bytes: 10 * 1024 * 1024,
+            max_video_bytes: 524_288_000,
+            max_file_bytes: 104_857_600,
+            public_base_url: String::new(),
+            upload_records_enabled: false,
+            upload_ip_header: None,
+            upload_port_header: None,
+        };
+        assert_eq!(
+            buzz_media_pkg::validation::validate_content(&sanitized, &relay_config)
+                .expect("relay rejected a sanitized agent snapshot PNG"),
+            "image/png"
+        );
+
+        // Recipient import path.
+        let imported = decode_snapshot_png(&sanitized)
+            .expect("import failed on a PNG that passed sanitize + relay validation");
+        assert_eq!(imported, snapshot);
+    }
+}
diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs
index cf4cdca91d..0374a7d392 100644
--- a/desktop/src-tauri/src/commands/mod.rs
+++ b/desktop/src-tauri/src/commands/mod.rs
@@ -25,6 +25,7 @@ mod link_preview;
 pub(crate) mod media;
 mod media_download;
 mod media_gif;
+mod media_snapshot_png;
 mod media_transcode;
 #[cfg(feature = "mesh-llm")]
 pub(crate) mod mesh_llm;
