{"record":{"id":"b68aaacba5a9f288","repo":"hashicorp/terraform","slug":"the-remote-state-does-not-match-the-expected-hash","errorCode":null,"errorMessage":"The remote state does not match the expected hash","messagePattern":"The remote state does not match the expected hash","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/consul/client.go","lineNumber":120,"sourceCode":"\t\t\t}\n\t\t\tpayload = append(payload, pair.Value[:]...)\n\t\t}\n\t} else {\n\t\tpayload = pair.Value\n\t}\n\n\t// If the payload starts with 0x1f, it's gzip, not json\n\tif len(payload) >= 1 && payload[0] == '\\x1f' {\n\t\tpayload, err = uncompressState(payload)\n\t\tif err != nil {\n\t\t\treturn nil, diags.Append(err)\n\t\t}\n\t}\n\n\tmd5 := md5.Sum(payload)\n\n\tif hash != \"\" && fmt.Sprintf(\"%x\", md5) != hash {\n\t\treturn nil, diags.Append(fmt.Errorf(\"The remote state does not match the expected hash\"))\n\t}\n\n\treturn &remote.Payload{\n\t\tData: payload,\n\t\tMD5:  md5[:],\n\t}, diags\n}\n\nfunc (c *RemoteClient) Put(data []byte) tfdiags.Diagnostics {\n\t// The state can be stored in 4 different ways, based on the payload size\n\t// and whether the user enabled gzip:\n\t//  - single entry mode with plain JSON: a single JSON is stored at\n\t//\t  \"tfstate/my_project\"\n\t//  - single entry mode gzip: the JSON payload is first gziped and stored at\n\t//    \"tfstate/my_project\"\n\t//  - chunked mode with plain JSON: the JSON payload is split in pieces and\n\t//    stored like so:\n\t//       - \"tfstate/my_project\" -> a JSON payload that contains the path of","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/backend/remote-state/consul/client.go#L102-L138","documentation":"After assembling the payload (and gunzipping if it starts with 0x1f) RemoteClient.Get computes md5(payload) and compares it against the current-hash stored in the chunked manifest. A mismatch means the bytes retrieved from Consul are not the bytes that were written — corruption or external modification.","triggerScenarios":"RemoteClient.Get with hash != \"\" (chunked mode) and fmt.Sprintf(\"%x\", md5.Sum(payload)) != hash.","commonSituations":"An external process modified the state key or a chunk key; a partial overwrite left chunks from one hash and a manifest from another; a Consul snapshot was restored from inconsistent point-in-time state; mixing gzip=true with gzip=false across Terraform versions or runs.","solutions":["Confirm the mismatch: re-read the manifest and recompute the md5 of the concatenated chunk payloads (gunzip first if gzip=true).","Restore the whole state prefix from a Consul snapshot taken when the hash agreed.","If you intentionally rewrote chunks (e.g. a migration), also update the manifest current-hash to match.","Audit Consul KV for external writers on the prefix and tighten ACLs."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before using state, recompute and compare the hash yourself.\nfunc verifyStateHash(client *consulapi.Client, statePath string, gzip bool) error {\n    pair, _, err := client.KV().Get(statePath, nil)\n    if err != nil || pair == nil {\n        return err\n    }\n    var manifest struct {\n        CurrentHash string   `json:\"current-hash\"`\n        Chunks      []string `json:\"chunks\"`\n    }\n    if json.Unmarshal(pair.Value, &manifest); manifest.CurrentHash == \"\" {\n        return nil // not chunked\n    }\n    var payload []byte\n    for _, c := range manifest.Chunks {\n        p, _, err := client.KV().Get(c, nil)\n        if err != nil || p == nil {\n            return fmt.Errorf(\"chunk %s missing\", c)\n        }\n        payload = append(payload, p.Value...)\n    }\n    if gzip && len(payload) > 0 && payload[0] == '\\x1f' {\n        payload, _ = uncompressState(payload)\n    }\n    if got := fmt.Sprintf(\"%x\", md5.Sum(payload)); got != manifest.CurrentHash {\n        return fmt.Errorf(\"hash mismatch: manifest=%s recomputed=%s\", manifest.CurrentHash, got)\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Do not edit state keys out-of-band; always go through Terraform.","Keep gzip setting stable across runs on the same workspace.","Restore from snapshots, not by hand-editing KV, after incidents.","Audit ACLs to prevent rogue writers on the state prefix."],"tags":["consul","backend","state-corruption","integrity","md5"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}