JuliusBrussee/caveman · error
ccr: typed object content_hash does not match data
Error message
ccr: typed object content_hash does not match data
What it means
prepareObject computes sha256 over obj.Data and compares it to the caller-supplied obj.ContentHash. If you set ContentHash yourself it must be exactly `sha256:` + lowercase hex of the SHA-256 of Data; any mismatch is rejected because content-addressed identity and dedup depend on the hash being truthful.
Source
Thrown at engine/ccr/store.go:130
return Object{}, fmt.Errorf("ccr: unknown currentness %q", obj.Currentness)
}
if obj.Lifecycle == "" {
obj.Lifecycle = Hot
}
if obj.Lifecycle != Hot && obj.Lifecycle != Warm && obj.Lifecycle != Cold && obj.Lifecycle != LifecycleArchived {
return Object{}, fmt.Errorf("ccr: unknown lifecycle %q", obj.Lifecycle)
}
if obj.CreatedAt.IsZero() {
obj.CreatedAt = time.Now().UTC()
} else {
obj.CreatedAt = obj.CreatedAt.UTC()
}
dataSum := sha256.Sum256(obj.Data)
computedHash := "sha256:" + hex.EncodeToString(dataSum[:])
if obj.ContentHash == "" {
obj.ContentHash = computedHash
} else if obj.ContentHash != computedHash {
return Object{}, errors.New("ccr: typed object content_hash does not match data")
}
if obj.OriginalByteLength == 0 {
obj.OriginalByteLength = len(obj.Data)
}
if obj.StoredByteLength == 0 {
obj.StoredByteLength = len(obj.Data)
}
if obj.OriginalByteLength < 0 || obj.StoredByteLength < 0 {
return Object{}, errors.New("ccr: typed object byte lengths cannot be negative")
}
if obj.ID == "" {
identity := strings.Join([]string{string(obj.Type), obj.SessionID, obj.Source, obj.RepositoryState, obj.ContentHash}, "\x00")
sum := sha256.Sum256([]byte(identity))
obj.ID = "ccr_obj_" + hex.EncodeToString(sum[:16])
}
obj.Dependencies = append([]string(nil), obj.Dependencies...)
obj.Data = bytes.Clone(obj.Data)
return obj, nilView on GitHub (pinned to 27d5a3981a)
Solutions
- Omit ContentHash entirely — prepareObject fills it in for you
- Or compute it as `"sha256:" + hex.EncodeToString(sha256.Sum256(data)[:])` over the exact Data bytes you pass
- Re-derive the hash after any transformation of Data, immediately before PutObject
Example fix
// before
sum := sha256.Sum256(original)
obj := ccr.Object{ContentHash: hex.EncodeToString(sum[:]), Data: transformed} // wrong: missing prefix, hashed wrong bytes
// after
obj := ccr.Object{Data: transformed} // ContentHash auto-computed
// or: obj.ContentHash = "sha256:" + hex.EncodeToString(sha256.Sum256(transformed)[:]) Defensive patterns
Strategy: validation
Validate before calling
if obj.ContentHash != "" {
sum := sha256.Sum256(obj.Data)
if obj.ContentHash != "sha256:"+hex.EncodeToString(sum[:]) {
return errors.New("content_hash stale; clear it or recompute")
}
}
store.PutObject(obj) Type guard
func contentHashMatches(data []byte, h string) bool {
sum := sha256.Sum256(data)
return h == "sha256:"+hex.EncodeToString(sum[:])
} Prevention
- Leave ContentHash empty and let the store compute it
- Never mutate Data after setting ContentHash
- Hash the exact bytes you pass, with the sha256: hex format
When it happens
Trigger: Passing a ContentHash computed with a different algorithm or encoding (no `sha256:` prefix, uppercase hex, base64); computing the hash over the original data but then mutating Data (compression, trimming) before PutObject; copying ContentHash from a different object.
Common situations: Data transformed (reflow/compression) between hashing and storing; hash generated by another language/tool with different hex casing; stale hash left on a struct whose Data field is later overwritten.
Related errors
- cave_harness_plan_digest_mismatch
- cave_sandbox_definition_mismatch
- cave_sandbox_tool_definition_mismatch
- ${skill.file} failed skill postflight
- ccr: recovery handle not found
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/e926bea2c6cc7943.
Report an issue: GitHub.