A static artifact is a directory, not one executable. Recording only the source commit or homepage checksum leaves gaps: a missing image, stale alias, or changed download can survive unnoticed. Enumerate every regular file in a frozen artifact before transport.
Generate a stable list
Run this in Bash with GNU findutils and coreutils, from the directory containing public/. Keep the manifest outside the measured tree. The release must contain only directories and regular files: inspect and reject symlinks or special files first, since they are outside this manifest’s contract.
set -euo pipefail
manifest="$PWD/release.sha256"
(
cd public
nonregular=$(find . ! -type d ! -type f -print -quit)
test -z "$nonregular"
find . -type f -print0 | LC_ALL=C sort -z | xargs -0 -r sha256sum --
) > "$manifest"
test -s "$manifest"
Sort filenames before hashing. NUL-delimited paths preserve spaces, tabs, and embedded newlines between tools; GNU sha256sum escapes special names in its ordinary checkable output. Do not parse that output with awk '{print $2}': whitespace is legal in filenames. The separate assignment preserves a failed find exit status; testing only its output could mistake a failed scan for an empty result. Treat any failed scan or pipeline as an incomplete manifest and do not publish it. Stop writers while measuring; these commands do not create a filesystem snapshot.
Verify after transport
In one disposable Bash session on the destination, run the remaining fragments in order so the paths and set options stay in scope. Set the manifest and artifact paths to their actual locations:
set -euo pipefail
manifest=/path/to/release.sha256
artifact=/srv/www/site.new
(
cd "$artifact"
sha256sum -c "$manifest"
)
Checking listed files catches missing or changed bytes, but not extra files. Recompute the destination manifest with the same procedure and compare the complete records:
observed=$(mktemp "${manifest}.observed.XXXXXX")
trap 'rm -f "$observed"' EXIT
(
cd "$artifact"
nonregular=$(find . ! -type d ! -type f -print -quit)
test -z "$nonregular"
find . -type f -print0 | LC_ALL=C sort -z | xargs -0 -r sha256sum --
) > "$observed"
diff -u "$manifest" "$observed"
No diff and exit status zero means the regular-file paths and hashes agree. An extra stale file now fails too. The temporary output remains outside the artifact so the check never includes itself. Empty directories, permissions, ownership, and timestamps are not measured; validate those separately if deployment depends on them.
Add an aggregate digest for release records
Hash the manifest itself for a compact release identifier:
sha256sum "$manifest"
Its meaning depends on fixed rules: relative paths, sorting locale, algorithm, tool format, and included metadata. Keep the trusted manifest with the source commit and builder version.
Do not confuse integrity with authenticity
Matching SHA-256 digests are strong practical evidence of matching bytes, not proof of authorship. An attacker able to replace both artifact and manifest can make the check pass. Signing and trusted distribution are separate controls; verify only a manifest obtained through the intended trusted channel.
Sources
GNU Coreutils: sha2 utilities, checksum generation and verification; reviewed through the linked upstream manual source, checked 2026-10-15.
GNU Coreutils manual source, untagged checksum escaping,
--check, and NUL-delimited sorting, checked 2026-10-15.sha256sum(1), command behavior and output format, checked 2026-07-16.
GNU Bash: Simple Command Expansion, assignment-only commands retain command-substitution exit status, checked 2026-10-18.