jdx/mise · error
staged blob does not match the declared CAS digest
Error message
staged blob does not match the declared CAS digest
What it means
`store_file` reflinks or copies the source into a staging temp file and then re-hashes the staged copy against the declared digest before publishing. This error means the staged bytes do not match: in practice the source file changed between when the digest was computed and when the copy was made.
Solutions
- Recompute the digest immediately before staging: `let digest = CacheDigest::blake3_file(&path)?; cas.store_file(&digest, &path)?` and ensure no writer is active.
- If the source may still be mutating, copy it to a stable location first, then hash and store the copy.
- Retry once the writer has finished; a racing writer that has since exited makes the next attempt succeed.
Example fix
// before: digest computed earlier, file changed since cas.store_file(&stale_digest, &path)?; // bails // after: hash the current file right before storing let digest = CacheDigest::blake3_file(&path)?; cas.store_file(&digest, &path)?;
Defensive patterns
Strategy: validation
Validate before calling
// hash the file right before staging so mutation windows are minimal
let digest = CacheDigest::blake3_file(&source)?;
if !digest.matches_file(&source)? {
// source is still changing; wait for the writer to finish
return wait_and_retry();
}
cas.store_file(&digest, &source)?; Prevention
- Only cache files after the producing build has fully written and closed them.
- For mutable sources, copy to a stable path first and hash/store the copy.
- Keep digest computation and store_file adjacent in code - no awaits or other work between them.
When it happens
Trigger: The digest was computed from a file that is concurrently rewritten (build output still being written by the build, another process touching the file), or it was computed from a different path than the one passed to `store_file`.
Common situations: Caching build artifacts while the build still holds the file open; hashing then copying with an intervening mutation; network filesystems with weak read consistency.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- bytes do not match the declared CAS digest
- local CAS blob failed digest verification
- staged blob size does not match the declared CAS digest
- cannot publish an action result with a missing blob
- expected pre-planted symlink destination to be refused
AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22).
Data as JSON: /api/errors/9e86f8d79c092432.
Report an issue: GitHub.
Appendix: source
Thrown at crates/mise-cache-core/src/local.rs:104
verify: bool,
) -> Result<PathBuf> {
let destination = self.path_for(digest)?;
if let Some(existing) = self.find(digest)? {
return Ok(existing);
}
let parent = destination.parent().expect("CAS path has a parent");
fs::create_dir_all(parent)?;
let staging = tempfile::tempdir_in(parent)?;
let temporary = staging.path().join("blob");
reflink_copy::reflink_or_copy(source, &temporary)?;
let temporary = tempfile::TempPath::try_from_path(temporary)?;
make_owner_writable(&temporary)?;
fs::OpenOptions::new()
.write(true)
.open(&temporary)?
.sync_all()?;
if verify && !digest.matches_file(&temporary)? {
bail!("staged blob does not match the declared CAS digest");
}
if fs::metadata(&temporary)?.len() != digest.size {
bail!("staged blob size does not match the declared CAS digest");
}
match temporary.persist_noclobber(&destination) {
Ok(()) => Ok(destination),
Err(error) if error.error.kind() == std::io::ErrorKind::AlreadyExists => self
.find(digest)?
.ok_or_else(|| eyre::eyre!("concurrent CAS write did not publish a valid blob")),
Err(error) => Err(error.error.into()),
}
}
fn store_with(
&self,
digest: &CacheDigest,
write: impl FnOnce(&mut tempfile::NamedTempFile) -> Result<()>,
) -> Result<PathBuf> {View on GitHub (pinned to 6f52dcdf99)