Compatibility guide

Why your WordPress password hash starts with $wp$2y$.

You opened wp_users and the value in user_pass does not look like the $P$ hashes in every tutorial. That is expected, and nothing is wrong with your site.

01It is the official format.

WordPress 6.8 and later store new passwords as $wp$2y$ values. It is not malware and not a plugin artifact.

02It is bcrypt with a twist.

Core pre-hashes the password with HMAC-SHA-384 before bcrypt, then marks the result with $wp.

03Do not "fix" it.

Replacing it with an old-style hash works as a reset, but converting formats by editing the string never does.

What the prefix is telling you

A stored WordPress password like $wp$2y$10$abcdefghijklmnopqrstuv… is three claims packed into one string. The leading $wp says "WordPress Core produced this value using its own pre-hashing step." The $2y$ that follows is the standard bcrypt identifier, exactly as PHP's password_hash() writes it. The 10 after that is the bcrypt cost. Strip the $wp off and what remains is a syntactically valid bcrypt hash — but one whose input was not your password directly.

That last point is what trips people up. Before bcrypt runs, WordPress computes an HMAC-SHA-384 of the password (keyed with the fixed string wp-sha384) and Base64-encodes the result. bcrypt then hashes that value. Core does this to work around bcrypt's 72-byte input limit so long passphrases keep their full strength.

Why you may be seeing it for the first time

WordPress switched its default hasher from portable phpass to bcrypt in version 6.8. Existing $P$ hashes were not rewritten during the upgrade — a stored hash cannot be converted without knowing the password. Instead, WordPress rehashes each account the next time that user successfully signs in. So a database migrated through the 6.8 boundary usually holds a mix: $wp$2y$ for accounts that have logged in since the upgrade, $P$ for those that have not. Both verify correctly. The mix is not an inconsistency to clean up.

If you need to identify every format you might encounter in user_pass — including plain bcrypt and legacy MD5 — the hash formats reference covers the full matrix.

What this means when you work with the database

Two practical consequences follow from the pre-hashing step:

First, a generic bcrypt tool cannot produce or verify a WordPress $wp$2y$ value. If you bcrypt the password itself and paste the result with a $wp prefix glued on, login will fail — the stored value must be bcrypt of the SHA-384 material, not of the password. The modern hash generator performs the full Core sequence locally in your browser, and the verifier checks a candidate password the same way wp_check_password() does.

Second, rolling a site back to WordPress 6.7 or earlier strands these hashes: old Core does not recognize the $wp prefix, so every account that was rehashed after the upgrade can no longer sign in until its password is reset. Factor that into any downgrade plan.

If you were worried it was a compromise

  1. Confirm the site is WordPress 6.8 or newer — if so, $wp$2y$ is simply the current default.
  2. A hash you cannot explain is only suspicious in an older WordPress, or when an account's hash changed without a login or reset.
  3. If you still suspect tampering, reset the affected passwords through WordPress or WP-CLI and review recent changes with an activity log.
  4. Never paste a production hash into an online cracker "to check it" — treat hashes as secrets.

Quick answers

Is a $wp$2y$ password hash a sign of malware?

No. It is the standard format WordPress 6.8 and later write for every new or rehashed password. A compromised site is indicated by hashes changing without a corresponding login or reset, not by this prefix.

Why do some users still have $P$ hashes on the same site?

WordPress cannot convert a stored hash without the password, so old phpass values remain until each user signs in again. A mixed table is normal after an upgrade and both formats keep working.

Can I verify a $wp$2y$ hash with a normal bcrypt tool?

Not directly. WordPress bcrypts an HMAC-SHA-384 of the password rather than the password itself, so a generic bcrypt check will always report a mismatch. Use a tool that implements the WordPress sequence, such as the local hash verifier.

What happens to these hashes if I downgrade below WordPress 6.8?

Older WordPress does not recognize the $wp prefix, so affected accounts cannot log in after a downgrade. Their passwords must be reset through the recovery flow or WP-CLI.

Primary references: wp_hash_password() and wp_check_password().