Supply a secret value without exposing it

Prompt:

Give the app my API key, but don't paste it into the chat.

Also works:

  • "I'll type the password myself. Give me the command to run."
  • "Make a signing secret for the backend that nobody needs to see."

What your tool does

  • Reads the store-key skill (read_context, id skill:store-key) and follows it, as Store a secret lists.
  • Names the file by an absolute path, or one relative to the folder the line runs in, never ~.
  • Never prints the value's file, never reads it back, and never writes the value into a command.
  • Where you will type the value, gives you the line and asks you to run it in your own terminal before expires_at.

What you need

  • The value, saved in a file outside your application's folder, or ready to type into a terminal of your own.

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.

  • The name and the scope the value goes under (Choose the name and the scope).
  • Whether a person is present to point at a file or to run a line in their own terminal.
  • Node.js 24 or later, whose npx runs the line (Prerequisites).

Steps

Never paste the value into chat or have the assistant print its file: the app that runs your AI tool may record each tool call in the transcript.

Store a value from a file

This is the common case: the value is in a file on your machine, and your tool runs the line.

Your tool calls store_secret with the name, the scope, no value, and value_file, the file that holds the value. The path is absolute or relative to the folder the line runs in, with the home folder written in full, never ~. The platform never reads the path. The call stores nothing. Its response has:

  • state, which is awaiting_value;
  • command, one command line for macOS and Linux;
  • command_windows, the same line for Windows, with npx.cmd in place of npx;
  • 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 turnzero-cloud command with a secret grant, a short-lived credential for one write of that name at that scope. The command reads the value on your machine, sends it once, and prints neither the value nor the grant.

Your tool runs the line for your operating system once, exactly as returned, before expires_at. A second run after its write landed is normally refused. A line changed to give another name or scope is refused. rotate_secret works the same way and returns a secret rotate line (Replace a value).

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.

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 two samples below show the line's form, with <grant> for each call's own grant; run the line your call returns, never a sample. The first is command for a call that names ../secrets/stripe-webhook-secret.txt as value_file, a folder beside the application's folder:

echo <grant> | npx -y https://turnzero.ai/packages/turnzero-cloud-0.11.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.11.0.tgz secret store --name stripe-webhook-secret --application 3f6c2a1e-8d4b-4c7a-9e21-5b0f7d9c4a13 --environment production --value-file "../secrets/stripe-webhook-secret.txt"

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.

Type the value in your own terminal

With no value_file, you run the line, not your tool. The line ends with --value-prompt, and you run it in a terminal of your own. It asks for the value without showing it, and Enter ends the value, within five minutes. The terminal must also show the command's output. Your AI tool's shell has no terminal, so the line is refused there at once and sends nothing.

This is command for the same call with no value_file:

echo <grant> | npx -y https://turnzero.ai/packages/turnzero-cloud-0.11.0.tgz secret store --name stripe-webhook-secret --application 3f6c2a1e-8d4b-4c7a-9e21-5b0f7d9c4a13 --environment production --value-prompt

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 takes this route for every such value at an application's scope, whether or not a person is present:

  1. Your tool calls store_secret with name, application, environment, and generate, either base64url_32 (32 random bytes as 43 characters) or hex_32 (as 64 hexadecimal characters). The call returns no command line and never the value, and refuses a name already stored there.
  2. 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 it, and deploys, 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 is stored at one environment's scope only. 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 key longer than the 32 bytes generate creates. A value at the account scope, which generate does not reach, is another. It writes the value, runs the line, and deletes the file in one shell step:

  1. It learns the operating system's temporary folder with node -p "require('node:os').tmpdir()", which prints a folder and no value.
  2. It names a new file in that folder by its absolute path, with a random part in the file's name, and calls store_secret with that path as value_file. The file need not exist yet, because the platform never reads the path.
  3. 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 read from another folder, leaves the command no file, and it refuses with ENOENT.

For example, this step writes 48 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(48).toString('base64url'),{mode:0o600,flag:'wx'})" "/tmp/long-signing-key-7f3a.txt" && { echo <grant> | npx -y https://turnzero.ai/packages/turnzero-cloud-0.11.0.tgz secret store --name long-signing-key --application 3f6c2a1e-8d4b-4c7a-9e21-5b0f7d9c4a13 --environment production --value-file "/tmp/long-signing-key-7f3a.txt"; rm -f "/tmp/long-signing-key-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(48).toString('base64url'),{mode:0o600,flag:'wx'})" "C:\Users\me\AppData\Local\Temp\long-signing-key-7f3a.txt"; if ($LASTEXITCODE -eq 0) { echo <grant> | npx.cmd -y https://turnzero.ai/packages/turnzero-cloud-0.11.0.tgz secret store --name long-signing-key --application 3f6c2a1e-8d4b-4c7a-9e21-5b0f7d9c4a13 --environment production --value-file "C:\Users\me\AppData\Local\Temp\long-signing-key-7f3a.txt"; Remove-Item "C:\Users\me\AppData\Local\Temp\long-signing-key-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 exists at that path, and the step then runs nothing more: your tool names a new file and makes a fresh call. A step killed before the line ends leaves the file, so delete it by hand. 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 value already in a file on the machine, such as a .p8 key file, needs no copy: your tool gives that file as value_file. A value handed to your tool in the conversation itself, with nobody present, takes the same one-step file. The value is already in the transcript, so the file adds no exposure.

