✓ Verified, November 11th, 2026Download the .zd file

Zipdown, specification version 0.5

Independent Submission                                        (TBD)
Request for Comments: XXXX                            November 2026
Category: Experimental

     Zipdown: Markdown with Embedded Assets (specification version 0.5)

Status of This Memo

Version 0.5 of this specification, written November 11th, 2026. It is not yet stable.

The specification and the file format are numbered separately, and files carry only the format's number. Every version of this specification before 2 describes format version 1, so files say zd-version: 1 until version 2 of the specification is published. Until then the format may still evolve: a file written to one version of this specification may not read the same under a later one.

Anyone may implement this specification, in any software, free or paid, without asking and without paying.

This is the site version, jokes included; the version submitted as an Internet-Draft removes the Acknowledgments jokes. Comments are, as tradition demands, requested: write to what this document hopes to become, plural, at treeflow.ai.

This document is itself a Zipdown file. Its diagrams travel inside it as assets, and so does the worked example from Section 24, so a reader can extract assets/example.zd and check it byte for byte against the text. The ASCII diagrams in the text remain the reference; the images are illustrations.

Abstract

Zipdown is a single-file text format that carries a Markdown document together with the assets it needs. The header is valid frontmatter, the body is ordinary Markdown, and the assets follow as base64 blocks inside an HTML comment, one block per asset, with a visible index and a short two-language note at the end. A plain CommonMark renderer shows the document, one visible marker line, the index, and the note. Every plain Markdown file is a valid Zipdown file with zero assets, and reads as Zipdown Markdown unless it says otherwise.

Table of Contents

  1. Conventions and Terminology
  2. Format Overview
  3. Character Encoding and Line Endings
  4. Header
  5. Files With No Assets
  6. Body
  7. Zipdown Markdown (zd1)
  8. Separator
  9. Boundary Marker
  10. Delimiters and the Comment
  11. Payload
  12. Index
  13. Epilogue
  14. Pointer
  15. Terminator
  16. Signatures
  17. Reading a File
  18. Appearance in Plain Renderers
  19. Extracting an Asset by Hand
  20. Security Considerations
  21. Media Type, Extension, and Site
  22. Implementations
  23. Future Work
  24. Example
  25. Open Issues
  26. References

Appendix A. Notes for Viewers and Editors

1. Conventions and Terminology

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 BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

This memo defines the bytes and what they mean. What readers, servers, and editors do with a file, including files that break the rules, is their business, except where Section 20 says otherwise.

2. Format Overview

 +-------------------------------------------------------+
 | ---                                                   |  header
 | zd-version: 1                                         |  (valid frontmatter)
 | zd-markdown-length-octets: 4821                       |
 | zd-markdown-sha256: <64 hex>                          |
 | zd-boundary: <65-char marker>                         |
 | ---                                                   |
 +-------------------------------------------------------+  <- header end
 | # Title                                               |  body
 | ... ![dot](assets/dot.png){width=10%} ...             |  (MLO octets)
 |                                                       |  ends with a blank line
 +-------------------------------------------------------+
 | ***                                                   |  separator (visible)
 | -----END OF DOCUMENT-----                             |
 | ***                                                   |
 +-------------------------------------------------------+
 | --<marker>                                            |  opening delimiter (visible)
 | <!--                                                  |  comment opens
 | <asset 1: base64, 64-char lines>                      |
 |                                                       |  blank line
 | <asset 2: base64, 64-char lines>                      |
 | --<marker>--                                          |  closing delimiter
 | --><br/>                                              |  comment closes
 | ```zipdown-index                                      |  index (visible)
 | path  size  lines(1-based,inclusive)  base64-sha256  sha256
 | assets/dot.png     70  14-15  <64 hex>  <64 hex>      |
 | assets/chart.svg  812  17-30  <64 hex>  <64 hex>      |
 | ```                                                   |
 | Psst, this is a Zipdown file! ... \                   |  epilogue (2 lines)
 | (the same, in Chinese)                                |
 | <!-- zd-index 5120 00000000000000000000000000001400 -->  pointer (hidden)
 | .                                                     |  terminator (visible)
 +-------------------------------------------------------+

