The source of truth for commands. Update it in the same commit as any change to a command.
Why a rule exists lives in docs/DECISIONS.md; results live in
docs/THREAT_MODEL.md §7.
| Tool | Needed for |
|---|---|
| rustup | everything. rust-toolchain.toml pins the compiler and rustup installs it on first use; the MSRV is rust-version in Cargo.toml (ADR-0013) |
Rust nightly, cargo install cargo-fuzz |
fuzzing. Linux and macOS only |
cargo install cargo-deny --locked --version 0.20.2 |
supply-chain checks; the version CI runs |
| Python 3 | fixtures, fuzz analysis, README images |
| mat2, ExifTool | differential testing. Never runtime dependencies |
ffmpeg, libheif, webpinfo, webp-pixbuf-loader, LibreOffice |
individual differentials; see Differential testing |
ImageMagick, qpdf |
performance measurement and fixture checks |
cargo build # whole workspace
cargo build --release
cargo test --all-features # what CI runs, on Linux, macOS and Windows
cargo test -p strypt # CLI contract tests only
cargo test -p strypt-gui # GUI output byte-identical to the CLI's, across the corpus
cargo run -p strypt-gui # the Phase 5 spike window
cargo test jpeg # tests matching a name
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings--all-features compiles the fuzzing module; without it clippy and the tests skip a module.
Installing a release without Rust: README.
cargo run -p strypt -- show corpus/pdf/info-dictionary.pdf
strypt show FILE... # report metadata; never writes
strypt show --show-values FILE... # include values, not only field names
strypt show --json FILE... # machine-readable
strypt strip FILE... # write FILE.stripped.EXT beside each input
strypt strip --in-place FILE... # replace the originals
strypt strip --output-dir OUT FILE... # write copies into OUT
strypt strip --force FILE... # overwrite an existing output
strypt strip --recursive DIR # descend into a directory; symlinks are not followed
strypt strip --max-bytes 1048576 FILE # refuse larger inputsStable across releases; scripts depend on them. When a batch hits several, the most serious wins.
| Code | Meaning |
|---|---|
| 0 | Success, and show found nothing removable |
| 1 | show found metadata, or strip had a file fail |
| 2 | Usage error |
| 3 | A file could not be read or written |
| 4 | A file's format has no handler |
| 5 | Output failed post-strip verification and was discarded — report it as a bug |
What CI runs:
cargo fmt --check && \
cargo clippy --all-targets --all-features -- -D warnings && \
cargo test --all-features && \
./scripts/check-no-network.sh && \
cargo deny check && \
./scripts/prove-gates.shCI also builds at the MSRV (rustup toolchain install 1.95, then cargo +1.95 build --all-features), runs the filesystem matrix on Linux, and
fuzzes every target for 60 seconds. Enable the local pre-commit hook once per clone:
git config core.hooksPath .githooksFrom the fuzz crate, which is its own workspace:
cd crates/strypt-core/fuzz
cargo +nightly fuzz list
T=pdf; mkdir -p corpus/$T && cargo +nightly fuzz run $T corpus/$T seeds/$T -- -max_total_time=300
cargo +nightly fuzz run $T artifacts/$T/crash-<hash> # reproduce a crash
cargo +nightly fuzz cmin $T corpus/$T # minimiseGive corpus/<target> first, never seeds/. libFuzzer writes discoveries into the first
directory; seeds/ is the committed corpus (seeds/README.md),
corpus/ is git-ignored. Crash inputs land in artifacts/<target>/.
The certification bar is ADR-0044's: 24 CPU-hours per handler and a saturated coverage curve. From the repository root:
./scripts/fuzz-sustained.sh -h # options; default is every target, 2h each
./scripts/fuzz-sustained.sh -d 86400 pdf jpeg # 24h each, in parallel
nohup caffeinate -ims ./scripts/fuzz-sustained.sh -d 86400 pdf > /tmp/strypt-fuzz.out 2>&1 &
./scripts/fuzz-status.sh # live view; Ctrl-C stops the viewer, not the run
python3 scripts/fuzz-tally.py # which handlers certify, which owe a run
python3 scripts/fuzz-plateau.py [target...] # saturated or PUNCTUATED, per curve- On macOS,
caffeinate -imson AC power, lid open — a sleeping machine delivers a fraction of the budget silently. IfELAPSEDinfuzz-status.shstops advancing, the run is void. - Quote CPU-hours delivered, never budgeted. They differ when a target stops early.
- Read a
summary.mdfrom before 2026-09-11 withfuzz-plateau.py; itsplateaucolumn uses ADR-0014's superseded rule. - The runner's target list must include every target in
fuzz list; check after adding one.
Each run writes summary.md, cov-<target>.tsv and <target>.log to fuzz-runs/<timestamp>/.
That is deliberately outside target/, which cargo clean empties (2026-09-23).
Stopping early is safe: discoveries are already in corpus/.
Generated, so no fixture carries real personal data (docs/TESTING_STRATEGY.md §3).
Each is documented in corpus/MANIFEST.md. Refresh the matching seeds/
copies after regenerating.
for t in pdf jpeg png webp tiff gif heif svg jxl flac wav mp3 ogg mp4 ooxml odf; do
python3 corpus/tools/make_${t}_fixtures.py
done # ooxml and odf embed JPEG and PNG fixtures, so they run last
qpdf --check corpus/pdf/info-dictionary.pdf # structurally sound
magick identify corpus/gif/animated-loop.gif # still decodes
heif-convert corpus/heif/clean.avif /tmp/x.png # decodes through libheif
exiftool corpus/jpeg/exif-gps.jpg # carries what the manifest says
unzip -l corpus/odf/everything.odt # first entry must be a stored `mimetype`Real files carrying real names and live GPS, kept out of the repository; only the build script and manifests are committed.
cd real-producer-corpus
python3 build_real_corpus.py # 102 fixtures; the first run clones three upstreams
python3 build_real_corpus.py --with-browser # + one Chrome-encoded WebP; opt-in, browsers churnEvery crash, hang or OOM needs a regression test and its input in the corpus before the fix is
accepted (docs/TESTING_STRATEGY.md §2.6).
Before every release, not per commit. Build --release first; every script defaults to
target/release/strypt (override with STRYPT=), refuses to run without its tools, and reports
what survives each tool's output — a gap is a bug or a documented limitation, never silence.
| Script | Also needs |
|---|---|
ooxml-differential.sh, odf-differential.sh, tiff-differential.sh, gif-differential.sh, svg-differential.sh |
— |
jxl-differential.sh |
python3 |
heif-differential.sh |
libheif's heif-convert |
flac-differential.sh, wav-differential.sh, mp3-differential.sh, ogg-differential.sh, mp4-differential.sh |
ffmpeg, python3 |
webp-differential.sh [DIR] |
webpinfo, and webp-pixbuf-loader — without it mat2 cannot read WebP |
cargo build --release && ./scripts/mp4-differential.shPDF, JPEG and PNG have no script:
mkdir -p /tmp/strypt-diff && cp corpus/pdf/*.pdf /tmp/strypt-diff/
./target/release/strypt strip /tmp/strypt-diff/*.pdf
exiftool -s -G -ee /tmp/strypt-diff/*.stripped.pdf # -ee: without it, a PDF's images go unread | grep -vE '^\[(File|ExifTool)\]'
mat2 --show /tmp/strypt-diff/*.stripped.pdfcargo build --release && ./scripts/odf-libreoffice-validation.sh
CORPUS=/path/to/odf/files ./scripts/odf-libreoffice-validation.shNeeds soffice on PATH. It checks that stripped packages still import, not that no repair
prompt appears; opening a few by hand is re-owed whenever the handler changes.
Hard merge gates (ADR-0045):
cargo deny check # advisories, licenses, bans, sources
cargo deny check advisories # or one at a time
./scripts/check-no-network.sh # ADR-0004: fails on any networking crate, transitive included
cargo tree -i reqwest # who pulls a crate in (expect: nothing)A red advisories run with no change here is a new advisory or yank: fix it, or add a reasoned
ignore to deny.toml.
./scripts/prove-gates.sh # ~20s; needs networkPlants one violation per check in a throwaway copy — a networking crate, a duplicate, a
wildcard, a copyleft licence, a git source, a vulnerability, a yanked crate — and requires each
gate to fail with its own diagnostic. CI runs it. If a case fails untouched, check its external
assumption first: the yank case needs chacha20 0.10.1 still yanked.
Linux, non-root with passwordless sudo, dosfstools and exfatprogs. Cases:
docs/TESTING_STRATEGY.md §2.7.
cargo build -p strypt && ./scripts/fs-matrix.sh [BINARY]
./scripts/prove-fs-matrix.sh # five io.rs mutants; each must be caughtOn macOS, in Docker:
docker build -t strypt-fsm - <<'EOF'
FROM rust:1-bookworm
RUN apt-get update -qq && apt-get install -y -qq sudo dosfstools exfatprogs python3 git \
&& chmod -R a+w /usr/local/rustup /usr/local/cargo && useradd -m u \
&& echo 'u ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/u && git config --system --add safe.directory '*'
USER u
EOF
docker run --rm --privileged -v "$PWD":/src:ro -w /src -e CARGO_TARGET_DIR=/tmp/t strypt-fsm \
sh -c 'cargo build -q -p strypt && scripts/fs-matrix.sh /tmp/t/debug/strypt && scripts/prove-fs-matrix.sh'ADR-0050. Release binaries come only from CI's release workflow, which builds each target twice
and fails unless the two match. A local release build embeds your home directory's paths.
scripts/build-release.sh aarch64-apple-darwin # -> target/release-artifacts/strypt-<version>-<target>
gh workflow run release.yml # the gate on all five targets; publishes nothing
gh workflow run release.yml -f prove=true # drops a remap; each job passes only if the gate catches it
scripts/sbom.sh out x86_64-unknown-linux-musl # -> out/strypt-<version>-<target>.cdx.json (ADR-0054)sbom.sh needs cargo install cargo-cyclonedx --version 0.5.9 --locked, the version release.yml pins.
Cutting a release:
- Bump
versioninCargo.tomland thestrypt-corerequirement incrates/strypt/Cargo.toml, refresh bothCargo.locks (the fuzz crate has its own), date CHANGELOG's[Unreleased], and update the version in README's install section. - Push the tag
v<version>.release.ymldrafts the release; review it, then publish it. cargo publish -p strypt-core, thencargo publish -p strypt.- In homebrew-strypt, set the four URLs and SHA256s
in
Formula/strypt.rbfromSHA256SUMS, push, then:
brew update && brew upgrade fadehack/strypt/strypt && brew test fadehack/strypt/strypt
brew audit --strict --online fadehack/strypt/stryptgh workflow run install.ymlruns README's install steps on fresh runners. Update the file names andBUILT_FROMin it first.
SECURITY.md is the reporter's side and its targets bind. Everything stays in the
advisory until step 6: no public issue, commit, branch or CI run.
-
Acknowledge within 72 hours, as a comment on the report; it needs no acceptance first. An emailed report becomes a draft advisory: Security and quality tab, New draft security advisory.
-
Grade it within 7 days:
- Critical: silent incomplete removal, or any network connection.
- High: leakage through strypt's own output, logs or temp files; memory corruption.
- Medium: panic, crash, hang or runaway memory on a crafted file.
-
Find what is affected: run a synthetic reproduction through each released binary, and record the versions and formats. The reporter's file never enters the repository. Judge the output with ExifTool and a byte search, not
strypt show, which is what is under suspicion:gh release download v<version> -R FadeHack/strypt -p 'strypt-<version>-<target>' -p SHA256SUMS sha256sum --ignore-missing -c SHA256SUMS
If it reproduces, Accept and open as draft; the advisory's affected versions come from here. If not, ask for the file's structure, never the file, and close the report only once the reporter agrees or stops replying.
-
Fix in the advisory's temporary private fork. CI does not run there, so run the full loop above locally. The fix carries a regression test and the synthetic input in the corpus.
-
If a Critical fix will miss 30 days, publish the advisory with a mitigation: the affected formats, and mat2 where it handles them (ADR-0012). Add it to KNOWN_LIMITATIONS.
-
Release: merge the fork, cut a release as above with a
Securityentry naming versions and formats, request a CVE from the advisory, publish it, and credit the reporter as they chose. File the RustSec advisory (rustsec/advisory-db) forstrypt-core.
The numbers in docs/PRD.md §9. Needs a release binary and ImageMagick.
cargo build --release && ./scripts/measure-performance.sh
REPS=15 BATCH=3000 ./scripts/measure-performance.sh
BIN=path/to/strypt ./scripts/measure-performance.sh # any other build
gh workflow run perf.yml # musl against glibc on Linux (ADR-0050)Re-run after any change to CLI output; it renders docs/assets/demo.svg from the release binary
and strips both README images with strypt.
python3 scripts/render-demo.py