Where WordPress 7.0 stores your AI API keys - and how to keep them safe
Core got the lookup order right and the default wrong, and every plugin on your site can read the key.

A couple of days back, I was reading Jeff Trumble’s post about which AI assistant one should pick for WordPress 7.0. It came to me through Bridget Willard’s tweet and I was mostly curious to see whether he’d recommend running a local model - only to reach the security section and stop right there.
Now, before I say anything - his advice is absolutely right. Use a unique key for each provider, set spending caps, rotate your keys every quarter, check which plugins have access, keep an eye on the usage graphs. If you have AI connected on your site and you’re not doing these things, please go and do them today.
But every single item on that list is you, doing manual work, because of where the key is being stored. And that is the part which bothered me. You only rotate a key every quarter when you have already accepted that it can leak, right? I’ve been with WordPress for about 20 years and I owe most of my career to it, however the last eight years or so have been mostly Drupal and mostly enterprise clients, and one thing that work teaches you very fast is that secrets handling is the platform’s job. The moment it turns into a checklist for the poor person running the site, you’ve already lost!
The line in the dev note
It wasn’t CVE-2025-11749, the AI Engine flaw from November 2025 which leaked bearer tokens through the REST API on a plugin running on 100,000+ sites. It wasn’t the launch day bug where browser autofill (of all things!) would helpfully show your key again in the setup form. But those are bugs - and bugs get fixed!
It was one line in core’s own Connectors API dev note, describing what happens to your key once you save it -
It’s the site setting, so every plugin can access it.
Yes, you read it right! This is not a bug and it is not something anyone forgot about. It’s written in the documentation as the expected behaviour. So you add your Anthropic key because you want the AI features working, and that abandoned gallery plugin from 2019, the one you keep meaning to delete but never do, can read the very same key. There is no permission check sitting in between and that is because nobody built one.
Think about what that actually means. It means the key you added for one feature is readable by every plugin you have ever installed. It means that half abandoned plugin which hasn’t seen an update since 2019 is holding the keys to your AI billing. Yes, EVERY plugin!
Oliver Sild from Patchstack said it better than I can - “WordPress 7.0 combined with plugin vulnerabilities = free AI tokens.” And if you go through Patchstack’s 2026 report it gets worse. The median time to mass exploitation of a high impact WordPress vulnerability is five hours, and 46% of plugin vulnerabilities don’t even have a patch available on the day they are made public.
An AI provider key is also not like your site password. If somebody steals your admin password, they mess up your site and you’ll know about it soon enough. If somebody steals your AI key, they quietly run up a bill on your account and you might not notice it for weeks!
How the Connectors API finds your key
Now, most of the write-ups I read skipped this part - and so did I on my first read!
The Connectors API looks for your key in three places, in this order -
- Environment variable, something like
ANTHROPIC_API_KEY - PHP constant, so
define( 'ANTHROPIC_API_KEY', 'sk-...' );in yourwp-config.php - And finally your database, as an option named
connectors_{$provider_type}_{$provider_id}_api_key
That’s the right order! Whoever designed this truly knew what they were doing, because it means a properly set up WordPress site never has to keep a provider key in MySQL at all. So if you run your own servers, you can stop reading after the next section and go set an environment variable.
Why your key ends up in wp_options
But this order only helps you if you can reach those first two options.
To set an environment variable you need shell access. To edit wp-config.php you need SFTP and the confidence to break your site at 11pm (we’ve all been there). And W3Techs puts WordPress at roughly 41% of the entire web (at the time of writing this post), where a huge chunk of that is shared hosting and the owner has a cPanel login and has never had any reason to open a terminal. So core can’t really assume anything else - because democratizing publishing means the feature has to work on one Linux box with PHP and MySQL and nothing else. That constraint is real and I don’t think it is laziness at all!
It does mean the admin screen is the only option most site owners will ever see, and that admin screen writes your key into wp_options in plain text.
The dev note doesn’t hide this either - “API keys stored in the database are not encrypted but are masked in the user interface.” Masked, err, sorry, covered up with dots on one screen. Open up options.php, that all settings page which WordPress has kept out of the admin menu for most of its life, and there is your key sitting in full! Anybody who can read wp_options has got it. An SQL injection in some completely unrelated plugin, a backup which isn’t encrypted, or a database dump somebody copied to a staging server and then forgot about.
Encrypting these values before they go into the database, so that a stolen copy is useless, is Trac ticket #64789. Yes, it’s open, it’s sitting there planned for some future release, there is a pull request looking at Sodium based encryption (libsodium). So the AI features have shipped and the storage they depend on is still a proposal!
Secrets management in Drupal
Let me be careful here. I spent most of my career telling everybody that WordPress is the best CMS on the planet - loudly, and sometimes to people who hadn’t even asked - so now that I mostly work in Drupal I really don’t want to go running to the other side and start throwing stones at something which gave me my career and most of my friends.
Drupal core doesn’t ship secrets management either. What Drupal has is the Key module in contrib (not core, mind you) - which has been the standard answer for years, and it exists because enterprise clients simply would not sign off without it. What Key does is treat your secret as a thing of its own, with a pluggable provider sitting behind it. So the same key can live in configuration on your laptop, or in a file outside the webroot, or in an environment variable, or in something like HashiCorp Vault or Azure Key Vault. Your module asks for the key by name and it never finds out where the thing actually lives.
And that is the part which matters here. The way you ask for the key never changes, so the storage behind it can be as strict as your compliance team wants and nobody has to touch a single line of module code. Moving a client from config storage to Vault becomes a deployment change and not a development project!
WordPress now has half of this and it’s genuinely good. It has the lookup order, and developers using the AI Client don’t have to handle credentials at all, which is so much better than every plugin inventing its own options row like we used to do. What’s missing is the provider part underneath.
The plugin which might just fix all of this
Now here is the part which got me really excited!
While going through that encryption pull request, I noticed it mentions Displace Secrets Manager by Eric Mann - and this is pretty much the secrets API which WordPress has always needed. Its own README says it plainly - “Every WordPress plugin that connects to an external service stores API keys, tokens, and credentials in the wp_options table - in plaintext.”
It gives you get_secret() and set_secret(), plus WP-CLI commands (wp secret set, wp secret get). Out of the box your secrets go into an encrypted options provider. But the part which matters is the Secrets_Provider interface, because that is what lets somebody plug in AWS KMS, or Vault, or whatever your compliance team has standardised on.
If that sounds familiar, it should! That is Drupal’s Key module, built for WordPress.
Now I have to be honest here - the plugin is early. 17 commits, wants WordPress 6.9+ and PHP 7.2+, and it isn’t coordinated with core in any way. So please don’t drop it on a client site this afternoon! However the shape of it is exactly right and I’ll be watching this one closely. Eric writes about this stuff at eric.mann.blog as well, worth following if WordPress security is your thing.
And if it is Vault you want specifically, Mohamed Imam has written up how he wires HashiCorp Vault into WordPress. It’s about database credentials rather than AI keys, however he uses the right parts of Vault - the database secrets engine handing out temporary credentials on a 5 hour TTL, and rotating the root password so Vault owns it and nobody else does.
But please don’t lift his script as it is. The Vault token sits hardcoded in the PHP file, right there in the article, so your secret has simply moved from one file on the server to another file on the same server. You are back where you started! If you’re on EC2 like he is, use the AWS IAM auth method so the instance identity becomes the credential, or AppRole elsewhere. The other bit which worries me is that the script rewrites wp-config.php with file_put_contents on a cron, and WordPress reads that file on every single request - so a visitor landing while it is half written gets a broken site. Write to a temp file and rename it, at least then it is atomic.
So can you do the same thing for your AI key?
Yes, and it fits better here than it does for database credentials! If you can get the key into an environment variable or into a constant in wp-config.php, then it just works, with no plugin code at all. Core goes looking in those two places first and finds it.
One difference matters though. Vault’s database engine creates fresh credentials whenever they are asked for, however your Anthropic or OpenAI key is one long lived string and nobody is generating those on the fly, so it would sit in Vault’s KV engine instead. What Vault gives you then is storage outside the site, access control and an audit trail - it is not going to rotate the key for you.
Three ways to wire it up, best one first -
- Write a Vault backed provider for Eric’s plugin using that
Secrets_Providerinterface. The key gets fetched when it is needed and never lands on your disk at all. - Have Vault Agent render an environment file for PHP-FPM, so the key becomes an environment variable and core finds it first. One warning - check
clear_envin your FPM pool config, because it defaults toyesand will happily wipe the environment before PHP ever sees your variable. It is an easy hour to lose. - Or write the constant into
wp-config.phpthe way Mohamed did, with the two fixes above. Most fragile of the three, however it needs the least new setup and gets you off plaintext today.
Securing your own site - what I’d do first
Do work through Rocket.net’s list - it’s a good one. But these are the things I’d do first -
- Get that key out of your database. Set an environment variable if your host gives you one, and if it doesn’t, then put a constant in
wp-config.php. After that go and check whether the old option row is still sitting in there, because setting the constant only changes where core looks - it doesn’t clean up what the admin screen already saved. - Have a look through
options.phpfor old keys which you’ve forgotten about. Those older AI plugins each built their own storage and moving to Connectors won’t have cleaned any of it up for you. - Set a hard spending cap at the provider and not a billing alert. An alert only tells you after the money is gone.
- Tie the key down to your server’s IP, if your provider offers it. OpenAI has IP allowlisting and a request from any other address gets rejected even with a perfectly valid key. On Google it’s under application restrictions on the Cloud Console credentials screen, and on Bedrock it’s an
aws:SourceIpcondition in the IAM policy. I couldn’t find the same at key level on Anthropic, so do check first. One warning - this works beautifully on a VPS with a fixed IP, however on shared hosting your outbound IP might be shared with strangers or change without notice, so ask your host! - Treat your staging database like it is production. Most of the leaks I have seen were not clever attacks at all, they were a copy of the live database sitting somewhere with a weaker password.
- Go and look at what is actually installed. Since every plugin can read every key, your key is only as safe as your worst maintained plugin.
Notes for plugin developers
Use the AI Client and let core handle the credentials. Please don’t add your own settings field for a provider key because it feels friendlier, and please don’t copy the resolved key into your own option row as well, because that quietly breaks the lookup order for everybody who set it up properly. And if you need some credential core doesn’t cover, read from a constant or an environment variable first and fall back to the database only if you really have to. It is about six lines of code - and it means those site owners who do know what they’re doing can protect themselves!
A provider interface for core
Encrypting the key in the database is worth doing - but it will not fix this. WordPress has to read that key to call the API, so whatever unlocks it is sitting right there on the same server. A plugin which gets compromised doesn’t have to break any encryption, it simply asks core for the key the way it always did. Encryption saves you from somebody who steals your database, however it does nothing about the code already running inside your site.
What would fix it is a provider interface, so the storage becomes pluggable the way Drupal did it, and some way of knowing which plugin is doing the asking. Because a key you added for AI generation has no business being readable by a contact form plugin, just because the two of them happen to share a database!
And notice where that interface has turned up. Not in core, but in contrib, in Eric Mann’s plugin - which is exactly where Drupal’s Key module lives as well. Maybe that is simply how these things get solved, somebody builds it outside, everybody starts using it, and core catches up afterwards. If core does pick it up, every managed host can put a real secret store behind it and 41% of the web gets this by default, without one single site owner ever reading a checklist.
So don’t wait for core. Go and move your key into an environment variable today. It takes two minutes! And it is the only thing on any of these lists which removes the problem instead of managing it.
So, is your key still sitting in wp_options? Go and check.