{"id":125732,"date":"2026-08-25T21:17:36","date_gmt":"2026-08-25T21:17:36","guid":{"rendered":"https:\/\/make.wordpress.org\/core\/?p=125732"},"modified":"2026-08-25T21:17:37","modified_gmt":"2026-08-25T21:17:37","slug":"proposal-a-secrets-api-for-wordpress-7-2","status":"publish","type":"post","link":"https:\/\/make.wordpress.org\/core\/2026\/08\/25\/proposal-a-secrets-api-for-wordpress-7-2\/","title":{"rendered":"Proposal: A Secrets API for WordPress 7.2"},"content":{"rendered":"<p class=\"wp-block-paragraph\">WordPress has no first-class way to store a credential. This proposal outlines a Secrets <span tabindex='0' class='glossary-item-container'>API<span class='glossary-item-hidden-content'><span class='glossary-item-header'>API<\/span> <span class='glossary-item-description'>An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways.<\/span><\/span><\/span> for 7.2, along with <span tabindex='0' class='glossary-item-container'>WP-CLI<span class='glossary-item-hidden-content'><span class='glossary-item-header'>WP-CLI<\/span> <span class='glossary-item-description'>WP-CLI is the Command Line Interface for WordPress, used to do administrative and development tasks in a programmatic way. The project page is <a href=\"http:\/\/wp-cli.org\/\">http:\/\/wp-cli.org\/<\/a> <a href=\"https:\/\/make.wordpress.org\/cli\/\">https:\/\/make.wordpress.org\/cli\/<\/a><\/span><\/span><\/span> support for it, and explains why the accompanying <span tabindex='0' class='glossary-item-container'>UI<span class='glossary-item-hidden-content'><span class='glossary-item-header'>UI<\/span> <span class='glossary-item-description'>User interface<\/span><\/span><\/span> should wait for 7.3.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why it matters<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Every <span tabindex='0' class='glossary-item-container'>plugin<span class='glossary-item-hidden-content'><span class='glossary-item-header'>Plugin<\/span> <span class='glossary-item-description'>A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory <a href=\"https:\/\/wordpress.org\/plugins\/\">https:\/\/wordpress.org\/plugins\/<\/a> or can be cost-based plugin from a third-party.<\/span><\/span><\/span> that needs an API key today writes it to the options table in plaintext. That is the only available path, so this isn\u2019t a criticism of plugin authors. The consequence is that credentials end up in the same table as the site tagline, and therefore in every database dump, every backup, every staging clone, and every <code>wp option get<\/code> in a shared terminal.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Plugins have been solving this independently for years, each with its own key derivation, its own cipher choice, and its own failure modes. Site Kit encrypts its Google credentials. WooCommerce gateways handle payment keys. SMTP plugins hold mail credentials. Each one is a separate audit surface, and none of them can be reviewed once on behalf of the ecosystem.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This was survivable when the average site held a Mailchimp key and a reCAPTCHA secret. AI service integrations have made credentials both more numerous and far more expensive to lose: a leaked model-provider key is a metered spend liability. The blast radius increased while the storage mechanism stayed the same.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A second gap is that site owners and hosts increasingly need to answer basic operational questions \u2014 which credentials exist on this site, what code is using them, when were they last rotated. None of those are answerable today, because there is no object to ask about. A credential is just a row in a key-value store.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><span tabindex='0' class='glossary-item-container'>Core<span class='glossary-item-hidden-content'><span class='glossary-item-header'>Core<\/span> <span class='glossary-item-description'>Core is the set of software required to run WordPress. The Core Development Team builds WordPress.<\/span><\/span><\/span> has no concept of a secret; there\u2019s nothing for either problem to attach to.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Clarifying priorities<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A tightly-scoped, stable API landed in 7.2 helps push forward on requirements like those documented in <a href=\"https:\/\/core.trac.wordpress.org\/ticket\/64789\">the Security Audit for the Connectors screen<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>No new UI in 7.2.<\/strong> The storage and retrieval semantics are the part that must be right the first time, because every plugin, every <span tabindex='0' class='glossary-item-container'>CLI<span class='glossary-item-hidden-content'><span class='glossary-item-header'>CLI<\/span> <span class='glossary-item-description'>Command Line Interface. Terminal (Bash) in Mac, Command Prompt in Windows, or WP-CLI for WordPress.<\/span><\/span><\/span> command, and every future <span tabindex='0' class='glossary-item-container'>admin<span class='glossary-item-hidden-content'><span class='glossary-item-header'>admin<\/span> <span class='glossary-item-description'>(and super admin)<\/span><\/span><\/span> screen inherits them. A settings screen can be designed properly in 7.3 once there\u2019s a stable API to build against and real usage to design from. The <span tabindex='0' class='glossary-item-container'>hooks<span class='glossary-item-hidden-content'><span class='glossary-item-header'>Hooks<\/span> <span class='glossary-item-description'>In WordPress theme and development, hooks are functions that can be applied to an action or a Filter in WordPress. Actions are functions performed when a certain event occurs in WordPress. Filters allow you to modify certain functions. Arguments used to hook both filters and actions look the same.<\/span><\/span><\/span> and accessors an admin screen would need are in scope now; the screen itself is not.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>WP-CLI is in scope.<\/strong> It gives the API a first-party consumer inside the same release, which is the most reliable way to discover whether the surface is actually usable before it freezes. It also closes a real leak of its own: commands that accept credentials as arguments expose them in shell history and in the process list on shared hosts. Reading and writing through the API fixes that in the same change.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>A <span tabindex='0' class='glossary-item-container'>feature plugin<span class='glossary-item-hidden-content'><span class='glossary-item-header'>Feature Plugin<\/span> <span class='glossary-item-description'>A plugin that was created with the intention of eventually being proposed for inclusion in WordPress Core. See <a href=\"https:\/\/make.wordpress.org\/core\/handbook\/about\/release-cycle\/features-as-plugins\/\">Features as Plugins<\/a><\/span><\/span><\/span> ships before the core <span tabindex='0' class='glossary-item-container'>patch<span class='glossary-item-hidden-content'><span class='glossary-item-header'>patch<\/span> <span class='glossary-item-description'>A special text file that describes changes to code, by identifying the files and lines which are added, removed, and altered. It may also be referred to as a <strong>diff<\/strong>. A patch can be <em>applied<\/em> to a codebase for testing.<\/span><\/span><\/span>.<\/strong> Rather than propose an API and ask people to evaluate it from a design document, I\u2019ll publish the implementation as a plugin first. This will let contributors run the API instead of reading about it, expose the awkward parts of the surface while they\u2019re still inexpensive to change, and give sites on 7.0 and 7.1 something usable immediately rather than waiting for December. The core patch then follows with an API surface identical to the plugin\u2019s; nothing anyone builds against it needs rewriting.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The plugin will prove out the API before we consolidate things into core.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Proposed API<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A small function set, a value object, and always-on encryption.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: php; title: ; notranslate\" title=\"\">\nwp_set_secret( string $name, string $value ): bool|WP_Error\nwp_get_secret( string $name, string $version = WP_Secret_Version::CURRENT ): WP_Secret|null|WP_Error\nwp_delete_secret( string $name ): bool|WP_Error\nwp_import_option_as_secret( string $option, string $name ): bool|WP_Error\n\nWP_Secret::reveal(): string\nWP_Secret::fingerprint(): string\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">At a call site:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: php; title: ; notranslate\" title=\"\">\nwp_set_secret( 'my-plugin\/api-key', $key_from_form );\n\n$secret = wp_get_secret( 'my-plugin\/api-key' );\n\nif ( is_wp_error( $secret ) ) {\n    \/\/ Exists, but unusable. Key material changed. Tell the user.\n} elseif ( null === $secret ) {\n    \/\/ Doesn't exist. Show the connect flow.\n} else {\n    $client = new API_Client( $secret-&gt;reveal() );\n}\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">Supporting surface:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Secrets are namespaced by convention, <code>plugin-slug\/secret-name<\/code>, so a future admin screen can group by owner and cross-namespace access has something to check against.<\/li>\n\n\n\n<li><code>WP_Secret<\/code> masks itself in logs, <code>var_dump()<\/code>, and error output. Getting the raw string requires an explicit <code>-&gt;reveal()<\/code>, which makes every point of use greppable.<\/li>\n\n\n\n<li>Default storage is the options API \u2014 ciphertext blobs, <code>autoload=no<\/code>, excluded from options.php and the REST settings endpoint. The same relationship the object cache has to options: the default substrate, replaceable by a drop-in.<\/li>\n\n\n\n<li>Two <span tabindex='0' class='glossary-item-container'>capabilities<span class='glossary-item-hidden-content'><span class='glossary-item-header'>capability<\/span> <span class='glossary-item-description'>A\u00a0<strong>capability<\/strong>\u00a0is permission to perform one or more types of task. Checking if a user has a capability is performed by the <code>current_user_can<\/code> function. Each user of a WordPress site might have some permissions but not others, depending on their\u00a0role. For example, users who have the Author role usually have permission to edit their own posts (the \u201cedit_posts\u201d capability), but not permission to edit other users\u2019 posts (the \u201cedit_others_posts\u201d capability).<\/span><\/span><\/span>, <code>manage_secrets<\/code> and <code>manage_network_secrets<\/code>.<\/li>\n\n\n\n<li>A change hook fires on write, carrying actor, timestamp, and old and new fingerprints. Fingerprints are readable; values are not.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Design decisions most likely to draw objections follow, with rationale.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Encryption is not optional<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Secrets are encrypted at rest under a master-key envelope: a per-secret data key, wrapped by a master key. There is no plaintext mode and no constant to disable encryption.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The obvious objection is data loss \u2014 a site rotates its salts or migrates hosts, key material changes, and the secrets no longer decrypt. Plaintext options never do that. With an envelope, individual secrets are encrypted under a random master key, and the site key only wraps that master key. Rotation re-wraps a single value (the master key) without touching stored secrets. A dedicated constant is the preferred key source, with a zero-config fallback derived from existing salts for sites that can\u2019t set one. When key material really is gone, the failure is bounded and visible: Site Health reports undecryptable secrets, and the recovery path is to re-enter them. Nothing is silently corrupted.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Site Kit is the empirical case. It derives from <code>LOGGED_IN_KEY<\/code> and <code>LOGGED_IN_SALT<\/code> by default, recommends a dedicated constant, and documents that salt rotation breaks stored credentials \u2014 and it has shipped that way at enormous scale. The ecosystem\u2019s response was to document the approach, not to reject it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rationale:<\/strong> an API that can be configured to store plaintext is an options API with a misleading name. Making encryption unconditional is what lets a plugin author make a claim to their users about how the key is stored. The alternative \u2014 encryption off unless a site opts in \u2014 protects the small number of sites that rotate salts by leaving everyone else in plaintext forever. Sites that would opt in are largely the sites already capable of running a drop-in. Sites that would not are exactly the ones the API exists for.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Availability of cryptographic APIs is not a concern: libsodium has been bundled with <span tabindex='0' class='glossary-item-container'>PHP<span class='glossary-item-hidden-content'><span class='glossary-item-header'>PHP<\/span> <span class='glossary-item-description'>The web scripting language in which WordPress is primarily architected. WordPress requires PHP 7.4 or higher<\/span><\/span><\/span> since 7.2, and core additionally ships <code>sodium_compat<\/code> for the cases where the extension has been disabled at build time. The primitives are present on every supported configuration without a new dependency.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Retrieval returns one of three things<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><code>wp_get_secret()<\/code> returns a <code>WP_Secret<\/code> on success, <code>null<\/code> when the secret does not exist, and <code>WP_Error<\/code> when it exists but could not be retrieved \u2014 wrong key, unavailable keyring, failed decryption.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rationale:<\/strong> collapsing \u201cabsent\u201d and \u201cbroken\u201d into a single false-y value is how you get a site that erroneously re-runs its onboarding flow, or disables an integration, when the real problem is key management. Separating them is also what lets a site distinguish a missing credential from an unauthorized change to key material.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">No <span tabindex='0' class='glossary-item-container'>filter<span class='glossary-item-hidden-content'><span class='glossary-item-header'>Filter<\/span> <span class='glossary-item-description'>Filters are one of the two types of Hooks <a href=\"https:\/\/codex.wordpress.org\/Plugin_API\/Hooks\">https:\/\/codex.wordpress.org\/Plugin_API\/Hooks<\/a>. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output.<\/span><\/span><\/span> on the retrieval path<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">There is no filter applied to a secret on its way out of storage. No endpoint, filter, or <span tabindex='0' class='glossary-item-container'>capability<span class='glossary-item-hidden-content'><span class='glossary-item-header'>capability<\/span> <span class='glossary-item-description'>A\u00a0<strong>capability<\/strong>\u00a0is permission to perform one or more types of task. Checking if a user has a capability is performed by the <code>current_user_can<\/code> function. Each user of a WordPress site might have some permissions but not others, depending on their\u00a0role. For example, users who have the Author role usually have permission to edit their own posts (the \u201cedit_posts\u201d capability), but not permission to edit other users\u2019 posts (the \u201cedit_others_posts\u201d capability).<\/span><\/span><\/span> returns a secret\u2019s value other than the API call itself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rationale:<\/strong> I expect the most pushback here \u2014 a filter on secret retrieval is, by construction, a hook that receives every credential on the site in plaintext. Any plugin could register it, making it a documented interception point. A filter that can intercept a credential is a filter that can steal one. The flexibility WordPress normally gets from filters is provided instead through explicit, replaceable providers, described next.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Two extension points, independently replaceable<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">A drop-in, <code>secrets.php<\/code>, exposes storage and keyring as separate extension points: where ciphertext is stored (Vault, Parameter Store, a host API) and what wraps the master key (a KMS, an HSM). Neither is ever handed a plaintext secret, and neither can turn encryption off.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rationale:<\/strong> these are different concerns and sites have different constraints. Replacing only the keyring while keeping default storage is a reasonable configuration, and so is the inverse. Coupling them into one swap would force an all-or-nothing decision that most sites can\u2019t make, and would push people back to storing credentials in options.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Two version slots, not unbounded history<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><code>CURRENT<\/code> and <code>PREVIOUS<\/code>, modeled as string constants on a <code>final class WP_Secret_Version<\/code> rather than a native enum. Overwriting a secret keeps the old value recoverable, so a mistyped key is fixable. Requests already in flight won\u2019t fail mid-rotation. Retiring the previous slot is an explicit operator action \u2014 no timers, no cron.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rationale:<\/strong> named version history would keep every credential a site has ever held recoverable from a backup indefinitely. Two slots covers the real use case. Retirement is operator-driven because core has no way to know when a third-party integration has finished draining the old value; a timer would just guess wrong on a schedule. Native enums require PHP 8.1, and while 8.3 or better is the recommended version, <a href=\"https:\/\/wordpress.org\/about\/requirements\/\">core\u2019s minimum remains 7.4<\/a> \u2014 so string constants on a <code>final class<\/code> are the portable equivalent.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><span tabindex='0' class='glossary-item-container'>Multisite<span class='glossary-item-hidden-content'><span class='glossary-item-header'>multisite<\/span> <span class='glossary-item-description'>Used to describe a WordPress installation with a network of multiple blogs, grouped by sites. This installation type has shared users tables, and creates separate database tables for each blog (wp_posts becomes wp_0_posts). See also <strong>network<\/strong>, <strong>blog<\/strong>, <strong>site<\/strong><\/span><\/span><\/span> is in v0<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><span tabindex='0' class='glossary-item-container'>Network<span class='glossary-item-hidden-content'><span class='glossary-item-header'>network<\/span> <span class='glossary-item-description'>(versus site, blog)<\/span><\/span><\/span>-level secrets are supported. Salts are network-wide, so distinct option rows would give logical separation and no cryptographic separation; instead a network root key derives per-site subkeys via <code>sodium_crypto_kdf_derive_from_key()<\/code>. Site secrets and network secrets are separate functions with separate capabilities, and there is no implicit fallback from one to the other.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rationale:<\/strong> adding this later would mean introducing a second key hierarchy alongside the first, and migrating between them. The useful consequence of doing it now: rotating the key on a 500-site network re-wraps one value rather than 500.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Import, don\u2019t migrate<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">No automatic sweep of the options table. <code>wp_import_option_as_secret()<\/code> lets a plugin author move one known option deliberately, on their own explicit upgrade schedule \u2014 and flags the imported secret for rotation rather than merely reporting success.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rationale:<\/strong> core cannot reliably tell which options are credentials, and guessing would break sites in ways close to undebuggable. The rotation flag matters just as much: a credential that has been sitting in <code>wp_options<\/code> is already in backups, replicas, and object caches, and encrypting it now does not change that. Rotating is what actually fixes it.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Other decisions worth stating<\/h4>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Strings in, strings out.<\/strong> The API does not serialize. Nothing stops a plugin from <code>json_encode()<\/code>-ing something and storing the result, but core neither encourages it nor unpacks it, so a secret can never expand into an object graph on read.<\/li>\n\n\n\n<li><strong>Fail closed.<\/strong> If an external store or key backend is unreachable, reads and writes return an error. There is no fallback to local storage or local key wrapping.<\/li>\n\n\n\n<li><strong>Nothing plaintext enters the object cache.<\/strong> On shared hosting a persistent object cache is shared infrastructure. <code>WP_Secret<\/code> refuses serialization outright.<\/li>\n\n\n\n<li><strong>No export.<\/strong> Values are write-only. Migrations and staging pushes mean re-entry at the destination, so staging never holds production credentials. Fingerprints let an admin confirm the re-entered value matches.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">What this does and does not do<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The API is a hardening interface. If a secret lives in the WordPress database and a function exists to decrypt it, then any code running as WordPress can obtain that secret.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In scope: database dumps and exfiltration, backups sitting in cloud storage, SQL injection reading <code>wp_options<\/code>, careless disclosure surfaces \u2014 options.php, export files, support screenshots, screen shares, debug logs \u2014 and a compromised read-only replica.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Out of scope: code execution in the WordPress process. Nothing defends against that. Code running inside WordPress can call <code>wp_get_secret()<\/code> for the same reason it can read environment variables or wp-config.php \u2014 the secret has to be usable, so anything that is WordPress can use it. Registering a secret against a plugin slug does not prevent a different plugin from asking for it; slugs aren\u2019t authenticated, and even a perfect allowlist wouldn\u2019t stop code that reads the revealed value out of memory after a legitimate call. This proposal makes no claim of per-plugin isolation, and masked values are hygiene against shoulder-surfing rather than a privilege boundary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What changes is the value of a stolen database, which is the overwhelmingly more common breach.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What it does provide:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Credentials are not sitting in plaintext in a table that gets dumped, cloned, and shared.<\/li>\n\n\n\n<li>Access has one chokepoint, which makes it loggable and auditable for the first time.<\/li>\n\n\n\n<li>Rotation and retirement are defined operations rather than an <code>UPDATE<\/code> and a hope.<\/li>\n\n\n\n<li>The storage extension point lets a host or site owner move secrets out of WordPress \u2014 which is the only configuration where real per-caller enforcement becomes possible.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Prior art<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Site Kit\u2019s credential encryption, <a href=\"https:\/\/core.trac.wordpress.org\/ticket\/64789#comment:1\">linked from comment #1 of the Connectors security audit<\/a>.<\/li>\n\n\n\n<li>The open <span tabindex='0' class='glossary-item-container'>ticket<span class='glossary-item-hidden-content'><span class='glossary-item-header'>ticket<\/span> <span class='glossary-item-description'>Created for both bug reports and feature development on the bug tracker.<\/span><\/span><\/span> and pull request on the <code>options.php<\/code> plaintext display.<\/li>\n\n\n\n<li><a href=\"https:\/\/core.trac.wordpress.org\/ticket\/64789#comment:12\">@dpknauss\u2019s comment on write-side governance<\/a>, which is where the fingerprint and change-notification design came from.<\/li>\n\n\n\n<li><a href=\"https:\/\/github.com\/ericmann\/displace-secrets-manager\">A proof-of-concept plugin I shipped roughly six months ago<\/a> after a discussion about encrypting TOTP tokens for the Two Factor plugin, implementing the master-key envelope, the provider model, and a WP-CLI surface. The <code>wp_<\/code>-prefixed function names here are proposed rather than shipped.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Timeline<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/make.wordpress.org\/core\/2026\/08\/12\/wordpress-7-2-call-for-volunteers\/\">Beta 1 for 7.2 is October 20\u201322<\/a>, which is the practical deadline for an API landing in this release. Working backward:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Now through mid-September:<\/strong> feedback on this proposal, particularly the questions below. Feature plugin published for testing.<\/li>\n\n\n\n<li><strong>Late September:<\/strong> patch on <span tabindex='0' class='glossary-item-container'>Trac<span class='glossary-item-hidden-content'><span class='glossary-item-header'>Trac<\/span> <span class='glossary-item-description'>An open source project by Edgewall Software that serves as a bug tracker and project management tool for WordPress.<\/span><\/span><\/span>, with the API surface matching the plugin.<\/li>\n\n\n\n<li><strong>Before <span tabindex='0' class='glossary-item-container'>Beta<span class='glossary-item-hidden-content'><span class='glossary-item-header'>Beta<\/span> <span class='glossary-item-description'>A pre-release of software that is given out to a large group of users to trial under real conditions. Beta versions have gone through alpha testing in-house and are generally fairly close in look, feel and function to the final product; however, design changes often occur as part of the process.<\/span><\/span><\/span> 1:<\/strong> committed, or explicitly deferred to 7.3 rather than rushed in.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">I\u2019ve volunteered for technical contributions to 7.2 and intend to do this work rather than propose it and hand it off.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Feedback wanted<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Objections now, while the surface is still cheap to change. Specifically:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>The no-filter decision on retrieval.<\/strong> Providers are meant to cover what a filter would normally give you \u2014 is that substitution sufficient for the cases you\u2019d actually need to hook?<\/li>\n\n\n\n<li><strong>Two version slots.<\/strong> Is <code>CURRENT<\/code>\/<code>PREVIOUS<\/code> sufficient, or is there a rotation pattern that genuinely needs more?<\/li>\n\n\n\n<li><strong>Import, not migrate.<\/strong> Plugin authors: does <code>wp_import_option_as_secret()<\/code> fit how you would actually move an existing key, or does it need a different shape?<\/li>\n\n\n\n<li><strong>WP-CLI surface.<\/strong> Which commands most need this, and in what order?<\/li>\n\n\n\n<li><strong>Hosts.<\/strong> If you run a real secret store or a real key backend and the drop-in surface is missing something you\u2019d need, that\u2019s the most useful feedback available \u2014 and much easier to add before anything ships than after.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Any feedback is welcome \u2014 please share your thoughts in the comments below.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Props to <a href=\"https:\/\/profiles.wordpress.org\/jeffpaul\/\" class=\"mention\"><span class=\"mentions-prefix\">@<\/span>jeffpaul<\/a>, who talked me into building the original proof of concept six months ago, and <a href=\"https:\/\/profiles.wordpress.org\/whyisjake\/\" class=\"mention\"><span class=\"mentions-prefix\">@<\/span>whyisjake<\/a> for shepherding this proposal.<\/p>\n<p class=\"o2-appended-tags\"><a href=\"https:\/\/make.wordpress.org\/core\/tag\/proposal\/\" class=\"tag\"><span class=\"tag-prefix\">#<\/span>proposal<\/a><\/p><nav class='o2-post-footer-actions'><ul class='o2-post-footer-action-row'><li class='o2-post-footer-action'><a href=\"https:\/\/login.wordpress.org\/?redirect_to=https%3A%2F%2Fmake.wordpress.org%2Fcore%2F2026%2F08%2F25%2Fproposal-a-secrets-api-for-wordpress-7-2%2F%23respond&#038;locale=en_US\" title=\"Login to Reply\"  class=\"genericon  genericon-reply\"  data-action=\"login-to-reply\"  data-actionstate=\"default\" >Login to Reply<\/a><\/li><\/ul><div class='o2-post-footer-action-likes'><\/div><ul class='o2-post-footer-action-row'><\/ul><\/nav>","protected":false},"excerpt":{"rendered":"<p>WordPress has no first-class way to store a credential. This proposal outlines a Secrets APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. for 7.2, along with WP-CLIWP-CLI WP-CLI is the Command Line Interface for WordPress, used [&hellip;]<\/p>\n","protected":false},"author":295019,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"advanced_seo_description":"","jetpack_seo_html_title":"","jetpack_seo_noindex":false,"jetpack_seo_schema_type":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1175],"tags":[1445],"class_list":["post-125732","post","type-post","status-publish","format-standard","hentry","category-general","tag-proposal","mentions-ericmann","mentions-jazzs3quence","mentions-jeffpaul","mentions-rmccue","mentions-whyisjake","author-ericmann"],"revision_note":"","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p2AvED-wHW","jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/make.wordpress.org\/core\/wp-json\/wp\/v2\/posts\/125732","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/make.wordpress.org\/core\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/make.wordpress.org\/core\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/make.wordpress.org\/core\/wp-json\/wp\/v2\/users\/295019"}],"replies":[{"embeddable":true,"href":"https:\/\/make.wordpress.org\/core\/wp-json\/wp\/v2\/comments?post=125732"}],"version-history":[{"count":2,"href":"https:\/\/make.wordpress.org\/core\/wp-json\/wp\/v2\/posts\/125732\/revisions"}],"predecessor-version":[{"id":125734,"href":"https:\/\/make.wordpress.org\/core\/wp-json\/wp\/v2\/posts\/125732\/revisions\/125734"}],"wp:attachment":[{"href":"https:\/\/make.wordpress.org\/core\/wp-json\/wp\/v2\/media?parent=125732"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/make.wordpress.org\/core\/wp-json\/wp\/v2\/categories?post=125732"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/make.wordpress.org\/core\/wp-json\/wp\/v2\/tags?post=125732"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}