Format overview

3. Character Encoding and Line Endings

4. Header

4.1. Must Be Valid Frontmatter

The header MUST be valid frontmatter in exactly one of these forms:

Readers MUST parse the header as plain data. In particular, a YAML reader MUST NOT honor tags that construct objects or run code; a "safe load" that yields only strings, numbers, booleans, lists, and maps is what is meant.

4.2. Zipdown Keys

Key Type Meaning
zd-version integer REQUIRED. Its presence marks the file as Zipdown. This memo defines version 1.
zd-markdown-length-octets integer OPTIONAL hint. Length of the body in octets.
zd-markdown-sha256 string REQUIRED. SHA-256 of the body, 64 lowercase hex digits.
zd-boundary string REQUIRED. The bare 65-character marker.
zd-payload-encoding string OPTIONAL. Default base64. In version 1 the only legal value is base64. binary is reserved.
zd-markdown-flavor string OPTIONAL. The body's Markdown: zd1, or an identifier from the IANA Markdown Variants registry. Default: zd1 (Section 6.2).
zd-signature string OPTIONAL. An Ed25519 signature over the header, the index, and the epilogue (Section 16).
zd-date string OPTIONAL. When the file was written, in ISO 8601 with nanoseconds, such as 2026-11-11T13:04:00.123456789-08:00. Conforming writers include the time zone; a date without one is local time.

(The table above is a GFM table. In a plain CommonMark renderer it shows as rows of text separated by vertical bars, which is still readable.)

4.3. Other Keys

4.4. MLO

MLO is the number of octets in the body, from the header end to the first octet of the separator:

offset of separator               = header end + MLO
offset of opening delimiter line  = header end + MLO + 34

34 is the separator's fixed length in octets (Section 8).

4.5. The zd- Line Grammar

Libraries for YAML, TOML, and JSON disagree in ways that matter: escaped key spellings, duplicate keys, and YAML 1.1 reading 0153 as an octal number. So every zd- entry has exactly one spelling in each header form, and readers find zd- entries by that spelling, never through a library.

YAML   zd-<name>: <value>
TOML   zd-<name> = "<value>"        or   zd-<name> = <integer>
JSON   "zd-<name>": "<value>"       or   "zd-<name>": <integer>

5. Files With No Assets

A file with no assets needs no header, no separator, no delimiters, no comment, no index, no epilogue, no pointer, and no terminator. It is plain Markdown. It MAY be named .md or .zd.

Either way, its body is zd1 (Section 6.2). If it starts with an ordinary frontmatter block (one with no zd-version) that holds a zd-markdown-flavor line, that flavor applies instead. A frontmatter block is never part of the body, so viewers do not show it as text.

Everything below applies to files with one or more assets.

6. Body

6.1. Rules

6.2. Which Markdown

zd-markdown-flavor names the body's Markdown. A file does not have to use Zipdown's own Markdown to use the Zipdown container.

7. Zipdown Markdown (zd1)

This section applies only when the flavor is zd1. Renderers decide how everything here looks: the actual sizes, colors, grays, and indent widths. A renderer that does not support a feature shows its text as written. Nothing in this section applies inside code spans, code blocks, link destinations, or autolinks.

7.1. Inline Marks and the Edge Rule

Size, highlight, and spoilers wrap text in a pair of marks. A mark counts only at an edge:

Ordinary text stays plain, because its dashes and pluses sit inside words or next to spaces:

self-evident   10-20   1+1+1   C++   y-axis   this — that   a == b

The rule is not perfect. ++i and C++ reads as XL text, because both ends sit at an edge. A backslash fixes it: \++i and C++.

7.2. Text Size

Five steps. M is normal text and has no marks.

