affaan-m/ECC · critical · ClaimError
approved text hash does not match
Error message
approved text hash does not match
What it means
After recomputing SHA-256 over the stored draft_text, the library compares it to the stored draft_sha256 using a constant-time comparison. A mismatch means the approved text was altered, corrupted, or the hash refers to different content, so the library refuses to grant any dispatch permission — this is the integrity check protecting against post-approval tampering.
Solutions
- Recompute the hash over the current draft_text and re-record it through the trusted approval writer, or re-approve the (correct) content
- Audit what changed: compare the stored hash to sha256 of the current text and diff against the original approved draft from your approval log
- Never edit approval_bound_drafts.draft_text in place after approval; write a new snapshot instead
- Ensure all writers hash exactly the same bytes that are stored (same encoding, no preprocessing)
Example fix
// before
text = row['draft_text'].replace('\r\n', '\n') # altered after hashing
...
# ClaimError: approved text hash does not match
// after
# re-approve and record a fresh snapshot
text = normalized_text
hash_hex = hashlib.sha256(text.encode('utf-8')).hexdigest()
writer.record_snapshot(oid, did, text, hash_hex) Defensive patterns
Strategy: try-catch
Validate before calling
import hashlib, secrets
def snapshot_intact(db, oid, did) -> bool:
row = db.execute('SELECT draft_text, draft_sha256 FROM approval_bound_drafts WHERE obligation_id=? AND decision_id=?',
(oid, did)).fetchone()
digest = hashlib.sha256(row['draft_text'].encode('utf-8')).hexdigest()
return secrets.compare_digest(digest, row['draft_sha256']) Try / catch
try:
token = claim(db, oid, did, now=ts)
except ClaimError as e:
if 'hash does not match' in str(e):
alert_tampering(oid, did) # do NOT auto-fix; escalate to re-approval
else:
raise Prevention
- Treat approval_bound_drafts as immutable after approval; write new snapshots instead of editing
- Recompute and compare hashes in monitoring jobs to detect drift early
- Ensure every writer hashes exactly the stored bytes (same encoding, no transformations)
- Keep an approval log outside the DB to diff text changes against
When it happens
Trigger: draft_text was modified after the hash was recorded (hand-edit, re-encoding/normalization like newline or encoding changes, whitespace trimming); the hash was computed over different bytes than stored; two rows wrote inconsistent text/hash pairs.
Common situations: A tool rewriting the draft with normalized line endings or encoding after approval; a migration re-encoding text (e.g. adding/removing a BOM); copying rows between databases and updating only one column.
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
- application bundle differs from its bound evidence
- artifact SHA-256 does not match receipt
- artifact SHA-256 mismatch
- Invalid supplied provenance declaration
- receipt source SHA-256 changed after generation
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/68b4d07765d7636e.
Report an issue: GitHub.
Appendix: source
Thrown at skills/operator-approval-loop/references/approval_claims.py:65
raise ClaimError('claim transaction failed; no permission granted') from error
raise
def _snapshot(db, obligation_id, decision_id):
row = db.execute('''SELECT * FROM approval_bound_drafts
WHERE obligation_id=? AND decision_id=?''', (obligation_id, decision_id)).fetchone()
if row is None:
raise ClaimError('a current bound approved draft is required')
try:
digest = hashlib.sha256(row['draft_text'].encode('utf-8')).hexdigest()
except (AttributeError, UnicodeError) as error:
raise ClaimError('approved text must be valid UTF-8 text') from error
stored_digest = row['draft_sha256']
if (not isinstance(stored_digest, str) or len(stored_digest) != 64
or any(character not in '0123456789abcdef' for character in stored_digest)):
raise ClaimError('approved hash must be lowercase SHA-256 hexadecimal')
if not secrets.compare_digest(digest, stored_digest):
raise ClaimError('approved text hash does not match')
return dict(row)
def _claim_row(db, token):
if not isinstance(token, str) or not token:
raise ClaimError('a claim token is required')
row = db.execute('SELECT * FROM obligation_delivery_claims WHERE token=?', (token,)).fetchone()
if row is None:
raise ClaimError('unknown claim token')
return row
def claim(db, obligation_id, decision_id, *, now):
"""Reserve one already-authorized decision; return only a random claim token."""
with _transaction(db, now):
_snapshot(db, obligation_id, decision_id)
token = secrets.token_hex(32)
db.execute('''INSERT INTO obligation_delivery_claimsView on GitHub (pinned to 8321021c54)