Manifests

A manifest is the heart of a snapdir snapshot: a UTF-8 text document that fully describes a directory tree — every file and directory, its permissions, its content checksum, and its size. Because the description is entirely content-addressed, a manifest is a portable, deduplicating snapshot. Two trees with identical content produce byte-identical manifests and therefore the same snapshot ID.

You can produce a manifest without writing anything to a store with snapdir manifest, and compute just its identifier with snapdir id.

Line format

Each non-comment, non-empty line is one entry with exactly five single-space-separated fields:

TYPE PERMS CHECKSUM SIZE PATH
Field Meaning
TYPE F for a regular file, D for a directory.
PERMS Octal permission string from stat, e.g. 700, 600.
CHECKSUM Lowercase hex content checksum of the entry (see below).
SIZE Content size in bytes.
PATH The entry's path, taken verbatim.

Example — two empty files in a directory:

D 700 dba5865c0d91b17958e4d2cac98c338f85cbbda07b71a020ab16c391b5e7af4b 0 ./
F 600 af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262 0 ./bar.txt
F 600 af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262 0 ./foo.txt

Some details that matter for round-tripping:

  • Paths may contain spaces. Lines are split on only the first four spaces; the fifth field, PATH, is taken verbatim, so ./a file with spaces.txt survives intact.

  • Directories always end with /./, ./a/, ./a/aa/. In relative mode (the default) every path is prefixed with ./ and the tree root is the ./ entry; with --absolute the full path is kept verbatim.

  • Symlinks have no type of their own. A followed symlink is recorded as the type of its target (F or D); --no-follow excludes symlinks entirely.

  • Ordering is by PATH using byte-wise (C-locale) sort -k5 semantics — purely on the path bytes, not on type or checksum. So ./a/ sorts before ./a/a1f, and a larger checksum can precede a smaller one when the paths demand it.

  • Comments (#…) and empty lines are ignored and are excluded from the snapshot ID.

Directory checksums (the merkle rule)

A directory's CHECKSUM is not a hash of its own metadata. It is a merkle derivation from the checksums of its direct children:

  1. Take the CHECKSUM field of each direct child entry.
  2. Sort them lexicographically (byte-wise).
  3. Deduplicate (sort -u).
  4. Concatenate with no separator.
  5. Re-hash the result with the active checksum function.

This makes the directory hash order-independent and lets identical subtrees collapse, which is the structural side of snapdir's deduplication. Two edge cases follow directly:

  • An empty directory hashes the empty string, giving af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262.

  • A directory of two empty files dedupes their identical checksums to one value, giving dba5865c0d91b17958e4d2cac98c338f85cbbda07b71a020ab16c391b5e7af4b.

The snapshot ID

The snapshot ID is what snapdir id reports and the key under which a manifest is stored. It is a distinct value from the root directory checksum:

The snapshot ID is the BLAKE3 hash of the entire #-stripped manifest text, including the single trailing newline after the last line — not the CHECKSUM field of the root D ./ line.

Confusing the two is the classic snapdir foot-gun. For the two-empty-files manifest above, the root D ./ checksum is dba5865c… while the snapshot ID is c678a299…. Reproducing the golden IDs requires that trailing newline; hashing the text without it does not match. The snapshot ID is always derived with default BLAKE3, independent of any --checksum mode chosen for the per-entry checksums.

Because the ID is a hash of the whole document, it changes if anything in the tree changes — a byte in a file (which changes that file's checksum, hence its line, hence the manifest), a permission bit, a rename, or an added/removed entry. That is what makes it a faithful, verifiable identity for a directory state.

Where to go next