Step Inline Line
XL ++text++ +## text
L +text+ +# text
M (no marks) (no marker)
S -text- -# text
XS --text-- or —text— -## text

7.3. Text Color and Grays

A color name in parentheses, directly after the closing mark of a styled span, colors that span:

the build is **failing**(red), the docs are *passing*(green)
~~old price~~(g50), `exit 1`(red), ++big news++(blue)

A plain Markdown viewer shows the styled span followed by the name: the word "failing" in bold, then "(red)", which still says what was meant.

7.4. Highlight

==text== highlights text, like a marker pen. The marks follow the edge rule (Section 7.1). A file cannot pick the highlight color. Each renderer picks its own, and a renderer may show no highlight at all.

7.5. Spoilers

||text|| hides text until the reader reveals it, as in Discord and Telegram. The marks follow the edge rule (Section 7.1). Inside a table cell, the pipes are escaped: \|\|text\|\|.

7.6. Mentions

A mention is @ followed by a name, and optionally / and a platform code, as in @qrux or @qrux/gh.

7.7. Emoji Shortcodes

A name between colons, such as :tada:, stands for an emoji. There is no standard list: Discord, Slack, and GitHub each keep their own. Zipdown keeps its own list on the site (Section 21). An unknown name shows as written.

7.8. Indent

> keeps its CommonMark and email meaning: a quote, with depth by repetition as in RFC 3676 [RFC3676], so >> is a quote inside a quote. Indent has its own markers.

Block indent. )) followed by a space indents one level, ))) two, and so on:

)) This paragraph is indented one level.
It continues here without a marker.

))) This one is indented two levels.

Region indent. A line holding only ))> indents everything after it, across blank lines, until a line holding only <((:

))>
First indented paragraph.

