Store a secret
Prompt:
The backend needs my weather API key. Keep it for the app without me pasting it into this chat.
Also works:
- "Save the Anthropic key from my .env.local for this project."
- "The key I stored last month leaked. Replace it."
- "Which keys does this account already hold?"
- "Stripe gave me a webhook signing secret. Make it available to the backend."
- "I stored that key under the wrong name. Remove it."
What your tool does
A stored value reaches your code in one of two ways. The egress gateway adds a declared upstream's key to each of your calls to that upstream, so your code never has the key. A value the manifest's settings bind enters your container as that setting (environment variable).
With nobody present, a tool supplies only a value nobody chooses: generate creates it, or, where it cannot, the tool writes, uses, and deletes a file in one step.
- Reads the platform's
store-keyskill: name the secret, callstore_secretwith no value, then run the command line it returns, or give it to you. The value never goes into code, the manifest, a committed file, or a tool call: the tools take no value. - Chooses how the value reaches your code by the rule in Steps. A key your code sends to an outside application programming interface (API) is declared with
declare_upstream(Call an external API with an API key). The exception is a key whose client's host is fixed in code you must not change. A value your code uses itself is bound in the manifest'ssettings. - Asks you for the name to store it under, the scope (your account, or one application in one environment), and the local file that contains the value, or that you will type it. It does not ask for the value.
- Calls
list_secretsto learn whether the name exists and at which scope (step 1). - Calls
store_secretwith the name, the scope, and no value. The response has a command line with a short-lived grant for that one write (step 2). - Runs the line itself where the call named the value's file as
value_file. Otherwise you run it in your own terminal, which asks for the value. - For a replacement, calls
rotate_secretthe same way, with the same name, application, and environment. A replacement needs no code change, and an upstream's key needs no redeploy. A bound setting takes the new value at the environment's next deploy or promote, or at a restart where the running copy already has the binding. - Records the name, never the value, in your project's credential register, if the project keeps one.
- Asks for no browser approval:
store_secretandrotate_secretrun without an approval step. - For a value stored under the wrong name or scope, or one no longer needed, calls
delete_secretand gives you the approval link. Nothing is deleted until you approve it in the browser (step 6). - For the development platform or database credential, calls
submit_manifestwithlocal_run: trueinstead, and runs the line it returns, because the platform returns a new value on the HTTP API only.
What you need
- The secret value, saved in a file outside your application's folder or ready to paste into your own terminal, and a name you choose for it.
- The scope: your whole account, or one application in one environment, development or production.
Before your AI starts
This section is for your AI tool: what it checks and gathers before it begins. You don't need to do these steps yourself.
- A connected, signed-in tool (Connect your tool).
list_applicationsreturns the application's identifier.- Node.js 24 or later, whose
npxruns the command line (Prerequisites).
Steps
A stored value has two uses, and each name has one of them:
- An upstream's key. The egress gateway adds it to each call to an upstream declared with its name. It never enters your container, so your code calls the gateway to use it.
- A bound setting. Your manifest's
settingsmember binds the name to a setting your code reads. Each deploy and promote reads the value at that environment's scope and injects it into the container. Bind a value to a setting shows how.
One rule chooses between them. An API key the code sends to an outside API is declared with declare_upstream, whose settings name the base-URL and key variables the SDK reads from its environment. A value the code uses itself, a webhook signing secret for one, is bound in the manifest's settings. A client whose host is fixed in code you must not change is the one exception: bind its key in the manifest's settings instead, and the key then enters the container. Then list the host that client calls in the manifest's egress.
Two members share the name settings. The manifest's settings binds a stored name to a setting your code reads, and the value enters the container. declare_upstream's settings names the settings an unchanged SDK reads, the gateway's address and an egress key, while the stored key stays at the gateway (An unchanged SDK).
To store, bind, or replace one secret, read to the end of Worked example: a webhook signing secret. For an outside API's key, also read The rule in code, which shows both paths. Every later section is reference: read one before the act it covers.
Worked example: a webhook signing secret
Your backend checks each webhook Stripe sends with the endpoint's signing secret. The code uses the secret itself, so it is bound as STRIPE_WEBHOOK_SECRET. Stripe issues one secret for test mode and another for live mode.
- Store. For each hosted environment, your tool calls
store_secretwithnamestripe-webhook-secret, theapplication, theenvironment, andvalue_file, a file outside the application's folder that contains that environment's secret. It runs the line each call returns (step 2). Production takes the live secret, and development, once turned on, the test-mode one. - Bind. Your tool adds
"STRIPE_WEBHOOK_SECRET": { "secret": "stripe-webhook-secret" }to the manifest'ssettingsand resubmits the manifest withsubmit_manifest. The response's rows say whether the name isstoredat each environment's scope. - Deploy. A
deployreads the value of the environment it reaches and injects it into the new copy asSTRIPE_WEBHOOK_SECRET, which the code reads fromprocess.env. Apromotereads the production value the same way. Either is refusedsetting_secret_missingwhere its environment has no value under the name. - Rotate. When you roll the secret at Stripe, your tool calls
rotate_secretthe same way, with the same name, application, and environment, and runs the line. The response'snextis the restart call for the environment whose running copy keeps the old value.read_statuslists the setting inrotated_since_read. - Restart. Once the line ends with status 0, your tool makes that
restart_applicationcall. The new copy reads the new value, because the running copy already has the binding. A binding added since the last deploy or promote waits for the next one instead (Restart an environment).
The rule in code
Your tool judges the exception, and the test is whether it is allowed to change the code. Where it is, it changes the client to read its base URL and key from settings, and declares the upstream. Where the client's host is fixed in code it must not change, it binds the key. That includes code you have asked it to leave as it is. A setting name you want kept is no reason to bind. declare_upstream's settings name the setting your code reads, so the name stays and the key stays at the gateway.
For example, the OpenAI SDK sends its requests through a base URL, so its key is declared as an upstream. The declaration's settings name the two variables the SDK reads, and your tool calls declare_upstream with these members. No test checks this sample.
{
"name": "openai",
"base_url": "https://api.openai.com",
"credential_name": "OPENAI_KEY",
"auth_header": "Authorization",
"auth_format": "Bearer {value}",
"application": "<application id>",
"settings": { "base_url": "OPENAI_BASE_URL", "key": "OPENAI_API_KEY", "base_path": "/v1" }
}
The same declaration can live in the manifest instead, as an entry of upstreams keyed by its name and without application. A copy of the application then needs only the stored key and a deploy (Declare an upstream in the manifest). There, a key not stored yet is accepted, and the gateway refuses the upstream's calls until store_secret stores it.
The code stays unchanged. From the next deploy, OPENAI_BASE_URL contains the gateway's address for the upstream and OPENAI_API_KEY an egress key, which the gateway swaps for the stored key. No test checks this sample.
import OpenAI from 'openai';
const client = new OpenAI();
const answer = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'Hello' }] });
A local run's calls through the gateway act in development, the environment its platform credential fixes, and read the key at the application's development scope, then at the account scope. So store a key a local run uses at the development scope, naming application and environment development, since store_secret defaults to production, or once at the account scope where both environments share it. A bound setting reads the application's own scope at each deploy and promote, never the account's.
The wrong choice does not fail at the deploy. An upstream declared for a client whose host is fixed never reaches that client. The client calls its host directly and sends the egress key, or no key, in place of your stored one. The deploy still passes its health check, and the call then fails: the provider refuses it, or the platform does once the application enforces its egress list.
The platform does not detect the wrong choice when you submit the manifest either. Which way is right depends on your application's code, which the platform does not read.
So check the choice after the first deploy. Your tool makes the application send one call that uses the key. It then calls read_logs for that environment twice, with filter: "undeclared" and source: "egress", then with source: "harness". The first read holds the calls made through the platform's proxy, and the second the calls a client tried without it.
A record that names the provider's host means the client called that host itself, not the gateway. An upstream declared for that client is not used: bind the key in the manifest's settings instead, and list the host in egress. Where the call was made and no record names that host, it went through the gateway or to a host the manifest lists. The reads show where a call went, never which key it carried.
A maps client whose host is fixed in code you must not change is that exception. Store it as maps-api-key, then bind it in the manifest's settings, and the code reads process.env.MAPS_API_KEY. The client calls its host directly, so list that host, maps.example.com here, in the manifest's egress, as Writing the egress list describes. No test checks this sample.
"settings": {
"MAPS_API_KEY": { "secret": "maps-api-key" }
},
"egress": ["maps.example.com"]
1. Choose the name and the scope
Turn Zero Cloud stores secret values apart from the names and details its management actions return. Your project and your conversations refer to a secret by name. Storing a value does not protect a copy that is already in a transcript, a log, or a repository.
The platform has two write actions, store_secret and rotate_secret. Both require name. The value goes only through the management HTTP API, where the command line of step 2 sends it. The Model Context Protocol (MCP) tools of the same names take no value: a call stores nothing and returns that command line. Both also accept an optional application identifier and an optional environment, development or production, which defaults to production. None of the secret actions returns a value you stored.
A store_secret call that names generate is the exception to a call storing nothing: the platform creates the value itself, and the call only creates, returning no command line.
The name has 1–64 letters, digits, underscores, or hyphens, and begins with a letter or digit. The value must not be empty, and its JavaScript string length must be at most 20,000.
A scope is the account, or one application in one environment:
- An account-scoped value is used by every environment.
- An application-scoped value is used only by the environment it was stored at. A development request never reads a production value, and neither environment falls back to the other.
Every application has a development scope, even with one environment: a local run's calls through the gateway read its values. If development and production use different keys, store the same name at each environment's scope of the application, each with its own value. Development can then use a test key and production a live one. If an upstream's two environments share one key, you may store it once at account scope instead. A bound setting never reads the account scope.
store_secret creates a name, or replaces its value at the same scope. A call naming generate only creates: it refuses a name already stored at that scope. Leaving out application selects account scope. A name keeps the scope it was first stored with, and neither write action moves it. The one exception is the application's other environment, which may have the name too. For any other scope, use a new name. Each application scope also has three names of the platform's own, which step 4 describes.
2. Supply the value without exposing it
Do not paste the value into chat or ask the assistant to print the file that contains it. The app that runs your AI tool may record each MCP tool call's arguments and output, so a value passed through a tool call may appear in the transcript.
Your tool calls store_secret with the name, the scope, and no value. The call stores nothing. Its response has:
state, which isawaiting_value;command, one command line for macOS and Linux;command_windows, the same line for Windows, withnpx.cmdin place ofnpx;expires_at, a few minutes after the call, after which the line is refused;detail, which says who runs the line.
The line runs the secret subcommand of the turnzero-cloud command. It includes a secret grant: a short-lived credential for one write, of that name at that scope. The grant is not the value, and it is not your account's credential. The command reads the value on your machine, sends it once to the management HTTP API, and prints neither the value nor the grant.
Run the line for your operating system once, exactly as the call returned it, before expires_at. A second run is refused, and so is a line changed to give another name or scope.
Who runs the line
The value reaches the command in one of two ways, and the way decides who runs the line.
- From a file: your tool runs the line. The call names the file as
value_file, a path on your machine. The path is absolute, or relative to the folder the line runs in. No shell and not the command expands~in it, so name the home folder in full. The line then ends with--value-fileand that path, and the command reads the file there. The platform never reads the path. - Typed at a terminal: you run the line. The call names no
value_file, and the line ends with--value-prompt. Your tool gives you the line, and you run it in a terminal of your own. The command asks for the value and does not show what you type or paste. Enter ends the value, so a value of more than one line goes in a file instead.
Your AI tool's shell has no terminal you can type into. A line that ends with --value-prompt is refused there at once, and sends nothing.
A value_file path that a shell would change in the line is refused 400 invalid_request, and no line is returned. Such a path has a control character, a double quote, $, a backtick, %, !, &, |, <, >, ^, a typographic double quote, a doubled backslash, or a trailing backslash. The line does not expand ~: name your home folder in full.
The value's file is kept outside your application's folder, because a deploy's zip includes every file of that folder but node_modules, .git, and the .env files. A value's file left in the folder would enter the artifact. Where a deploy from the folder the line runs in would include the file, the command prints a line that opens warning: and stores the value all the same.
The three samples below show the line's form, with <grant> where each call's own grant goes. Run the line your own call returns, never a sample. The first is command for a call that names ../secrets/stripe-webhook-secret.txt as value_file. Where the line runs in the application's folder, the path's ../ puts the secrets folder beside that folder, outside it:
echo <grant> | npx -y https://turnzero.ai/packages/turnzero-cloud-0.8.0.tgz secret store --name stripe-webhook-secret --application 3f6c2a1e-8d4b-4c7a-9e21-5b0f7d9c4a13 --environment production --value-file "../secrets/stripe-webhook-secret.txt"
This is command_windows for the same call:
echo <grant> | npx.cmd -y https://turnzero.ai/packages/turnzero-cloud-0.8.0.tgz secret store --name stripe-webhook-secret --application 3f6c2a1e-8d4b-4c7a-9e21-5b0f7d9c4a13 --environment production --value-file "../secrets/stripe-webhook-secret.txt"
And this is command for a call that names no value_file:
echo <grant> | npx -y https://turnzero.ai/packages/turnzero-cloud-0.8.0.tgz secret store --name stripe-webhook-secret --application 3f6c2a1e-8d4b-4c7a-9e21-5b0f7d9c4a13 --environment production --value-prompt
A line for the account scope has no --application and no --environment. Where your tool is connected to an origin other than https://turnzero.ai, the line includes --origin with that origin, so you set nothing yourself.
To store one value in both environments of an application, your tool makes one call for each environment and runs each line.
When no person is present
The platform creates a value nobody chooses itself, such as a session signing secret, so the value never passes through a file or the transcript:
- Your tool calls
store_secretwithname,application,environment, andgenerate.base64url_32creates 32 random bytes as 43 characters, andhex_32the same bytes as 64 hexadecimal characters. - The call stores the value at that environment's scope of the application. It returns
generated, the form, and never the value. - Your tool binds a setting to the name in the manifest's
settings, then deploys.
A created value exists in the deployed environments alone, so a local run sets its own value for that setting. Your application's own code is the one place the value is read, so a secret an outside service must also hold, such as a webhook's signing secret, is supplied rather than created.
rotate_secret creates no value, so a created value is replaced under a new name. Your tool stores a new name with generate, binds the setting to the new name, and deploys. It then deletes the old name with delete_secret, which you approve in the browser.
Two cautions go with replacing a created value. Create the new name at each environment the application deploys to before the deploy and the promote, because a created name stands at one environment's scope. Keep the old name while you might roll back to a version that binds it, because nobody can store a created value again.
For a value the platform cannot create, the tool uses the one-step file route below.
A tool that works with no person present can supply a value that the tool itself creates, such as a random signing secret or a generated key. It writes the value, runs the line, and deletes the file in one shell step:
- It learns the operating system's temporary folder with
node -p "require('node:os').tmpdir()", which prints a folder and no value. - It names a new file in that folder by its absolute path, with a random part in the file's name, and calls
store_secretwith that path asvalue_file. The file need not exist yet, because the platform never reads the path. - In one shell step, it writes the value into that file, runs the line the call returns, and deletes the file.
One step keeps the file where the line reads it. A path that opens with ~, or a relative path the line reads from another folder, leaves the command no file, and it refuses with ENOENT.
For example, this step writes 32 random bytes as text into the file, runs command, and deletes the file, in bash or zsh on macOS and Linux. /tmp stands for the folder step 1 printed:
node -e "require('node:fs').writeFileSync(process.argv[1],require('node:crypto').randomBytes(32).toString('base64url'),{mode:0o600,flag:'wx'})" "/tmp/session-signing-secret-7f3a.txt" && { echo <grant> | npx -y https://turnzero.ai/packages/turnzero-cloud-0.8.0.tgz secret store --name session-signing-secret --application 3f6c2a1e-8d4b-4c7a-9e21-5b0f7d9c4a13 --environment production --value-file "/tmp/session-signing-secret-7f3a.txt"; rm -f "/tmp/session-signing-secret-7f3a.txt"; }
This is the same step in PowerShell on Windows, with command_windows:
node -e "require('node:fs').writeFileSync(process.argv[1],require('node:crypto').randomBytes(32).toString('base64url'),{mode:0o600,flag:'wx'})" "C:\Users\me\AppData\Local\Temp\session-signing-secret-7f3a.txt"; if ($LASTEXITCODE -eq 0) { echo <grant> | npx.cmd -y https://turnzero.ai/packages/turnzero-cloud-0.8.0.tgz secret store --name session-signing-secret --application 3f6c2a1e-8d4b-4c7a-9e21-5b0f7d9c4a13 --environment production --value-file "C:\Users\me\AppData\Local\Temp\session-signing-secret-7f3a.txt"; Remove-Item "C:\Users\me\AppData\Local\Temp\session-signing-secret-7f3a.txt" }
In cmd, put the line and a del of the file in parentheses after the write and &&. Git Bash on Windows runs the bash form, the first sample, with the folder step 1 printed. It never runs the PowerShell or the cmd form, whose delete it does not have.
The write refuses where a file already stands at the name, and the step then runs nothing more: your tool names a new file and makes a fresh call. Once the write succeeds, the step deletes the file whatever the line's status. A printed line that opens with stored: or rotated: means the value was written. On any other outcome, your tool makes a fresh call and a fresh step with a new value.
A tool with no person fills the file only with a value it creates in that step, as the samples do. A value already in a file on the machine, such as a .p8 key file, needs no copy: your tool names that file as value_file.
The temporary folder is outside your application's folder, so no commit and no deploy's zip includes the file. On macOS and Linux, only your user can read the file. The path is not the value, so it may appear in the transcript.
A value that only you know, such as a key a provider issued to you, needs you. No tool can supply it. Your tool calls store_secret with no value_file and gives you the line, and you run it in your own terminal before expires_at. Or you save the value in a file outside your application's folder and tell your tool the path, and your tool names it as value_file.
How the value stays out of the transcript
A transcript records your tool's calls, the commands it runs, and their output. The value is in none of them:
- No tool call has it.
store_secretandrotate_secrettake no value, andvalue_fileis a path. - No command line has it. The line has the grant and the path, never the value. Your tool never writes the value into a command, for example to fill a file or a variable, because the command would reach the shell's history and the transcript.
- No output has it. The command prints the action's result, never the value or the grant. Your tool never prints the file and never reads it back.
The grant does appear in the transcript, inside the line. A copy of the grant can make that one write, of that name at that scope, once, before expires_at. It can do nothing else. Once your line has run, the grant is spent.
What the command reads and prints
The command reads the file as UTF-8 text. It drops a UTF-8 byte-order mark, and one line ending at the end of the file. It refuses a UTF-16 file before sending anything. Windows PowerShell 5.1 writes UTF-16 when it saves with > or Out-File, and Set-Content -Encoding utf8 writes UTF-8 instead.
Where the value is written, the command prints one line that opens with stored: or rotated: and gives the name and the scope. It then prints the platform's detail. It ends with one of three statuses:
- 0: the value was written.
- 3: nothing was written. The printed refusal says why.
- 2: the write got no response, so the outcome is unknown.
list_secretsshows when the name was last written.
Storing the value does not delete local copies. Once the line ends with status 0, delete the file that contains the value, or keep it outside your application's folder.
The value still passes through the command and the platform. This procedure keeps it out of the assistant's conversation, but it cannot guarantee that every local diagnostic or outside host hides secrets.
A key never belongs in a command. A short-lived grant does, because your tool's own shell has to present it. The upload line and upload commands mint_upload_grant returns contain an upload grant: it covers one file, allows one write, and expires within minutes.
The line a deploy call returns contains a one-time deploy code instead, which the command presents once to prepare the deploy's upload. The upload grant that preparing call returns to the command also starts that file's deploy, once. After that start, it is valid for the command's own progress reads of the deploy, until five minutes after the deploy ends and never past fifteen minutes after the start.
3. Replace a value
rotate_secret replaces an existing value. Supply the same name, application, and environment; leaving out application selects account scope. Only the stored value changes, and the application is not rebuilt.
rotate_secret creates no value, so a value the platform created with generate is replaced under a new name. When no person is present says how. A later store_secret or rotate_secret that carries a value replaces a created value for good, and no action restores it.
Your tool calls rotate_secret with no value, as it calls store_secret, and the response has the same members. Its line names secret rotate, and it ends in the same two ways (step 2). Where the environment's running copy has a binding of the name, the response also has next. That is the restart_application call your tool makes once the line ends with status 0. The response's detail also says why the restart is a separate call: a copied line cannot by itself make the running copy read a value.
When the new value is used depends on how it reaches your code:
- An upstream's key. The gateway uses the new value on later calls, with no redeploy.
- A bound setting. The running container keeps the previous value. The environment reads the new one at its next deploy or promote. A
restart_applicationreads it too where the running copy already has the binding, while a binding added since waits for the deploy or promote. The rotation'ssettingslists the settings the name supplies, and itsdetailsays which a restart reaches. Itsrestart_environmentgives the environment whose running copy has the binding and keeps the old value. Where only a deploy, promote, or redeploy in progress has the binding, it gives none, andread_statusreports it once that action ends.
A deploy, promote, or redeploy in progress may get either value, depending on when it read its bindings, and restart_application is refused until it ends. After a rotation at an application's scope, read_status for that environment lists in rotated_since_read each setting the running copy read before the rotation. If the platform cannot read where the name is bound, the rotation still completes, and the detail names no setting.
Check that a rotation reached the running copy
A bound setting takes three calls to check. An upstream's key needs none, because the gateway uses the stored value from its next call.
- Your tool calls
read_statusfor the environment.rotated_since_readlists each setting whose secret changed after the running copy started. - Where the setting is listed, your tool makes the
restart_applicationcall for that environment, the one the rotation's response gave asnext. - Your tool calls
read_statusagain. Where the setting is absent fromrotated_since_read, the running copy has the stored value.
The list counts from the running copy's start: a setting is listed where its secret was stored or rotated after the copy started, and a restart clears it. So a setting absent after step 3 is the platform's own reading that the copy started after the rotation and read the stored value. To prove it from outside the platform as well, revoke the previous value at the service that issued it, then make a call the secret authorizes. The call succeeds only on the new value.
If a secret appears in a chat, a log, a commit, or an email, treat it as exposed. Replace it at the service that issued it, then replace the stored value with rotate_secret. If you decide to accept a known exposure, record that decision; it should not be the default.
4. The platform's own names
Each application scope has three names that belong to the platform:
credential-<application id>, the environment's platform credential;database-<application id>, the database credential named in the provisioning result; andissue-tracking-<application id>, the token of the application's issue-tracking space. It is the space's first token, at its highest permission level, and the platform never shows its value.
store_secret refuses all three with platform_minted_name, and rotate_secret refuses the issue-tracking token at every scope. rotate_secret accepts the other two on the development scope only, with no value: the platform creates a new value, stores it, and returns it once for your environment file. On the production scope, rotate_secret refuses them with platform_minted_name, because a production credential is never shown to you.
The platform returns the new development value on the HTTP API only. Through the MCP tool, the same call is refused local_route_required before anything is written. Your tool calls submit_manifest with the application, its manifest, and local_run: true instead, and runs the provision line the response returns. The line calls rotate_secret on the HTTP API under a grant, receives each value once, and writes the environment file.
After a development rotation:
- Database password. Rotating the development database credential changes the database role's password. A PostgreSQL session already open keeps working; new connections need the new value.
- Running container. Where development is turned on, a running development container keeps the previous values until its next deploy, so deploy to it again.
- Previous platform credential. The HTTP API refuses it at once. The storage, logging, and egress endpoints accept it for up to the interval Sign-in, sessions, and tokens states.
5. List what is stored
list_secrets takes no arguments and returns one row per stored name and scope. A row contains the name, the scope (account or application), the application identifier and the environment (both null at account scope), and the created_at and rotated_at times. Its settings member lists the settings the application's manifest binds to the name, empty where none does. The action reference gives every action's arguments, results, and refusals.
6. Delete a value
delete_secret deletes one stored value: a key stored under the wrong name or at the wrong scope, or one no longer needed. It takes name, with application and environment giving the scope as the write actions do. Each environment's value is deleted by its own call.
The deletion is a destructive action. The call returns a pending action and an approval link, and nothing is deleted until you approve it in the browser. Your tool reads the outcome with read_pending_action (Management action tiers and approvals). A token bounded to one application is refused; your tool calls it under your signed-in connection.
To move a key stored under the wrong name or scope, the order depends on where it belongs. Where the key belongs under a new name or at the application's other environment, store it there first and then delete the wrong entry. Where it belongs under the same name at another scope, delete the wrong entry first and store only once read_pending_action reads completed, because store_secret refuses the name scope_fixed while the wrong entry exists.
The platform refuses a name something still reads, secret_in_use, and the detail names each use:
- a binding in the manifest's
settings, or in the running copy of the environment whose scope has the name or a deploy, promote, or redeploy in flight to it; - an upstream that names it as its key;
- a sign-in method of the same environment's realm, or that environment's push configuration, that names it now.
A sign-in method of the application's other environment reads that environment's own value, never this one. When you point a realm's sign-in method at another stored name, the earlier value stays in the realm vault, where nothing reads it. That copy is no use, and the deletion removes it too.
End each use first, then delete the name. An upstream your application no longer calls ends with undeclare_upstream (End an upstream). An account-scope name is in use only where an upstream of one of your applications names it as its key, since no binding, realm sign-in method, or push configuration reads the account scope. The three names of the platform's own from step 4 are refused platform_minted_name, because the platform deletes each with its environment, its application, or your account.
If a use begins after the request and before your approval, the platform finds it when it describes the deletion again at your approval. The new description differs from the one you approved, so the pending action ends declined, with nothing deleted. A use that begins after your approval ends it failed with secret_in_use, and nothing is deleted. Where the value is already gone, or was re-supplied, rotated, or stored again since your approval, the action completes with deleted: false and deletes nothing. Request the deletion again to delete the value stored now.
After the deletion, list_secrets no longer lists the name, and no caller reads the value again. The secret store keeps the deleted value for ninety days before it purges it, and no action of yours can read or restore it. To stop a key from working, replace it at the service that issued it. Storing the same name again later stores a new value.
How the value reaches your application
The gateway never gives an upstream key to your process; only an upstream that echoes the key sends it back. Your backend calls the external API through the platform's gateway, which adds the value stored for the request's environment, or at account scope, as it forwards the call. The gateway removes the key's header from the upstream's response and passes the rest of the response through, so declare only an upstream you trust with the key. Call an external API with an API key shows how.
The other settings a hosted process receives are the platform's own. They include its environment's platform credential, the database connection string if the application has a database, and TURNZERO_CLOUD_REALM_KEYS, the realm's public keys, if the environment has a realm. Each deploy supplies the settings of the environment it reaches, and a promote supplies production's.
Bind a value to a setting
Some values your code uses itself, such as a webhook signing secret. Bind such a value in the manifest's settings member, which maps a setting name to the stored name. No test checks this sample.
"settings": {
"STRIPE_WEBHOOK_SECRET": { "secret": "stripe-webhook-secret" }
}
Author the manifest shows a whole manifest with this member, which a test validates. Store the value at the scope of each hosted environment of the application, with the same name: production's alone on one environment. Each deploy and promote then reads the value stored for its environment and injects it as STRIPE_WEBHOOK_SECRET. The value never enters the manifest, the artifact, or a log.
A binding gives up the protection the gateway gives a stored key. The value is in the container, so anything that can read the process's environment can read it. A replacement also waits for the environment's next deploy or promote, or for a restart where the running copy already has the binding.
submit_manifest refuses a binding before recording anything in these cases:
- the setting name is one the platform reserves, refused
manifest_invalid; - the name is stored at account scope or at another application's scope, refused
setting_scope_refused; - the name is one of the platform's own names from step 4, refused
platform_minted_name; - an upstream of the application names it as its key, refused
setting_is_upstream_key; - a realm's sign-in method or a push provider names it, refused
name_bound_to_realmorname_bound_to_push.
The rule runs the other way too: configure_realm and configure_push refuse a name a binding feeds name_bound_to_setting, and declare_upstream refuses it upstream_key_is_bound. A binding counts where the manifest's settings contains it, and where the running copy of either environment has it until that environment's next deploy or promote of a manifest without it. It also counts where a deploy, promote, or redeploy in flight to an environment has recorded it, until that action ends. The refusal's detail names the setting and where it is bound.
A deploy or promote whose bound name is not stored at its environment's scope is refused setting_secret_missing. Deploy an application lists the settings a copy receives.
A local run reads the environment file the provision line writes, which a submit_manifest call naming local_run returns. That file contains seven settings: APP_DATABASE_URL, APP_DATABASE_CONNECTION_LIMIT, TURNZERO_CLOUD_TOKEN, APP_ENVIRONMENT=local, TURNZERO_CLOUD_API, TURNZERO_CLOUD_GATEWAY_URL, and TURNZERO_CLOUD_APPLICATION. A setting the manifest's settings binds is not among them, because no call answers a stored value. Give it a local value on a line of your own in that file, or in your shell. Without a database in the manifest, APP_DATABASE_URL and APP_DATABASE_CONNECTION_LIMIT are left out. Run locally explains each.
A key that a sign-in method uses
Two sign-in methods of an end-user realm read a stored secret: the work-account route's client secret and Sign in with Apple's signing key. Store either as any other secret, at the application's scope and in the realm's environment. For Apple, the value is the text of the .p8 key file Apple issues, so the call names that file as value_file.
configure_realm then takes its name: entra.client_secret_name for the work-account route, and apple.key_secret_name for Apple (Manage end users). The call moves the value into the realm vault, where it stays under that name. Only the accounts service reads it there, and it reads the Apple key only to sign Apple's client secret. No upstream may use the name, and no action returns the value. Rotate it by name with rotate_secret.
The same two names can live in the manifest's realm member instead, which names them for each realm the application has (Configure sign-in and push in the manifest). There, a name not stored yet is accepted, each deploy and promote lists it in outcome.provider_credentials_missing, and the value moves into the realm vault at the manifest's next submission after it is stored. A push entry's apns and fcm key names are accepted and listed the same way, and their values stay where store_secret put them.
Recording the name in your project
A project can keep a credential register: a list of names and how to use them, with no secret values. The platform requires no particular file or format. One form that works gives each credential one entry. The entry records the credential's name, the service that uses it, the access it grants, and how to obtain a value. That record lets the next person or agent repeat the setup.
What secret storage is not
The platform stores service credentials for its own use, such as adding a key to calls through the egress gateway. It is not a personal password manager. The secret actions show names and details but offer no way to read a stored value back. When you need a new value, get it from the service that issued the credential.
Expected result
Your tool's store_secret or rotate_secret call returns state: awaiting_value. The response has expires_at, command, command_windows, a detail, and page. Its secret object has stored: false, or rotated: false for a rotation. A rotation's response also has next where a restart applies the new value. Nothing is stored until the line runs.
A store_secret call naming generate returns no state and no command line. Its secret object has stored: true, resupplied: false, and generated, the form the call named. The created value is never returned.
When the line runs, the command sends the value, and the action returns its result to the command. store_secret returns a secret object with:
- the
name; - the
scope,accountorapplication; - the
applicationidentifier and theenvironment, both null at account scope; stored: true; andresupplied, true if the name was already stored and its value was replaced.
rotate_secret returns name, scope, application, environment, and rotated: true, with no resupplied member. Where the manifest binds the name, it also returns settings, the settings the name supplies. Where that environment's running copy has a binding of the name, it also returns restart_environment, naming the environment whose restart_application applies the new value.
Each result includes a detail that never contains a value you stored. The last sentence of the detail names the worked example above. Each result also includes page, the address of this page. The one value ever returned is the development value step 4 describes, on the HTTP API only. The command prints the result and its detail, and nothing of the value, so the conversation contains only the name.
list_secrets then lists the name with its created_at, and with a rotated_at after a replacement. A later call through the gateway to a declared upstream uses the value stored for the request's environment.
Where the manifest binds the name, submit_manifest returns one row per setting and environment the application has, showing stored: true where the name is stored at that environment's scope. The next deploy or promote injects the value as the setting, and after a rotation a restart gives the running copy the new value.
delete_secret returns 202 with pending_action and approval_url. After your approval, read_pending_action returns completed, with an outcome that contains deleted: true, the secret's name, scope, application, and environment, and a detail stating the store's retention window. list_secrets then no longer lists the name. Where the value was already gone, or changed after your approval, the action completes with deleted: false and a detail saying which; a use made after your approval ends it failed with secret_in_use.
Refusals
A refusal identifies the action, gives the cause in its detail, and writes nothing. The rows below are the refusals of the four secret actions and the refusal a deploy or promote meets when a bound name is not stored. Then come the refusal a declared upstream meets when its stored name is missing, and three the command line meets when its grant no longer works. The last two are for a credential that is wrong.
| Refusal | Status | Cause | Remedy |
|---|---|---|---|
invalid_request |
400 | A field is invalid: name breaks the rule in step 1, value is empty or too long, application is not a string, or environment is not development or production. Or value_file is named beside value, or is a path one command line cannot contain, which Who runs the line lists. Any field containing a NUL character (U+0000) or an unpaired UTF-16 surrogate is refused too. So is generate named beside value or value_file, with no application, or outside its two forms, and a tool call that carried a value beside generate. |
Correct the field the detail names, or remove the character where the detail names one, and send the request again. Where a tool call carried a value, treat that value as exposed, as step 3 says. |
no_such_application |
404 | application names no application of your account. |
Use the identifier list_applications returns. |
not_found |
404 | rotate_secret or delete_secret named a secret that is not stored at the scope named, or rotate_secret a development database credential where no development database exists. Where the name is stored at the application's other environment alone, detail says so. |
Name the scope list_secrets gives, or store the name at that scope with store_secret first, as step 2 describes. For the database credential, submit a manifest that declares the database service. |
scope_fixed |
409 | The request named a scope the name may not move to: the account scope for a name at an application's, another application's scope, or an application's scope for a name at the account scope. The application's other environment is never refused. | Use the scope the refusal's scope, application, and environment members give, or store the value under a new name. |
secret_exists |
409 | store_secret named generate for a name already stored at that scope. A call naming generate never replaces a value. |
Store a new name with generate, bind the setting to it, deploy, then delete the old name with delete_secret. To store a value of your own under the name, call store_secret without generate. |
secret_count_limit |
409 | store_secret would add a name to an application's environment scope that already holds 100 names, counting every name stored at that scope, the platform's own among them. Nothing was written, and no grant was minted. |
Delete a name no longer needed there with delete_secret, once nothing uses it, then store again. A re-supply or a rotation of a name that stands there is never refused. |
platform_minted_name |
409 | store_secret or delete_secret named one of the three names in step 4, or rotate_secret named the issue-tracking token or another of them outside the development scope. |
Store your own value under a name of your own. Create new development credentials: submit the manifest again naming local_run and run the line the response returns, then deploy development again where development is turned on. Every deploy to production and every promote replaces the production platform credential, and one made with rotate_database_credential: true also replaces the production database credential. The platform deletes its own names with their environment, their application, or your account. |
secret_in_use |
409 | delete_secret gave a name that something still reads. A binding in the manifest's settings, or in its environment's running copy or a deploy, promote, or redeploy in progress, uses it. Or an upstream, or a realm sign-in method or push configuration of the same environment, refers to it now. The detail names each use. |
End each use the detail names, then call delete_secret again. For a binding, remove it from the manifest's settings, resubmit the manifest, and deploy or promote each environment whose running copy has it. Point an upstream, a realm sign-in method, or a push configuration at another stored name, or end an upstream your application no longer calls with undeclare_upstream. |
local_route_required |
409 | rotate_secret named a platform name on the development scope through the MCP tool, which returns no credential value. A call from your tool for a name of your own is never refused this way: it returns the command line. |
Submit the manifest again naming local_run and run the line the response returns; it makes the same call on the HTTP API. Then deploy development again where development is turned on. |
rotation_in_flight |
409 | Another rotate_secret call for the same development database credential was still running, so nothing was changed. |
Wait for that call to finish, then submit the manifest again naming local_run and run the line the response returns. It writes the current password. Then deploy development again where development is turned on. |
plan_quantity_unset |
409 | rotate_secret for the development database credential needs the plan's connection limit, and the plan has no value set for it, so nothing was changed. |
Wait for platform staff to set it, then submit the manifest again naming local_run and run the line the response returns. Then deploy development again where development is turned on. |
database_provisioning_failed |
502 | rotate_secret found the development database recorded but its database role missing, so nothing was changed. |
delete_environment naming development deletes the database, with development's data and secrets, on one environment or two. A resubmission then sets it up again with a new database; on two environments, call create_environment before it. If that deletion fails, report the refusal and quote the reference in its detail. |
setting_secret_missing |
409 | A deploy or promote read a binding in the manifest's settings whose stored name has no entry at that environment's scope. The detail names the setting and the secret. |
Store the value with store_secret naming the application and that environment, then deploy or promote again. Or remove the binding from the manifest's settings, resubmit the manifest, and deploy or promote again. |
credential_not_in_custody |
409 | The secret name a declared upstream uses is not stored at the scope the call reached. Where it is stored for the application's other environment alone, detail names both environments. |
Store the value at that scope with store_secret, as step 2 describes, then retry. If detail names the other environment, store the same name at the missing environment's scope, with that environment's value. Keep a key meant for one environment at that environment's scope. |
secret_grant_expired |
403 | The command line ran after its expires_at, so nothing was written. |
Call store_secret or rotate_secret from your tool again, and run the fresh command before its expires_at. |
secret_grant_spent |
403 | The line's grant had already been used, and a grant can be used once, so this run wrote nothing. A second run of one line is refused this way. Where a line's first run is refused this way, someone else used its grant first, and the stored value may not be yours. | Do not run the same line again. list_secrets shows when the name was last written. Where it shows no write at the time your line first ran, or that first run was refused, call store_secret or rotate_secret from your tool again. Run the fresh command, which replaces the stored value. |
secret_grant_not_admitted |
403 | The line was changed to give another name, action, application, or environment than the call that returned it. Or its grant was presented to another action or route. Nothing was written, and the grant is not spent. | Run the line exactly as the call returned it. For another name or scope, call store_secret or rotate_secret from your tool for that name and scope, and run the command it returns. |
token_scope_refused |
403 | A script called a secret action under a minted token bounded to one application, and the request named another application or none. A bounded token cannot reach account scope or call list_secrets or delete_secret. The command line never meets this refusal, because its grant is for one name at one scope. |
Use a token bounded to the named application or to the whole account. Call delete_secret under your signed-in connection. |
authentication_required |
401 | The request presented no credential, or one that matched no account, and the detail says which. For a command line, the detail says "The grant this request presented was not accepted". That grant expired and was removed, or the line's grant was changed. |
For a grant that was not accepted, call store_secret or rotate_secret from your tool again and run the fresh command. Otherwise sign in again through your tool. |
Related
- Call an external API with an API key declares an upstream against the stored name and calls it through the gateway.
- Running on your machine describes the line that creates the development credentials and writes the environment file.
- The turnzero-cloud command describes the command the line runs, its options, and its statuses.
- Secrets in Troubleshooting starts from what a refused command line prints.
- Secrets states what the secret service provides and what a deletion removes.
- Sign-in, sessions, and tokens compares the credentials a script can present.
- store_secret, rotate_secret, list_secrets, and delete_secret in the generated reference give each action's arguments and result.