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, nil

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Omit ContentHash entirely — prepareObject fills it in for you
  2. Or compute it as `"sha256:" + hex.EncodeToString(sha256.Sum256(data)[:])` over the exact Data bytes you pass
  3. 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

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


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/e926bea2c6cc7943. Report an issue: GitHub.