# ValueError: Invalid salt

TL;DR: you handed bcrypt.checkpw something that is not a valid bcrypt hash, usually the plaintext password itself or a truncated stored hash. Make sure the second argument is the full stored hash (60 chars, starts with the bcrypt version prefix) and both arguments are bytes.

```text
ValueError: Invalid salt
```

## The fix

1. Inspect what you are passing:
```python
print(repr(stored_hash))
```
Success check: it looks like `$2b$12$...` and is 60 characters long.

2. Common bug, both arguments are the plaintext:
```python
bcrypt.checkpw(candidate.encode(), stored_hash)
```
not `checkpw(candidate, candidate)`.

3. If hashes are truncated in the database, the column is too short. bcrypt hashes need at least 60 characters; widen the column and re-hash affected rows.

4. Verify with a known pair:
```
python -c "import bcrypt; h=bcrypt.hashpw(b'pw', bcrypt.gensalt()); print(bcrypt.checkpw(b'pw', h))"
```
Expected output: True.

Agent? Search Vectle for more: `curl -s 'https://vectle.com/api/v1/search?q=bcrypt+ValueError+Invalid+salt'`

## When this applies
- checkpw raises it but hashpw works fine.
- Stored hashes look short, mangled, or are plaintext.
- Migrating hashes between systems or databases.

## When it does NOT apply
- `password cannot be longer than 72 bytes`, which is the input-length rule.
- `Unicode-objects must be encoded`, which is the bytes-vs-str rule.

## Why this happens
bcrypt parses the salt out of the stored hash string itself. If the string is not a real bcrypt hash, there is no salt to parse and it raises instead of returning False. This is deliberately loud: silently returning False on corrupt data would hide storage bugs.

## Compatibility
All bcrypt versions. The 60-character modular crypt format has been stable for years.