diff --git a/cep-0019.md b/cep-0019.md
index a19c8c11..8059038e 100644
--- a/cep-0019.md
+++ b/cep-0019.md
@@ -10,6 +10,8 @@
| Implementation | https://github.com/conda/conda-build/pull/5277 |
+> This CEP has been superseded by CEP XXXX
+
## Abstract
Given a directory, propose an algorithm to compute the aggregated hash of its contents in a cross-platform way. This is useful to check the integrity of remote sources regardless the compression method used.
diff --git a/cep-XXXX.md b/cep-XXXX.md
new file mode 100644
index 00000000..9d8a211f
--- /dev/null
+++ b/cep-XXXX.md
@@ -0,0 +1,577 @@
+# CEP XXXX - Computing the hash of the contents in a directory (v2)
+
+
+| Title | Computing the hash of the contents in a directory (v2) |
+| Status | Draft |
+| Author(s) | pb01ka <gaganpb08singh@gmail.com>, hunger <tobias.hunger@slint.dev> |
+| Created | Jun 4, 2026 |
+| Updated | Jun 5, 2026 |
+| Discussion | https://github.com/conda/ceps/issues/150, https://github.com/conda/ceps/pull/174 |
+| Implementation | https://github.com/conda/conda-build/pull/5992 |
+| Supersedes | CEP 19 |
+
+
+> The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT",
+ "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as
+ described in [RFC2119][RFC2119] when, and only when, they appear in all capitals, as shown here.
+
+## Abstract
+
+This CEP supersedes [CEP 19](cep-0019.md) and amends the algorithm for computing the aggregated
+hash of a directory's contents in a cross-platform way. The original algorithm was susceptible to
+hash collisions when filenames contained the same byte sequences used as type markers or field
+separators. This CEP fixes that vulnerability by length-prefixing every variable-length field in the
+hash stream, making all field boundaries unambiguous.
+
+## Motivation
+
+The CEP 19 algorithm builds the hash stream for each directory entry by concatenating raw bytes in
+the order:
+
+```text
+
+```
+
+Because there is no field-length information, two structurally different directory trees can produce
+an identical byte stream whenever a filename happens to contain the same bytes used as type markers
+(`F`, `D`, `L`) or the entry separator (`-`).
+
+**Concrete example:**
+
+- **Tree 1:** a single file named `testFhello-world` with content `www`
+- **Tree 2:** a file named `test` (content `hello`) and a file named `world` (content `www`)
+
+Both trees produce the byte stream `testFhello-worldFwww-`, yielding the same digest: a true hash
+collision on structurally different directory trees.
+
+An attacker who controls filenames could exploit this to produce a directory tree whose computed
+hash matches that of a different, potentially malicious tree.
+
+## Specification
+
+Given a directory (the "scanned directory"), recursively scan all its contents (without following
+symlinks) and sort them by their path relative to the scanned directory. The scanned directory
+itself (`.`) MUST NOT be included as an entry. Paths MUST be NFC-normalized and then UTF-8-encoded
+before sorting; the sort MUST be a byte-wise comparison of the resulting UTF-8 bytes, ordering by
+unsigned byte value (0-255).
+
+The paths MUST be normalized before they are processed by the algorithm below. The following rules
+apply to both entry paths and symlink targets:
+
+- Before encoding a path to UTF-8 for sorting or hashing, implementations MUST normalize it to NFC.
+- If NFC normalization produces two entries with the same path, implementations MUST error out
+ rather than silently feeding duplicate bytes into the hasher.
+- Backslashes in the path MUST be normalized to forward slashes (e.g. `path\\to\\file`
+ becomes `path/to/file`).
+- Redundant path components MUST be removed (e.g. `path/to/../to/file` becomes `path/to/file`).
+- A leading `./` MUST be stripped (e.g. `./README.txt` becomes `README.txt`).
+- If any path component contains a null byte (`\0`), implementations MUST error out.
+- Every path MUST be UTF-8-encodable. If a path contains bytes that are not valid UTF-8,
+ implementations MUST error out rather than silently substituting or passing through the invalid
+ bytes (see [Rationale](#why-require-utf-8-encodable-paths) for a Python-specific pitfall this
+ guards against).
+
+**Entry paths** (the paths of directory entries) are additionally subject to:
+
+- Entry paths MUST be relative; a leading `/` MUST be rejected (implementations MUST error out if an
+ entry path is absolute).
+- Entry paths MUST NOT contain a drive letter (e.g. `C:`) or a UNC prefix (e.g. `//server/share`);
+ implementations MUST error out if such a path is encountered.
+
+**Symlink targets** are subject to different rules, since a symlink MAY legitimately point to an
+absolute path:
+
+- Symlink targets MAY be relative or absolute. A leading `/` is a legitimate absolute Unix path and
+ MUST be preserved, not rejected.
+- Windows drive letters (e.g. `C:`) and UNC prefixes (e.g. `//server/share`) in a symlink target
+ MUST cause the implementation to error out, because there is no meaningful cross-platform way to
+ normalize them into the hash stream.
+
+For each entry in the sorted contents, feed the following bytes into the hasher in order:
+
+1. The byte length of the UTF-8-encoded normalized relative path, written in decimal and that decimal
+ string encoded as UTF-8, followed by the UTF-8 encoded byte of `:`.
+2. The UTF-8 encoded bytes of the normalized relative path.
+3. Then, depending on the entry type:
+ - For a **regular file**:
+ - The UTF-8 encoded byte of `F`.
+ - The content bytes are determined as follows:
+ - If the file is a text file (i.e. its entire contents can be UTF-8 decoded): the file's raw
+ bytes with every `\r\n` byte sequence (`0x0D 0x0A`) replaced by `\n` (`0x0A`). The
+ substitution MUST be performed directly on the raw bytes, not via a decode-to-text-then-
+ re-encode round-trip. This is the default, normative mode; see
+ [Rationale](#why-keep-the-textbinary-distinction-and-line-ending-normalization) for an
+ optional alternative mode.
+ - If the file is binary: its raw bytes, unmodified.
+ - If the file can't be opened or read, implementations MUST error out.
+ - The byte length of those content bytes, written in decimal and encoded as UTF-8, followed by
+ the UTF-8 encoded byte of `:`, MUST be fed into the hasher immediately before the content
+ bytes themselves. Content is length-prefixed for the same reason entry paths and symlink
+ targets are: without it, content containing `-` followed by bytes that look like a valid
+ subsequent entry can reproduce the byte stream of a different, structurally distinct tree
+ (see [Rationale](#why-length-prefix-file-content-too) for a worked collision).
+ - For a **directory**: the UTF-8 encoded byte of `D`, and nothing else.
+ - For a **symlink**:
+ - The UTF-8 encoded byte of `L`.
+ - The byte length of the UTF-8-encoded symlink target path, written in decimal and that decimal
+ string encoded as UTF-8, followed by the UTF-8 encoded byte of `:`.
+ - The UTF-8 encoded bytes of the symlink target path, normalized per the common rules and the
+ **Symlink targets** rules above (the **Entry paths** rules do not apply to symlink targets).
+ - For **any other type**, implementations MUST error out.
+4. The UTF-8 encoded byte of `-`.
+
+### Summary of changes from CEP 19
+
+| Field | CEP 19 | This CEP |
+| --- | --- | --- |
+| Path | `` | `:` |
+| Symlink target | `` | `:` |
+| File content | `` | `:` |
+| Sorting | Raw Unicode code point comparison | NFC-normalized, UTF-8-encoded byte comparison |
+| Error handling | Unreadable files, unknown entry types | Additionally: null bytes, invalid/absolute entry paths, drive letters/UNC prefixes, non-UTF-8 paths, NFC-induced path collisions |
+
+Text vs. binary detection and line-ending normalization work the same way as in CEP 19: given the
+same file, they produce the same content bytes in both algorithms. Only the reference
+implementation's internal mechanics were clarified (see
+[Rationale](#why-keep-the-textbinary-distinction-and-line-ending-normalization)) - this does not
+mean the overall per-entry byte stream is unchanged, since it still includes the new length prefixes
+shown in the table above.
+
+### Reference implementation
+
+For Python 3.6+:
+
+```python
+import hashlib
+import posixpath
+import re
+import unicodedata
+from pathlib import Path
+
+_DRIVE_LETTER_RE = re.compile(r"^[A-Za-z]:")
+
+
+def _normalize_path(raw: str) -> str:
+ # Applies the common rules: reject null bytes, normalize separators, collapse
+ # redundant components (including a leading "./"), then NFC-normalize.
+ if "\0" in raw:
+ raise ValueError(f"path contains a null byte: {raw!r}")
+ raw = raw.replace("\\", "/")
+ collapsed = posixpath.normpath(raw)
+ if collapsed == ".":
+ collapsed = ""
+ return unicodedata.normalize("NFC", collapsed)
+
+
+def _encode_path(path: str) -> bytes:
+ # `path` may contain surrogate-escaped code points (PEP 383) if the on-disk name isn't
+ # valid UTF-8. Encoding with the default "strict" handler (not "surrogateescape") raises
+ # UnicodeEncodeError in that case, which is the required behavior: error out rather than
+ # silently letting non-UTF-8 bytes into the hash stream.
+ return path.encode("utf-8")
+
+
+def _check_entry_path(path: str) -> None:
+ if path.startswith("/"):
+ raise ValueError(f"entry path must be relative: {path!r}")
+ if _DRIVE_LETTER_RE.match(path):
+ raise ValueError(f"entry path must not contain a drive letter: {path!r}")
+
+
+def _check_symlink_target(target: str) -> None:
+ if target.startswith("//"):
+ raise ValueError(f"symlink target must not contain a UNC prefix: {target!r}")
+ if _DRIVE_LETTER_RE.match(target):
+ raise ValueError(f"symlink target must not contain a drive letter: {target!r}")
+
+
+def contents_hash(directory: str, algorithm: str) -> str:
+ directory = Path(directory)
+ entries = []
+ seen_paths = set()
+ for path in directory.rglob("*"):
+ rel = _normalize_path(path.relative_to(directory).as_posix())
+ _check_entry_path(rel)
+ if rel in seen_paths:
+ raise ValueError(f"NFC normalization collision on path: {rel!r}")
+ seen_paths.add(rel)
+ entries.append((_encode_path(rel), path))
+
+ hasher = hashlib.new(algorithm)
+ for rel_bytes, path in sorted(entries, key=lambda entry: entry[0]):
+ hasher.update(f"{len(rel_bytes)}:".encode("utf-8"))
+ hasher.update(rel_bytes)
+ if path.is_symlink():
+ target = _normalize_path(str(path.readlink()))
+ _check_symlink_target(target)
+ target_bytes = _encode_path(target)
+ hasher.update(b"L")
+ hasher.update(f"{len(target_bytes)}:".encode("utf-8"))
+ hasher.update(target_bytes)
+ elif path.is_dir():
+ hasher.update(b"D")
+ elif path.is_file():
+ hasher.update(b"F")
+ with open(path, "rb") as fh:
+ data = fh.read()
+ try:
+ data.decode("utf-8")
+ except UnicodeDecodeError:
+ pass # binary: hash the raw bytes unmodified
+ else:
+ data = data.replace(b"\r\n", b"\n") # text: normalize line endings
+ hasher.update(f"{len(data)}:".encode("utf-8"))
+ hasher.update(data)
+ else:
+ raise RuntimeError(f"Unknown file type: {path}")
+ hasher.update(b"-")
+ return hasher.hexdigest()
+```
+
+This implementation attempts to cover every normalization and error-handling rule from the
+[Specification](#specification) above (NFC normalization and collision detection, null bytes,
+redundant components, absolute/drive-letter/UNC rejection, non-UTF-8 paths, unreadable files); it is
+provided as a normative illustration, not a certified conformance suite, so a discrepancy against the
+Specification text should be treated as a bug in this snippet rather than in the rule it's illustrating.
+
+## Examples
+
+### Example 1: Text file and subdirectory
+
+Given the directory:
+
+```text
+mydir/
+├── README.txt (content: "Hello\n")
+└── src/
+```
+
+Entries sorted by NFC-normalized UTF-8 byte comparison: `README.txt`, `src`.
+
+**CEP 19 byte stream** (concatenated across both entries):
+
+```text
+README.txtFHello\n-srcD-
+```
+
+**This CEP byte stream:**
+
+```text
+10:README.txtF6:Hello\n-3:srcD-
+```
+
+The `10:` prefix encodes the 10-byte path `README.txt`; `6:` encodes the 6-byte content `Hello\n`;
+`3:` encodes the 3-byte path `src`. The type markers (`F`, `D`) and the entry separator (`-`) are
+unchanged and still unambiguous because both the path and content boundaries are now known exactly.
+
+### Example 2: Directory containing a symlink
+
+Given the directory:
+
+```text
+mydir/
+└── link -> ../target
+```
+
+**CEP 19 byte stream:**
+
+```text
+linkL../target-
+```
+
+**This CEP byte stream:**
+
+```text
+4:linkL9:../target-
+```
+
+The first `4:` encodes the 4-byte path `link`. After the `L` type marker, `9:` encodes the 9-byte
+symlink target `../target`.
+
+### Example 3: Dotfile sort position
+
+Given the directory:
+
+```text
+mydir/
+├── .gitignore (content: "*.pyc\n")
+├── README.txt (content: "Hello\n")
+└── src/
+```
+
+Sorting compares the UTF-8 bytes of each path. The byte `.` (`0x2E`) is lower than any ASCII
+letter, so `.gitignore` sorts before `README.txt` and `src`:
+
+```text
+.gitignore, README.txt, src
+```
+
+**This CEP byte stream:**
+
+```text
+10:.gitignoreF6:*.pyc\n-10:README.txtF6:Hello\n-3:srcD-
+```
+
+The `10:` prefix encodes the 10-byte path `.gitignore`, which is processed as an ordinary regular
+file: the leading `.` has no special meaning to the algorithm. Its `6:` content prefix encodes the
+6-byte content `*.pyc\n`.
+
+### Example 4: NFC normalization before sorting
+
+Unicode allows the same visible filename to be encoded as different code point sequences. For
+example, `é` can be a single precomposed code point (`U+00E9`) or a decomposed sequence of `e`
+(`U+0065`) followed by a combining acute accent (`U+0301`). Filesystems are not consistent about
+which form they hand back to a directory scan (e.g. HFS+ on macOS tends to decompose), so the
+algorithm normalizes every path to NFC first to get a reproducible result across platforms.
+
+Given the directory:
+
+```text
+mydir/
+├── café.txt (name stored on disk as decomposed "e" + combining acute accent, content: "espresso\n")
+└── cafe.txt (content: "drip\n")
+```
+
+**Step 1 - NFC normalize each path:**
+
+| On-disk path (code points) | NFC-normalized path (code points) |
+| --- | --- |
+| `c a f e U+0301 . t x t` (9 code points) | `c a f U+00E9 . t x t` (8 code points) |
+| `c a f e . t x t` (8 code points) | `c a f e . t x t` (unchanged) |
+
+**Step 2 - encode the normalized paths as UTF-8 bytes:**
+
+| Normalized path | UTF-8 bytes (hex) | Byte length |
+| --- | --- | --- |
+| `café.txt` | `63 61 66 c3 a9 2e 74 78 74` | 9 |
+| `cafe.txt` | `63 61 66 65 2e 74 78 74` | 8 |
+
+**Step 3 - compare the byte sequences (unsigned byte value) and sort:**
+
+The first three bytes (`63 61 66`) are equal in both paths. The fourth byte decides the order:
+`cafe.txt` has `0x65` (`e`) and `café.txt` has `0xc3` (the first byte of `é`). Since `0x65 < 0xc3`,
+`cafe.txt` sorts before `café.txt`:
+
+```text
+cafe.txt, café.txt
+```
+
+**This CEP byte stream:**
+
+```text
+8:cafe.txtF5:drip\n-9:café.txtF9:espresso\n-
+```
+
+Without the NFC normalization step, a directory tree using the decomposed form of `café.txt` would
+hash differently from an otherwise identical tree using the precomposed form, even though both
+represent the same filename and content. Normalizing to NFC before hashing removes that
+platform-dependent discrepancy.
+
+### Example 5: Collision (CEP 19 vulnerability)
+
+This example reproduces the collision from the Motivation section at the byte-stream level.
+
+**Tree 1:** one file named `testFhello-world` (16 UTF-8 bytes) with content `www`.
+
+**Tree 2:** a file named `test` (content `hello`) and a file named `world` (content `www`).
+
+| Algorithm | Tree 1 | Tree 2 | Collision? |
+| ------------- | ---------------------------- | ------------------------------- | ---------- |
+| CEP 19 | `testFhello-worldFwww-` | `testFhello-worldFwww-` | Yes |
+| This proposal | `16:testFhello-worldF3:www-` | `4:testF5:hello-5:worldF3:www-` | No |
+
+Under CEP 19 both trees yield the identical stream `testFhello-worldFwww-` and therefore the same
+digest. Under this proposal, the `16:` length prefix on Tree 1 unambiguously marks the path as 16
+bytes, so the `F` that follows is the type marker - not part of the filename. Tree 2 produces a
+completely different stream and a different digest.
+
+The step-by-step breakdown for this CEP:
+
+**Tree 1** - one file named `testFhello-world` (content `www`):
+
+| Step | Field | Bytes fed to hasher |
+| ---- | -------------------- | ------------------- |
+| 1 | path length + `:` | `16:` |
+| 2 | path | `testFhello-world` |
+| 3 | type marker | `F` |
+| 4 | content length + `:` | `3:` |
+| 5 | file content | `www` |
+| 6 | entry separator | `-` |
+
+Full stream: `16:testFhello-worldF3:www-`
+
+**Tree 2** - file `test` (content `hello`) followed by file `world` (content `www`):
+
+| Step | Field | Bytes fed to hasher |
+| ---- | ----------------------------- | ------------------- |
+| 1 | path length + `:` for `test` | `4:` |
+| 2 | path | `test` |
+| 3 | type marker | `F` |
+| 4 | content length + `:` | `5:` |
+| 5 | file content | `hello` |
+| 6 | entry separator | `-` |
+| 7 | path length + `:` for `world` | `5:` |
+| 8 | path | `world` |
+| 9 | type marker | `F` |
+| 10 | content length + `:` | `3:` |
+| 11 | file content | `www` |
+| 12 | entry separator | `-` |
+
+Full stream: `4:testF5:hello-5:worldF3:www-`
+
+The two streams are distinct, so the digests are distinct.
+
+## Rationale
+
+### Why length-prefix rather than escaping?
+
+An alternative fix would be to escape occurrences of the separator bytes inside filenames. However,
+escaping requires a second escape character, which must itself be escaped, creating additional
+complexity and potential for bugs. Length-prefixing is simpler, well-established (it is the basis
+of netstrings and many binary protocols), and adds a constant, predictable overhead of a few bytes
+per field.
+
+### Why length-prefix file content too?
+
+Type markers (`F`, `D`, `L`) and the entry separator (`-`) are single fixed bytes that are always
+present in a known position relative to the length-prefixed path, so they do not need their own
+length prefix. File content is different: it is arbitrary bytes chosen by whoever created the file,
+so it cannot be assumed to be safely bracketed by the `F` marker on one side and the `-` separator on
+the other - content can itself contain a `-` followed by bytes that look like a valid subsequent
+entry, reproducing a different tree's byte stream.
+
+For example, a single file `README.txt` containing the literal bytes `Hello\n-3:srcD` and a
+directory containing `README.txt` (content `Hello\n`) plus an empty subdirectory `src` would, without
+a content length prefix, both produce the identical stream `10:README.txtFHello\n-3:srcD-`: a hash
+collision between two structurally different trees, the same class of bug this CEP exists to fix for
+paths. Prefixing the content with its byte length, exactly as done for paths and symlink targets,
+closes this gap: the two trees instead produce `10:README.txtF13:Hello\n-3:srcD-` and
+`10:README.txtF6:Hello\n-3:srcD-`, which are no longer equal.
+
+### Why erroring out on unreadable files?
+
+The rationale is unchanged from CEP 19: we can't verify the contents of such entries, and an
+attacker could hide malicious content in those paths and later make them accessible
+(e.g. by `chmod`ing them readable).
+
+### Why keep the text/binary distinction and line-ending normalization?
+
+This was considered for removal, since it adds complexity: implementations must attempt a UTF-8
+decode of every file and rewrite `\r\n` to `\n` before hashing. It is kept because it exists to
+prevent an otherwise-identical directory checked out on Unix and on Windows from producing different
+hashes purely due to line-ending differences (CRLF vs. LF), which is a common and unwanted source of
+false hash mismatches in practice. Relying solely on `.gitattributes` (or equivalent) to pin line
+endings does not fully solve this, since some data files are intentionally meant to carry
+platform-specific line endings and would be misclassified if forced to a single style.
+
+Because normalizing line endings is a deliberate, lossy step - it makes two files that differ only
+in line endings hash identically - implementations MAY offer skipping it (i.e. hashing every file's
+raw bytes, with no text/binary distinction) as an additional, opt-in mode for cases where exact byte
+fidelity matters more than cross-platform reproducibility. Implementations that do so MUST treat the
+resulting hashes as a distinct key family from the default mode's hashes (following the same
+versioning approach described in
+[Backwards compatibility and migration](#backwards-compatibility-and-migration)), since the two modes
+are not interchangeable and silently mixing them would reintroduce the same kind of ambiguity this
+CEP otherwise eliminates.
+
+### Why single out `\0`, `/`, `\\`, and `:` in filenames?
+
+These four bytes have a well-documented history as sources of archive-extraction vulnerabilities:
+
+- `\0`: Used to truncate strings passed to C-based filesystem APIs. Path-validation code then
+ inspects a different (longer) name than the one actually written to disk.
+- `/` and `\\`: Embedded path separators inside what should be a single path component. This is the
+ classic vector for "Zip Slip"-style path traversal. It lets an entry escape the intended output
+ directory during extraction.
+- `:`: Significant on Windows. It introduces a drive letter or an NTFS Alternate Data Stream.
+
+This CEP treats null bytes and drive-letter/UNC colons as hard errors. It normalizes path separators
+explicitly (see Specification). Implementations are not left to independently decide - and
+potentially disagree - on how to handle these historically dangerous characters.
+
+### Why require UTF-8-encodable paths?
+
+On POSIX systems, filenames are arbitrary byte sequences and are not guaranteed to be valid UTF-8.
+Python (and similar language runtimes) bridges this gap by decoding raw filesystem paths with a
+`surrogateescape` error handler ([PEP 383](https://peps.python.org/pep-0383/)): any byte that isn't
+valid UTF-8 is silently mapped to a lone surrogate code point (`U+DC80`-`U+DCFF`) instead of raising
+an error. This makes it easy to obtain a `str` for any path on disk, but that `str` is not itself
+valid Unicode text, and it is easy to forget that a later encoding step needs to reject it rather
+than pass it through.
+
+If an implementation is not careful - for example, by explicitly or implicitly re-encoding with
+`errors="surrogateescape"` instead of the default strict handler - a non-UTF-8 filename can silently
+round-trip into the hash stream as an encoded surrogate, rather than causing an error. That would
+mean two different, non-UTF-8 filenames could feed distinguishable-looking but non-standard byte
+sequences into the hasher, or, depending on the implementation, be normalized in incompatible ways
+across languages and platforms, undermining the cross-platform reproducibility this CEP is meant to
+guarantee. Requiring implementations to error out on non-UTF-8 paths - i.e. to encode with a strict
+error handler, not `surrogateescape` - keeps the hash stream well-defined and prevents this class of
+implementation-specific divergence.
+
+### Backwards compatibility and migration
+
+The algorithm change produces different digests for the same directory contents, so existing stored
+hashes computed with CEP 19 are not compatible with hashes computed under this CEP. Implementations
+that currently use CEP 19 should:
+
+1. Treat the v1 (CEP 19) and v2 (this CEP) hashes as distinct key families (e.g. by using
+ different key names such as `content_sha256` vs `content_sha256_v2`, or by tagging stored
+ hashes with a version identifier).
+2. Provide an option to compute hashes using the CEP 19 algorithm for backwards compatibility with
+ existing stored hashes.
+
+The `conda-build` implementation in [conda/conda-build#5992][conda-build-5992] follows this
+approach by retaining support for the legacy algorithm behind a compatibility flag.
+
+## Backwards Compatibility
+
+This CEP changes the computed hash for any directory that contains entries whose relative path or
+(for symlinks) target path contains any of the bytes `F`, `D`, `L`, or `-`. In practice, that means
+many real-world directories will produce a different digest under this CEP than under CEP 19.
+
+Existing `content_sha256`, `content_sha384`, and `content_sha512` recipe keys continue to be
+validated using the original CEP 19 algorithm (legacy mode) but are deprecated; implementations
+SHOULD emit a deprecation warning when these keys are used. New recipes SHOULD migrate to
+`content_sha256_v2`, `content_sha384_v2`, and `content_sha512_v2`, which are validated using the
+fixed algorithm defined in this CEP.
+
+## Alternatives
+
+### Merkle trees
+
+Merkle trees provide a collision-resistant structure by design, since each node's hash incorporates
+the hashes of its children rather than raw content bytes. They were explicitly considered and
+rejected in CEP 19 for simplicity reasons. That rationale still applies here: the fix requires only
+a small, localized change to the existing algorithm, and the benefits of Merkle trees (efficient
+incremental updates, pinpointing which file changed) are not needed for this use case.
+
+### A different separator byte
+
+Choosing a separator byte that is unlikely to appear in paths (e.g. `\0`) would not by itself close
+the vulnerability. This CEP already forbids `\0` in path components (see
+[Specification](#specification)), but file content is arbitrary bytes and may legitimately contain
+`\0`, so a null-byte separator could still be defeated via file content the same way `-` can be
+today. Length-prefixing every variable-length field - paths, symlink targets, and file content, as
+this CEP already does - is the only approach that is provably collision-free regardless of what
+bytes appear in filenames or file data.
+
+## References
+
+- The hash collision bug is reported in [`conda/ceps#150`][ceps-issue-150].
+- The working fix is implemented in [`conda/conda-build#5992`][conda-build-5992].
+- CEP 19 (superseded): [`cep-0019.md`](cep-0019.md).
+- The original issue that motivated CEP 19: [`conda-build#4762`][conda-build-4762].
+- Netstrings, a well-known length-prefixing format: [cr.yp.to/proto/netstrings.txt][netstrings].
+
+## Copyright
+
+All CEPs are explicitly [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/).
+
+
+
+[RFC2119]: https://www.ietf.org/rfc/rfc2119.txt
+[ceps-issue-150]: https://github.com/conda/ceps/issues/150
+[conda-build-5992]: https://github.com/conda/conda-build/pull/5992
+[conda-build-4762]: https://github.com/conda/conda-build/issues/4762
+[netstrings]: https://cr.yp.to/proto/netstrings.txt