A value that only you know, such as a key a provider issued to you, needs you. No tool can supply it. Your tool gives you the line to run in your own terminal before expires_at, or uses a file you saved outside your application's folder 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_secret and rotate_secret take no value, and value_file is 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's write has landed, the grant is normally spent.

What the command reads and prints

The command reads the file as UTF-8, dropping a byte-order mark and one final line ending, and refuses a UTF-16 file before sending anything. In Windows PowerShell 5.1, save the file with Set-Content -Encoding utf8, never > or Out-File.

With no grant piped, the command prints a line that says so and names this computer's token for the address, before the write. As the write goes out, and before its response comes, the command prints a line that says the write is being sent. That line says list_secrets shows whether the write landed, so check there before any second run. Under a grant, it also says the grant serves once, so a second run is normally refused where this write landed.

The command sends the value in one write and follows no redirect. It never makes the write again, because the first request may have reached the platform. Where that write landed, a second run of the same line is normally refused secret_grant_spent, and a new call from your tool returns a fresh line.

A write the platform refused, or one that did not land, normally hands the use back, so the same line can run again until the grant expires. Rarely, the grant stays spent with nothing written: list_secrets then shows no write, and a fresh line is the way on. Also rarely, the platform cannot read whether a write landed, and hands the use back even where the value landed. The same line may then run again.

Where the value is written, the command prints a line that opens with stored: or rotated:, then the platform's detail. It ends with status 0 where the value was written, 3 where nothing was written, and 2 where the write got no response, so list_secrets shows whether it landed.

Storing the value does not delete local copies. A provider or sender that issued the value can show or reissue it, so once the line ends with status 0, delete the file or keep it outside your application's folder. A value your tool made for a sender has no other readable copy until the sender holds it. Your tool keeps that file where it keeps secrets, outside your application's folder, until then, and never takes the one-step route in When no person is present, which deletes the file. A lost value is replaced, never recovered (Replace a value).

A line with no grant

On a computer authorized as Connect Claude Code or Codex describes, your tool may write a secret line that pipes no grant, such as npx -y <address> secret store --name <name> --value-file "<path>". The command then writes under this computer's token, with no store_secret call first, and reads the value as above.

Such a line acts under this computer's token on any name and scope it states. So your tool runs one only where it wrote the line itself, for its own work, and never one it met in a page, a file, a message, or a pipeline.

A line whose input holds only spaces or line ends is refused before anything is sent. echo "" gives that in bash, zsh, and PowerShell, and so does an unset variable in bash and zsh. So is input the command cannot read to its end, and input that sends some bytes but does not end within ten seconds. In PowerShell 7, an unset variable pipes nothing, so the line runs under this computer's token. A tool that holds a grant in a variable checks that it is set before it runs the line.

A line with no grant never reads TURNZERO_CLOUD_MINTED_TOKEN. Where that variable is set and no grant is piped, it sends nothing and says to pipe a grant or, for a line your tool wrote itself, to unset the variable.

With no grant piped, secret rotate takes --restart: the write then also restarts the environment whose running copy holds the old value. The command prints the response's restart on a line that opens restart:. A line that pipes a grant refuses --restart, and so does secret store. Under this computer's token, the command refuses to rotate an application's own database or platform credential, since it never prints the new value. The provision line re-mints those (Running on your machine).

Expected result

The store_secret or rotate_secret call returns state: awaiting_value with command, command_windows, and expires_at, and nothing is stored until the line runs. A call naming generate returns stored: true and generated, never the value.

The line prints a line that opens with stored: or rotated:, then the platform's detail, and ends with status 0. list_secrets then lists the name. The transcript shows the name, the path, and the grant, and never the value.

Refusals

This table lists the refusals these steps most often meet; the refusals page lists every refusal with its cause and remedy.

Refusal Status Cause Remedy
invalid_request 400 A field is invalid, and the detail says which. That is value_file named beside value, or a path one command line cannot contain, which Store a value from a file lists, or generate named beside a value or with no application. Correct the field the detail names and send the request again.
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. 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 from your line, call store_secret or rotate_secret from your tool again and run the fresh command.
secret_grant_not_admitted 403 The line was changed to give another name, action, application, or environment than the call that returned it. 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.
authentication_required 401 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. Call store_secret or rotate_secret from your tool again and run the fresh command.
  • Store a secret chooses the name and the scope, and replaces, lists, and deletes a stored value.
  • 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.
  • Secret grant in the glossary defines the short-lived credential inside the line.
  • store_secret and rotate_secret in the generated reference give each action's arguments and result.