{"record":{"id":"91300caf0de6c1cf","repo":"redis/redis-py","slug":"hashed-password-i-must-be-prefixed-with-a-to","errorCode":null,"errorMessage":"Hashed password {i} must be prefixed with a \"+\" to add or a \"-\" to remove","messagePattern":"Hashed password (.+?) must be prefixed with a \"\\+\" to add or a \"-\" to remove","errorType":"exception","errorClass":"DataError","httpStatus":null,"severity":"error","filePath":"redis/commands/core.py","lineNumber":513,"sourceCode":"                    pieces.append(b\"<%s\" % password[1:])\n                else:\n                    raise DataError(\n                        f\"Password {i} must be prefixed with a \"\n                        f'\"+\" to add or a \"-\" to remove'\n                    )\n\n        if hashed_passwords:\n            # as most users will have only one password, allow remove_passwords\n            # to be specified as a simple string or a list\n            hashed_passwords = list_or_args(hashed_passwords, [])\n            for i, hashed_password in enumerate(hashed_passwords):\n                hashed_password = encoder.encode(hashed_password)\n                if hashed_password.startswith(b\"+\"):\n                    pieces.append(b\"#%s\" % hashed_password[1:])\n                elif hashed_password.startswith(b\"-\"):\n                    pieces.append(b\"!%s\" % hashed_password[1:])\n                else:\n                    raise DataError(\n                        f\"Hashed password {i} must be prefixed with a \"\n                        f'\"+\" to add or a \"-\" to remove'\n                    )\n\n        if nopass:\n            pieces.append(b\"nopass\")\n\n        if categories:\n            for category in categories:\n                category = encoder.encode(category)\n                # categories can be prefixed with one of (+@, +, -@, -)\n                if category.startswith(b\"+@\"):\n                    pieces.append(category)\n                elif category.startswith(b\"+\"):\n                    pieces.append(b\"+@%s\" % category[1:])\n                elif category.startswith(b\"-@\"):\n                    pieces.append(category)\n                elif category.startswith(b\"-\"):","sourceCodeStart":495,"sourceCodeEnd":531,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/commands/core.py#L495-L531","documentation":"Raised by Redis.acl_setuser() when an entry in the `hashed_passwords` list does not begin with '+' (add) or '-' (remove). Like plain passwords, each SHA-256 hashed password must be prefixed to indicate add vs remove. The library inspects the first byte of the encoded entry and rejects missing prefixes.","triggerScenarios":"Calling client.acl_setuser('alice', hashed_passwords=['5e88...']) with no prefix, or with a wrong prefix. Hashed passwords are SHA-256 hex strings of the cleartext password and must still carry '+'/'-' on the client side.","commonSituations":"Precomputing SHA-256 hashes and forgetting the prefix; porting hashes from redis-cli ACL SETUSER syntax that uses '#'/'!' separators (those are added internally by the client based on '+'/'-').","solutions":["Prefix every hashed_passwords entry with '+' to add or '-' to remove.","Generate the hash correctly: hashlib.sha256(password.encode()).hexdigest(), then prepend '+' or '-'.","If migrating from plain passwords, switch to the `passwords` argument instead and let Redis hash them server-side."],"exampleFix":"# before\nimport hashlib\nh = hashlib.sha256(b'secret').hexdigest()\nclient.acl_setuser('alice', hashed_passwords=[h])\n# after\nclient.acl_setuser('alice', hashed_passwords=['+' + h])","handlingStrategy":"validation","validationCode":"import hashlib\ndef make_hashed_password(cleartext: str, remove: bool = False) -> str:\n    h = hashlib.sha256(cleartext.encode()).hexdigest()\n    return ('-' if remove else '+') + h\n\ndef normalize_hashed(items):\n    out = []\n    for h in list_or_args(items, []):\n        if not (h.startswith('+') or h.startswith('-')):\n            h = '+' + h\n        out.append(h)\n    return out","typeGuard":"def is_prefixed_hashed(h) -> bool:\n    return isinstance(h, str) and len(h) > 1 and h[0] in '+-'","tryCatchPattern":"from redis.exceptions import DataError\ntry:\n    client.acl_setuser('alice', hashed_passwords=hashed)\nexcept DataError as e:\n    if 'must be prefixed' in str(e):\n        hashed = ['+' + h if not h[:1] in '+-' else h for h in hashed]\n        client.acl_setuser('alice', hashed_passwords=hashed)\n    else:\n        raise","preventionTips":["Always prefix hashed password entries with '+'/'-'.","Prefer the `passwords` argument to let Redis hash server-side when possible.","Generate hashes with a helper that prepends the prefix automatically."],"tags":["acl","validation","acl-setuser","hashed-passwords","input-validation","prefix"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}