affaan-m/ECC · error · ClaimError
approved hash must be lowercase SHA-256 hexadecimal
Error message
approved hash must be lowercase SHA-256 hexadecimal
What it means
Each approval snapshot stores the expected SHA-256 of the draft in draft_sha256, and this library requires it to be exactly a 64-character lowercase hex string. Malformed hashes mean the stored approval metadata cannot be trusted, so the library refuses to verify or dispatch.
Solutions
- Store hashlib.sha256(draft_text.encode('utf-8')).hexdigest() (lowercase, 64 chars) when writing the snapshot
- Normalize an existing wrong hash: recompute it from draft_text and UPDATE the row
- Check the row: SELECT draft_sha256 FROM approval_bound_drafts ... and validate len==64 and all chars in 0-9a-f before calling
- Fix the writing tool/migration to always produce lowercase SHA-256 hex
Example fix
// before
digest = hashlib.sha256(text.encode()).hexdigest().upper()
db.execute('UPDATE approval_bound_drafts SET draft_sha256=? ...', (digest,))
// after
digest = hashlib.sha256(text.encode('utf-8')).hexdigest() # lowercase 64-hex
db.execute('UPDATE approval_bound_drafts SET draft_sha256=? ...', (digest,)) Defensive patterns
Strategy: validation
Validate before calling
import re
HEX64 = re.compile(r'^[0-9a-f]{64}$')
def hash_ok(db, oid, did) -> bool:
row = db.execute('SELECT draft_sha256 FROM approval_bound_drafts WHERE obligation_id=? AND decision_id=?',
(oid, did)).fetchone()
return isinstance(row['draft_sha256'], str) and HEX64.fullmatch(row['draft_sha256']) is not None Type guard
def is_sha256_hex(v) -> bool:
return isinstance(v, str) and len(v) == 64 and all(c in '0123456789abcdef' for c in v) Try / catch
try:
token = claim(db, oid, did, now=ts)
except ClaimError as e:
if 'lowercase SHA-256 hexadecimal' in str(e):
fix_hash_from_text(db, oid, did) # recompute and UPDATE via trusted writer
token = claim(db, oid, did, now=ts)
else:
raise Prevention
- Always write hashes with hashlib.sha256(text.encode('utf-8')).hexdigest()
- Add a CHECK constraint or writer-side validator for 64-char lowercase hex
- Never uppercase, prefix, or truncate digests
- Audit snapshot rows with the is_sha256_hex guard before dispatch runs
When it happens
Trigger: draft_sha256 stored as None, an int, an uppercase-hex digest, a digest with '0x' prefix, or any string whose length != 64 or containing non-[0-9a-f] characters; calling claim()/begin_dispatch() against such a row.
Common situations: A custom writer using hexdigest().upper() or hashing with SHA-1/MD5 (40/32 chars); storing the hash as bytes or a BLOB; hand-edited rows or a schema migration that reformatted the column.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- now must be a nonnegative integer
- receipt has an invalid source identity
- receipt source SHA-256 changed after generation
- resolved SHA-256 required
- rule lacks immutable reference evidence
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/6833d0b294dc4ab2.
Report an issue: GitHub.
Appendix: source
Thrown at skills/operator-approval-loop/references/approval_claims.py:63
db.rollback()
if isinstance(error, sqlite3.Error):
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)View on GitHub (pinned to 8321021c54)