Second indented paragraph.
<((

7.9. Tables

Tables are GFM pipe tables [GFM], with column alignment set by colons in the second row (:--- left, ---: right, :---: center), plus:

|        |       |       |
|--------|-------|-------|
| North  | 10    | 12    |
| South  | 11    | 13    |

Table: Sales by region

In a plain GFM viewer, the empty header shows as a blank row, and the caption shows as an ordinary line of text.

7.10. Strikethrough, Task Lists, Plain Links, and Footnotes

As on GitHub [GFM]:

GFM's tag filter is not part of zd1: zd1 shows no raw HTML at all (Section 7.12).

7.11. Image Size and Crop

An image may be followed directly by attributes in braces, in the style of Pandoc, Quarto, and kramdown:

![chart](assets/chart.svg){width=50%}
![portrait](assets/face.jpg){width=100% height=20lh position=top}
![banner](assets/wide.pdf#page=2){width=100% height=8lh position="50% 0%"}

In a plain renderer the braces show as text next to the image. The image is broken there anyway, because the asset lives inside the payload, so the leftover costs little.

7.12. HTML

zd1 shows no HTML. Raw HTML in the body is never rendered, and there is no allowlist.

Flavors other than zd1 keep their own rules for raw HTML, narrowed by Section 20.

8. Separator

Directly after the body come exactly these three lines, 34 octets in all:

***
-----END OF DOCUMENT-----
***

9. Boundary Marker

marker = hex( R XOR H[0..191] )  "-"  hex( H[192..255] )     65 characters
         \______ 48 hex ______/       \____ 16 hex _____/

H = SHA-256 of the body (256 bits)
R = 192 bits from a cryptographically secure random source

Marker construction

The last 16 hex digits of the marker equal the last 16 hex digits of zd-markdown-sha256, so the two can be checked by eye. If the marker appears in the body, the writer replaces R with the first 192 bits of SHA-256(R) and tries again. The marker MUST NOT contain -->; one built this way never does. The marker fits MIME's 70-character boundary limit.

10. Delimiters and the Comment

After the separator, in order:

--<marker>LF          opening delimiter line (visible, on purpose)
<!--LF                comment opens
<payload>             asset blocks (Section 11)
--<marker>--LF        closing delimiter line
--><br/>LF            comment closes

11. Payload

11.1. Asset Blocks

With blank lines between blocks, vip in vi selects one asset's whole block.

11.2. Paths

11.3. Linking

11.4. Image Formats and Alternates

12. Index

Directly after the comment-close line, with nothing in between, comes a fenced code block with the info string zipdown-index, one row per asset. A file has exactly one index:

```zipdown-index
path  size  lines(1-based,inclusive)  base64-sha256  sha256
assets/dot.png      70  14-15  <b64-sha256>  <sha256>
assets/chart.svg   812  17-30  <b64-sha256>  <sha256>
assets/empty.txt     0  -      e3b0c442...b855  e3b0c442...b855
```

The first row is mandatory and is exactly path size lines(1-based,inclusive) base64-sha256 sha256, its words separated by spaces. It makes every index self-describing.

Columns are separated by one or more ASCII spaces (0x20), never other whitespace, and aligned by the writer. Each row has exactly five columns:

  1. Path. Paths never contain spaces, so splitting on whitespace is safe.
  2. Size of the decoded asset, in octets, in decimal, with no leading zeros.
  3. Line range of the asset's block, first-last, 1-based and inclusive, counted from the first line of the file, as sed, awk, and vi count. A zero-byte asset shows -.
  4. Base64 SHA-256: the hash of the asset's block exactly as it sits in the file, every line with its LF, 64 lowercase hex digits. A reader can hash the bytes in the line range directly, without decoding.
  5. Decoded SHA-256: the hash of the decoded asset, 64 lowercase hex digits.

Both hashes are full length. For a zero-byte asset both are the SHA-256 of nothing.

  line 12  --<marker>
  line 13  <!--
  line 14  iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4  \  base64-sha256
  line 15  2mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==                  /  covers these lines
  line 16                                                                with their LFs
  line 17  PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZp  \  next asset's
  ...                                                                /  base64-sha256
  line 30  ...
  line 31  --<marker>--

  sha256 (decoded) = SHA-256( base64-decode( lines 14-15 ) )

Hash ranges

The index MUST be internally consistent:

A reader can check all three without decoding anything, so a lying index is caught before memory is allocated for an asset. Readers MUST compare a size against its range's length in octets before doing any arithmetic with it, so a huge size cannot overflow the check.

The index is visible in plain renderers, in monospace, as a list of what the file carries. Fenced code blocks are core CommonMark.

13. Epilogue

After the index comes the epilogue: the English line, then zero or more translation lines, with no prefixes. The English line is:

Psst, this is a Zipdown file! There are images and files tucked inside that a plain Markdown viewer can't show you. Grab a Zipdown reader at zd.treeflow.ai to see everything.

The reference writer adds one translation, in Chinese, so its epilogue reads:

Psst, this is a Zipdown file! There are images and files tucked inside that a plain Markdown viewer can't show you. Grab a Zipdown reader at zd.treeflow.ai to see everything. \
嘿,这是一个 Zipdown 文件!里面藏着普通 Markdown 查看器显示不了的图片和文件。到 zd.treeflow.ai 下载 Zipdown 阅读器,就能看到全部内容。

14. Pointer

The line just before the terminator is:

<!-- zd-index <decimal> <32 lowercase hex digits> -->LF

15. Terminator

The last line of the file is a single period:

.LF

16. Signatures

A file MAY carry one signature, in its header:

zd-signature: ed25519 <key-id> <signature>

It covers the header, the index, and the epilogue. The header and the index hold the body's hash and every asset's hashes, and every other part of the file is fixed or checked byte for byte, so a valid signature vouches for the whole document.

What a reader and a viewer do:

A known signer means the content is authentic, not that it is safe. Stripping, sanitizing, and the rule against remote loads apply to signed files the same way (Sections 17.1 and 20).

No encryption. Zipdown defines no encryption: an encrypted file is no longer readable Markdown. To protect a file in transit, encrypt the whole file with a standard tool such as age [age].

17. Reading a File

Informative.

  read the first few KB
          |
          v
  zd-version in the header? --no--> plain Markdown, done
          | yes
          v
  MLO > 0, separator at           --no--> scan line by line for
  header end + MLO, delimiter ----------> the opening delimiter;
  line right after it?                    body ends 34 octets before it
          | yes                                 |
          v                                     v
  body known: render it  <----------------------+
          |
          v
  last line is "."? read the pointer above it --> seek to the index
          |
          v
  check the index (ranges inside the payload, ascending,
  no overlaps, sizes match line counts)
          |
          v
  for each asset a link needs: hash the line range (base64-sha256),
  decode, hash again (sha256), hand the bytes to the renderer

Reader flow

A tool that never shows the file to a person may skip verification. A tool that shows it MUST verify what it shows (Section 20). A valid signature from a known key lets it skip some cross-checks (Section 16).

17.1. Verify, Then Strip

Some parts of a file are invisible in every renderer: HTML comments in the body, assets nothing links to, and invisible characters. People cannot see them, but software that reads the file can, which makes them a channel for hiding instructions aimed at AI agents. So a conforming reader, after verifying the original bytes, strips them before handing the document on:

Everything downstream of the reader, agents and tools included, sees only the stripped document. A reader that hands a document to an agent SHOULD pass images as pixels only, never as the original file bytes.

A viewer shows what was stripped in a sidebar beside the document: each comment anchored where it sat, the unlinked assets, a mark wherever invisible characters were removed, and each image's metadata, original profile text included. HTML is not in the sidebar; it gets the notice in Section 7.12. Source mode, the raw file, shows everything. People can see all of a file; agents cannot be steered by what people cannot see.

18. Appearance in Plain Renderers

Informative. A renderer that knows nothing about Zipdown shows:

  +--------------------------------------------------+
  | Title                                            |   body, as usual;
  | ... [broken image: dot]{width=10%} ...           |   asset images break
  | ------------------------------------------------ |   rule
  | -----END OF DOCUMENT-----                        |   separator
  | ------------------------------------------------ |   rule
  | --3e9c81245fe5...dbd9ba-af2fb38cc34248a2         |   marker line, visible
  |                                                  |   on purpose
  | path  size  lines(1-based,inclusive)  ...        |   index, monospace
  | assets/dot.png  70  14-15  ...                   |
  |                                                  |
  | Psst, this is a Zipdown file! ...                |   epilogue, one
  | (the same, in Chinese)                           |   language per line
  |                                                  |
  | .                                                |   terminator
  +--------------------------------------------------+
     (payload and pointer: hidden inside HTML comments)

Plain renderer view

Renderers vary. Some escape raw HTML, in which case the payload shows as text. Renderers that follow comment rules older than CommonMark 0.31, which forbid -- inside a comment, may end the comment early at the closing delimiter line.

19. Extracting an Asset by Hand

Informative. Given a line range L1-L2 from the index:

sed -n 'L1,L2p' file.zd | base64 -d > out
awk 'NR>=L1 && NR<=L2' file.zd | base64 -d > out
head -n L2 file.zd | tail -n +L1 | base64 -d > out

In vi, from command mode:

:L1,L2w !base64 -d > out

Older macOS base64 spells decode -D.

Extracting by line range

20. Security Considerations

A Zipdown file is untrusted input. Most risks of Markdown itself, including raw HTML in the body, are already described in [RFC7763], and renderers apply their usual sanitization. What Zipdown adds:

21. Media Type, Extension, and Site

22. Implementations

23. Future Work

24. Example

A complete, verified Zipdown file carrying a 1x1 PNG and a short text file. For reproducibility, R was derived from a fixed seed instead of a random source; a real writer MUST use a cryptographically secure random source. The same bytes travel inside this document as assets/example.zd.

---
zd-version: 1
zd-markdown-length-octets: 107
zd-markdown-sha256: 701b9eb2ff1a4cd68184c120c84f68d715fe0bceab30cfbd41703a59dcbf40ce
zd-boundary: bd0805a4e5e77c086265536f5094c682f8dc0d06a8c26e62-41703a59dcbf40ce
---
# Hello, Zipdown

Here is a dot: ![dot](assets/dot.png){width=10%}

And a note: [notes](assets/notes.txt)

***
-----END OF DOCUMENT-----
***
--bd0805a4e5e77c086265536f5094c682f8dc0d06a8c26e62-41703a59dcbf40ce
<!--
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhf
DwAChwGA60e6kgAAAABJRU5ErkJggg==

SGVsbG8gZnJvbSBpbnNpZGUgYSBaaXBkb3duIGZpbGUuCg==
--bd0805a4e5e77c086265536f5094c682f8dc0d06a8c26e62-41703a59dcbf40ce--
--><br/>
```zipdown-index
path  size  lines(1-based,inclusive)  base64-sha256  sha256
assets/dot.png    70  18-19  a666a27bb8de48cd9fa7e147d451a1d5b7ed400fd7f8e3a3944607c68ff1e6d4  497790947d4666760ce38f3c00e852c71fdb66cae849bae8e9ede352719e1581
assets/notes.txt  34  21-21  4eb8c6623242149abd72ed676f95c77a595766610c2e8b34dcf33634b8537843  2733e8b5c00a8b570f9fd273463a75600f35ec69d6533877568ebf5b809b1c76
```
Psst, this is a Zipdown file! There are images and files tucked inside that a plain Markdown viewer can't show you. Grab a Zipdown reader at zd.treeflow.ai to see everything. \
嘿,这是一个 Zipdown 文件!里面藏着普通 Markdown 查看器显示不了的图片和文件。到 zd.treeflow.ai 下载 Zipdown 阅读器,就能看到全部内容。
<!-- zd-index 658 00000000000000000000000000000292 -->
.

Checks performed on this example: it contains no CR; its header ends at octet 217, and the separator begins exactly at that plus MLO (107), with the opening delimiter line right after it; the marker's tail equals the last 16 hex digits of zd-markdown-sha256; the index rows (assets/dot.png, 70 octets, lines 18-19; assets/notes.txt, 34 octets, lines 21-21) lie inside the payload, ascend, and span the line counts their sizes imply; both hashes on each row match; the epilogue starts with the exact English line and has one translation; the pointer (658) lands on the index fence; and the last line is the terminator.

A signed copy travels as assets/example-signed.zd. It is signed with a test key whose 32-octet private seed is the SHA-256 of the text Zipdown example key: published, never trust. Anyone can recompute that key, so it MUST NOT be trusted for anything real. Its public key, in hex:

3cec666df17aaa5394160c49bf387f23b91356fd8917c3b6febf495d9d493cd0

Its header:

---
zd-date: 2026-11-11T21:04:00.000000000Z
zd-version: 1
zd-markdown-length-octets: 107
zd-markdown-sha256: 701b9eb2ff1a4cd68184c120c84f68d715fe0bceab30cfbd41703a59dcbf40ce
zd-boundary: bd0805a4e5e77c086265536f5094c682f8dc0d06a8c26e62-41703a59dcbf40ce
zd-signature: ed25519 ad7117d6ed57a87b ENM7uyF9j23ETthigh+g4iDvJRqRGaV2Mq7CdLzH/EcnZ2/B/qIAMIp0CKuvqJ7CyBpPrf7dYpd3H0greJbqAA==
---

Checks performed on the signed copy: every check above passes for it too; its key id matches the public key; and the Ed25519 signature over the context string, the header with the signature removed, the index, and the epilogue verifies.

25. Open Issues

26. References

26.1. Normative References

26.2. Informative References

Acknowledgments

Site version only.

The author thanks TurboAsshole™ for several proposals:

Ever since we dropped ZIP, the file's really opened up.

Appendix A. Notes for Viewers and Editors

Informative. What the reference viewer and editor do; other tools are free to differ.