How to timestamp normaliser in developer workflows
- Step 1Confirm timestamps are your non-determinism source — Build twice, hash both archives. If they differ, list entries and check whether only the mtimes vary. If so, this tool fixes it; if file ordering or content also varies, address those too (see the edge cases).
- Step 2Open the normaliser — Go to /archive-tools/timestamp-normalizer. Processing is client-side via fflate + libarchive WASM. On Pro+ the page can offload to your paired @jadapps/runner automatically when available.
- Step 3Drop your build artifact — ZIP, tar.gz, gz, 7z, etc. — the format is detected from magic bytes. Remember the output is always a ZIP, so if your release format is tar.gz, plan a conversion step afterward.
- Step 4Pin the Target date to your epoch — Leave it at
1980-01-01to match the classic reproducible-build epoch, or set the calendar date yourSOURCE_DATE_EPOCHresolves to. The picker is date-only; every entry lands at 00:00:00 UTC. - Step 5Normalise and read the metrics — The result reports the applied Timestamp (ISO) and Entries count. Confirm the entry count equals your expected file count — a mismatch usually means empty directories were dropped or the wrong file was loaded.
- Step 6Wire reproducibility checks downstream — Hash the normalised ZIP with checksum-generator and assert it against the expected digest in your verification step. For remaining non-determinism, use path-prefix-remover and filename-sanitizer.
Where it fits in a developer pipeline
The normaliser handles the timestamp axis of reproducibility. The other axes need siblings or build-config changes.
| Reproducibility axis | Handled by this tool? | If not, use |
|---|---|---|
| Entry modification times | Yes — set to one date | — |
| File ordering within the archive | No | Sort before packing / build config |
| Wrapper / top-level folder | No | path-prefix-remover |
| Path casing / illegal chars | No | filename-sanitizer |
| Compression level | Fixed at DEFLATE 6 | compression-level-optimizer |
| Container format (must stay tar.gz) | No — outputs ZIP | zip-to-tar-gz |
Execution model and limits
Browser-first execution. No public POST run API; Pro+ can offload to the @jadapps/runner. Real tier limits below.
| Aspect | Detail |
|---|---|
| Where it runs | Browser (fflate + libarchive WASM) |
| Server-side path | None (browserOnly) |
| Public REST run endpoint | No (apiAvailable: false) |
| Pro+ acceleration | @jadapps/runner — headless Chromium |
| Free limit | 50 MB / 500 entries / 1 file |
| Pro limit | 500 MB / 50,000 entries / 20 files |
| Developer limit | 2 GB / 500,000 entries / unlimited files |
The single option
No compressionLevel, no per-entry rules, no time-of-day — just one date.
| Option | Type | Default | Range / behaviour |
|---|---|---|---|
| Target date | Date picker | 1980-01-01 | Any calendar date; applied as 00:00:00 UTC to all entries |
Cookbook
Reproducible-build and CI snippets showing the normaliser as one step among several.
Converge laptop and CI artifact hashes
The same commit built in two places yields two hashes. Normalise both to the epoch and they converge — assuming identical file ordering and that both end up as ZIP.
laptop: dist.zip sha256: a0... (mtimes = build time) ci: dist.zip sha256: b7... (mtimes = build time) Normalise both, Target date 1980-01-01: dist-normalized.zip sha256: 4f... (both) Reproducible -> cache key now stable across machines.
Resolve SOURCE_DATE_EPOCH to the picker date
If your toolchain pins SOURCE_DATE_EPOCH to the commit date, set the picker to the same calendar date so the browser-normalised artifact lines up date-wise.
git log -1 --format=%cI -> 2026-05-31T18:22:09Z SOURCE_DATE_EPOCH date -> 2026-05-31 Browser: Target date = 2026-05-31 => every entry mtime = 2026-05-31T00:00:00.000Z (note: 00:00 UTC, not 18:22 — date-only picker)
Pre-publish normalisation before release upload
Right before publishing a release asset, normalise so the published artifact is reproducible and its checksum is stable for users to verify.
build -> app-2.0.0.zip timestamp-normalizer (1980-01-01) -> app-2.0.0-normalized.zip /archive-tools/checksum-generator -> SHA-256 published in release notes Users re-hash the download and get the same value.
Full reproducibility chain for a repo download
A GitHub source download has a wrapper folder, build-time mtimes, and possibly mixed path casing. Chain three tools to canonicalise all three axes.
repo-main.zip -> timestamp-normalizer (1980-01-01) # dates -> /archive-tools/path-prefix-remover # strip repo-main/ -> /archive-tools/filename-sanitizer # canonicalise paths => repo-canonical.zip (stable hash candidate)
Offload a large job to the runner (Pro+)
On Pro and above, the tool page can dispatch the job to your paired @jadapps/runner — a local headless Chromium — instead of the active tab, useful for bigger artifacts.
Pro+ with paired runner: drop build-300mb.zip -> tool dispatches to @jadapps/runner -> runner executes in headless Chromium locally -> build-300mb-normalized.zip returned to the page No REST endpoint; this is runner-acceleration, not a server API.
Edge cases and what actually happens
Output is ZIP even from a tar.gz build
ExpectedThe tool always emits <name>-normalized.zip. If your release format must remain tar.gz, add a conversion step with zip-to-tar-gz after normalising — but note re-wrapping introduces its own gzip mtime to manage.
Hashes still differ after pinning dates
ExpectedTimestamps are only one source of non-determinism. Different entry ordering, a wrapper folder, path casing, or compression level will still change the bytes. Pin those too — the table above maps each axis to the right tool.
Compression level is fixed at 6
By designThere is no level option; output is DEFLATE level 6. So a store-level (level 0) reproducible build cannot be reproduced byte-for-byte here. Manage compression in your build or with compression-level-optimizer.
No public REST run API
Browser onlyArchive tools expose no POST run endpoint, so you cannot curl this in a headless CI runner. Pro+ can offload to the @jadapps/runner, but for fully scripted pipelines a native strip-nondeterminism step is usually cleaner.
Date-only — no exact epoch second
Partial matchThe picker stamps 00:00:00 UTC. If your CLI pins a non-midnight SOURCE_DATE_EPOCH second, the browser output matches the date but not the second, so byte parity with that CLI requires both sides at midnight UTC.
Empty directories dropped from output
Not preservedExtraction keeps only file entries, so empty-folder records won't appear in the output. If your build or tests rely on an empty directory existing in the archive, recreate it post-extraction or via build config.
Encrypted artifact
FailNo password field, so an encrypted ZIP fails to read. Encrypt as a final step after normalising, or decrypt with multi-format-extractor before normalising.
Artifact exceeds the tier cap
Tier limitFree is 50 MB / 500 entries. CI monorepo bundles often exceed this; Pro reaches 500 MB / 50,000 entries and Developer 2 GB / 500,000. Both file size and entry count are enforced before processing.
Invalid target date string via the runner
Invalid date errorThe UI picker only emits valid dates, but an automation/runner path passing a malformed targetTimestamp triggers Invalid target timestamp. Always pass a parseable YYYY-MM-DD.
Re-compression changes the output size
ExpectedBecause every artifact is re-zipped at level 6, the output size differs from the input even when contents are identical. Compare hashes of the normalised outputs, not raw sizes, to judge reproducibility.
Frequently asked questions
Does this give me a reproducible build on its own?
It handles the timestamp axis — every entry gets one uniform mtime. Full reproducibility also needs consistent file ordering, no build-time wrapper folder, stable path casing, and matching compression/container. Pin those alongside, and the archive hash becomes stable.
Why is 1980-01-01 the default?
It's the MS-DOS / ZIP epoch — the earliest representable ZIP date — and the convention reproducible-build toolchains (Nix, Bazel, Maven, strip-nondeterminism) standardise on. Matching it makes your artifacts line up with that ecosystem.
Can I call this from CI like a CLI?
Not via a REST endpoint — archive tools have apiAvailable: false. Pro+ tiers can offload execution to the paired @jadapps/runner (a local headless Chromium), but there is no HTTP run API to curl. For fully unattended pipelines, a native strip-nondeterminism step is typically the better choice.
My output is a .zip but I need a .tar.gz — why?
The normaliser always writes ZIP regardless of input. Convert afterward with zip-to-tar-gz. Be aware the gzip wrapper adds its own mtime, so set it deterministically in the conversion/build to keep reproducibility.
Can I match an exact SOURCE_DATE_EPOCH second?
No — the picker is date-only and stamps 00:00:00 UTC. You can match the calendar date, but not a non-midnight second. For exact-second parity, drive both your CLI and the archive to midnight UTC.
Does it change my files or just metadata?
Only modification-time metadata. File contents are extracted and re-packed verbatim. The overall ZIP size may change because it re-compresses to DEFLATE level 6, but each file's bytes are identical to the source.
Why do my hashes still differ after I normalise?
Something other than timestamps varies — usually file ordering, a wrapper folder, path casing, or compression level. Use the axis table: path-prefix-remover, filename-sanitizer, and compression handling cover the common remaining causes.
Can I control the compression level?
No. Output is fixed at DEFLATE level 6. If you need a specific level (including store), manage it in your build or with compression-level-optimizer; the normaliser won't reproduce a different level.
Will it preserve empty directories my build expects?
No — empty-directory entries are dropped during extraction. If a build step or test relies on an empty folder existing in the archive, recreate it after extraction or via build configuration.
How big an artifact can it handle?
Free: 50 MB / 500 entries / 1 file. Pro: 500 MB / 50,000 / 20. Pro + Media: 2 GB / 500,000 / 100. Developer: 2 GB / 500,000 / unlimited files. Both size and entry count are checked, so large file counts can trip the cap under the size limit.
Is my proprietary code uploaded anywhere?
No. Processing is in-browser via fflate and libarchive WASM, and on Pro+ via your local @jadapps/runner. The only server-side record is an anonymous usage counter with no file content — safe for proprietary build artifacts.
What's the cleanest reproducibility chain using JAD tools?
timestamp-normalizer for dates, then path-prefix-remover to drop the wrapper folder, then filename-sanitizer for path canonicalisation, then checksum-generator to assert the resulting hash. Use archive-diff to investigate any mismatch.
Privacy first
Every JAD Archive tool runs entirely in your browser using fflate, @zip.js/zip.js, and the libarchive WASM bridge. Your archives never leave your device — verified by zero outbound network requests during processing.