Skip to main content

MCP servers and secrets

This page documents two command groups: mcp, which manages hosted MCP (Model Context Protocol) tool servers, and secrets, which covers the project secret store.

Read this first: secrets names two different resources

The secrets group carries two resources that have almost nothing in common beyond the word in their name. Get them confused and you will look for a version history that does not exist, or reveal a value that was never there.

platformctl secrets setEvery other secrets subcommand
What it writesAn agent environment secretA project secret
Owned byOne agentThe project
ShapeKEY=VALUE pairs, one value eachOne named value per secret
HistoryNone — a write replaces the valueEvery write is a new version
Who can read itOnly that agent, as an environment variableAny agent in the project that binds it, plus secrets reveal
Reading the value backNot possible, eversecrets reveal only — project admin, and audited
Documented inAgents and functionsThe project secret store, below

The CLI's own group help says the same thing:

platformctl secrets --help
'secrets set <agent> KEY=VALUE' writes a per-agent environment secret (one
value, no history).

Every other subcommand operates on the PROJECT secret store: versioned values
owned by the project, which many agents can bind. Values are never returned by
list or show - use 'secrets reveal'.

How both groups behave

Both mcp and the project-secret commands talk to the core platform API, so they resolve their endpoint the usual way — --api, then $CAI_API, then the public API. See the CLI overview.

They require a project. These routes carry the project in the URL path, so there is no server-side default to fall back on. With nothing set:

this command needs a project: pass --project, set $CAI_PROJECT, or run 'platformctl config set-project' (use the id or slug shown by 'platformctl projects list')

A project UUID is used as-is with no extra call. A slug, short id, or name costs one GET /v1/projects lookup on the same connection; if nothing matches:

no project matches "my-proj" among the projects you can access (see 'platformctl projects list')

Roles differ per command. Reading is a project member's right; the irreversible actions are a project admin's.

Project member mayProject admin is required for
mcp list, mcp get, mcp tools list, mcp versionsmcp create, mcp delete, mcp tools set, mcp tools delete, mcp rollback, mcp yank, mcp unyank
secrets list, secrets show, secrets versions, secrets put, secrets issue-tokensecrets reveal, secrets delete

Writing a project secret is deliberately a member's right — a write is versioned and therefore reversible, and a developer who cannot store their own API key stores it in the source instead. Revealing and deleting are neither reversible nor implied by "may use this project's resources", so both are admin.

What a rejection actually looks like. The CLI does not unwrap the server's error envelope. Any 4xx or 5xx prints as workload-api returned <code>: followed by the raw JSON body, and on a 401 or 403 the CLI appends the credential it actually sent. Running secrets reveal without the admin role:

Error: workload-api returned 403: {"error":"this action requires the project admin role","request_id":"4b1e77a0c2d93f58"} [platformctl credential: cached login (/Users/you/Library/Application Support/crusoe-ai/token)]

For readability, the rest of this page quotes only the error field from that body — the part worth reading, and the part you would grep for.


platformctl mcp

An MCP server is a hosted, scale-to-zero container that agents and external MCP clients attach to in order to call tools. Three things are worth holding in your head before you start:

  • A server is a container for tools. mcp create makes an empty one. It has no tools, no image, and nothing running.
  • A tool is Python source that the platform builds into the server. mcp tools set is the only write that produces a running server.
  • A version is an immutable snapshot. Every publish renders the server's whole current tool set into a new version, pinned to an image digest. That is why mcp rollback needs no rebuild and why mcp versions is the record of every publish.

There is no "update the server" command, because there is nothing on a server to update apart from its tools.

mcp list

platformctl mcp list
platformctl mcp list

You should see:

NAME STATE EXPOSE VISIBILITY TOOLS URL
weather-tools ready apps private 2 https://mcp-weather-tools-k7m3qz.apps.codyhill.dev
billing-tools pending internal private 0 -

Two columns are easy to misread:

  • STATE is one of pending, building, deploying, ready, or failed. A server that has never had a tool published reads pending — the API leaves the field empty in that state, and the CLI prints the word rather than a blank cell. In -o json a ready boolean sits beside it, true only at ready: the word is what you show, the boolean is what a script branches on, and it is the same field spelled the same way that agents, functions, and the data services answer with.
  • VISIBILITY reads private on every server today. The column exists because the underlying field allows org and public, but neither is available on the platform. Do not read private as "not reachable from other projects" — see connect agents and clients for what actually protects the endpoint.

An empty project prints:

no mcp servers

mcp get

platformctl mcp get <name>
platformctl mcp get weather-tools

The default table is the generic object view: one flattened field per line, keys sorted.

created_at 2026-08-12T18:10:03Z
expose apps
image registry.us-east1-a.ccr.crusoecloudcompute.com/cai-ab12cd-mcp-weather-tools@sha256:0f6b2c4d…
name weather-tools
ready true
state ready
tool_count 2
tool_names forecast, geocode
url https://mcp-weather-tools-k7m3qz.apps.codyhill.dev
version 3
visibility private

Notes on the fields:

  • image is the pinned digest the current version was built to. It sits in a repository in your own Crusoe Cloud Registry, created for this server on its first publish. (The digest is abbreviated above; the real output prints it in full.)
  • url is the base address. MCP clients speak to it at the /mcp path, and GET /healthz is served without a token. See connect agents and clients.
  • version, image, and url are absent until the server's first successful publish; message appears only when there is a build or deploy error to report.

Use -o json when you want the credential-key union and the full digest without truncation.

mcp create

Creates an empty server. Nothing is built and nothing is deployed until you publish a tool.

platformctl mcp create <name> [--expose]
FlagDefaultWhat it does
--exposeoffPublish the server on the apps domain. Off means internal: reachable from inside the platform and not from the public internet.

The name must be a lowercase DNS label of 40 characters or fewer; anything else is a 400:

missing or invalid 'name' (must be a lowercase DNS label, <=40 chars)
platformctl mcp create weather-tools --expose

You should see:

created_at 2026-08-12T18:10:03Z
expose apps
name weather-tools
ready false
state pending
tool_count 0
tool_names -
visibility private
Exposure is fixed at creation

There is no mcp update, and no API route behind one. --expose is decided here and cannot be changed afterwards from the CLI, the console, or the API — to switch a server between internal and published you delete it and create it again, which means republishing its tools and losing its version history. Decide before you publish.

Either way the endpoint authenticates every request with a per-server bearer token. "Internal" is not "safe to leave open."

Creating a name that is already taken is a 409:

an mcp server named weather-tools already exists in this project

mcp delete

platformctl mcp delete <name>
platformctl mcp delete billing-tools
deleted billing-tools

This tears down the running server and removes its tools and its version history. The images built for it are not reachable afterwards, so there is nothing left to roll back to. There is no confirmation prompt.


platformctl mcp tools

mcp tools list

platformctl mcp tools list <server>
platformctl mcp tools list weather-tools

You should see:

NAME HANDLER DESCRIPTION
forecast 18 lines Return tomorrow's forecast for a place.
geocode 12 lines Resolve a place name to latitude and longitude.

The HANDLER column reports the source's size, not its content — a handler is a whole Python module, and printing it would destroy the table. -o json carries the full source, the stored schema, and the credential keys. Long descriptions are collapsed onto one line and elided past 60 characters.

An empty server prints:

no tools published on this server

mcp tools set

Publishes a tool — creating it or replacing it — and rebuilds the server.

platformctl mcp tools set <server> <tool> --handler <source|@file|-> [flags]
FlagDefaultWhat it does
--handlernone — requiredThe tool's Python source: a literal string, @path to read a file, or - to read stdin.
--descriptionkeeps the current valueOne line describing the tool, shown to MCP clients in tools/list.
--credential-keykeeps the current keysA project-secret name this tool may fetch at call time. Repeatable.
--schemakeeps the current valueAn explicit JSON parameter schema for the catalog, inline.
--schema-filekeeps the current valueThe same value, read from a file.

Because a tool is a complete module, @file is the form you will use:

platformctl mcp tools set weather-tools forecast \
--handler @tools/forecast.py \
--description "Return tomorrow's forecast for a place." \
--credential-key weather-api-key

You should see:

build_id 6c2f0f5a-6c8f-4b31-9a2e-1c5f0d9a7b44
name forecast
note building a new immutable version from the server's current tool set
published true
server weather-tools

The response is a build id, not a finished deployment. The build runs asynchronously. Poll mcp get <server> until state reaches ready — or, in a script, until ready is true — or watch mcp versions <server> for the new row.

It is a read-modify-write, so an unpassed flag is not a cleared flag

This is the single subtlest thing on the page, and getting it wrong has broken real servers.

The API's tool write is a full replace — every stored column is overwritten from the request body. So before sending, the CLI reads the tool back and re-sends whatever you left out unchanged. A flag you do not pass keeps its current value. It is not cleared.

That behavior is deliberate, and the dangerous field is --credential-key. Publishing recomputes the server's credential-key union from what the write stored, so if omitting the flag meant "no keys", an edit that only touched the description would ship a version that no longer injects the secret — and the tool would start failing at call time, in production, for a change you thought was cosmetic. The console merges the same way, so an edit means the same thing in both places.

Clearing is therefore explicit, and each field has its own spelling:

To clearPass
The description--description ""
Every credential key--credential-key=
A pinned schema--schema '{}'

Note the = in --credential-key=. A repeatable flag cannot be given with no value at all, so an empty value is the only way to spell "this tool needs no secrets"; blank values are dropped rather than stored as a key whose name is the empty string.

To change the set of keys rather than clear it, pass the complete new set — the flag replaces, it does not append:

platformctl mcp tools set weather-tools forecast \
--handler @tools/forecast.py \
--credential-key weather-api-key \
--credential-key geocode-api-key

Credential keys are what let a tool read a secret

A credential key is the name of a project secret this tool is allowed to fetch while it is running. Values are never mounted into the server, never baked into the image, and never stored on the tool. The declared names travel from the tool, into the version snapshot, into the running server's configuration; at call time the tool asks the platform for the value with a short-lived, scoped token.

In the handler that looks like this:

import crusoe_mcp as crusoe


@crusoe.tool(credential_keys=["weather-api-key"])
def forecast(place: str) -> str:
"""Return tomorrow's forecast for a place.

Args:
place: The place to forecast, e.g. "Reykjavik".
"""
api_key = crusoe.secret("weather-api-key")
...
A tool published without its credential keys fails closed

crusoe.secret(...) fails closed. If the key was not declared when the version was built, the call raises at request time rather than returning an empty string — so the failure shows up as a broken tool call for a user, not as a warning at publish time. The decorator's credential_keys and the CLI's --credential-key must both name the secret. Check mcp versions <server> -o json and read credential_keys on the current row if a tool starts failing to read a secret it used to read.

The secret itself must already exist in the project store — see secrets put below.

Schemas are usually not yours to write

--schema and --schema-file are optional and most servers never use them. The platform infers the parameter schema MCP clients actually see from your handler's signature and docstring at build time. The stored schema is catalog metadata, captured into the version snapshot; it is not what ships to clients. Leave both unset unless you specifically need to pin one.

Passing both is an error:

--schema and --schema-file are two ways to give the same value: pass one

so is malformed JSON, rejected locally before anything is stored:

the schema is not valid JSON (it must be a JSON object describing the tool's parameters)

Other local errors

Omitting the required flag:

Error: required flag(s) "handler" not set

Pointing --handler at something empty:

--handler resolved to an empty handler: it must be the tool's Python source (a complete @crusoe.tool module)

If reading the tool back fails — for example the server name is wrong — the CLI says why it needed to read it in the first place, rather than silently publishing a body missing your other fields:

reading the current tools on weather-tools (needed to keep the fields you did not pass): ...

mcp tools delete

platformctl mcp tools delete <server> <tool>
platformctl mcp tools delete weather-tools geocode
deleted tool geocode from weather-tools; the server is rebuilding without it

The server is rebuilt from its remaining tools and rolled onto a new version, so the tool keeps being served until that version is live.


MCP versions and rollback

mcp versions

platformctl mcp versions <server>
platformctl mcp versions weather-tools

You should see:

VERSION CURRENT YANKED TOOLS CREATED
3 yes no 2 2026-08-12T18:22:41Z
2 no yes 2 2026-08-11T09:04:12Z
1 no no 1 2026-08-10T16:41:55Z

Each row is an immutable snapshot: a pinned image digest, the tool set it was built from, and that set's credential-key union. Nothing about a published version ever changes except its yanked flag. -o json adds image_digest, tool_names, and credential_keys.

Before the first publish:

no versions yet (publish a tool to build one)

mcp rollback

platformctl mcp rollback <server> <version>
platformctl mcp rollback weather-tools 2

You should see:

image registry.us-east1-a.ccr.crusoecloudcompute.com/cai-ab12cd-mcp-weather-tools@sha256:5a41e8b7…
note re-pointed the server at version 2's recorded image; no rebuild
rolled_back true
server weather-tools
version 2

No rebuild happens. The target version's pinned digest is redeployed exactly as it shipped — that is the whole point of versions being immutable, and it is why a rollback is fast and cannot come back subtly different from what ran before. The server's current version advances to the target.

A version number that is not a positive integer is rejected locally, before any request:

invalid version "abc": it must be a positive integer (see 'platformctl mcp versions <server>')

A yanked version is refused as a target with a 409:

version 2 is yanked (retired); unyank it or publish a new version instead

So is a version with nothing to redeploy:

version 2 has no recorded image to roll back to

mcp yank

Retires a version so it cannot be rolled back to — the control for "this build was bad, make sure nobody puts it back."

platformctl mcp yank <server> <version>
platformctl mcp yank weather-tools 3
server weather-tools
version 3
yanked true

A yanked version stays in the history as a record. It is not deleted and its image is not removed; it is simply refused as a rollback target until unyanked.

You cannot yank the version the server is currently running — retiring what is deployed would leave the running image with no live history entry. That is a 409:

version 3 is the one the server currently runs; roll to another version before yanking it

Roll onto another version first, then yank.

mcp unyank

platformctl mcp unyank <server> <version>
platformctl mcp unyank weather-tools 3
server weather-tools
version 3
yanked false

Worked example: publish a tool, then roll it back

Start from an empty project. Store the API key the tool will need, create the server, publish the tool, watch the build, then retire a bad version.

# 1. The secret the tool will read at call time.
printf %s "$WEATHER_API_KEY" | platformctl secrets put weather-api-key

# 2. An empty server, published on the apps domain.
platformctl mcp create weather-tools --expose

# 3. Publish the first tool. This is what builds and deploys the server.
platformctl mcp tools set weather-tools forecast \
--handler @tools/forecast.py \
--description "Return tomorrow's forecast for a place." \
--credential-key weather-api-key

# 4. Wait for it. The publish returned a build id, not a running server.
platformctl mcp get weather-tools

# 5. Publish a second tool. This builds version 2 from BOTH tools.
platformctl mcp tools set weather-tools geocode \
--handler @tools/geocode.py \
--description "Resolve a place name to latitude and longitude."

# 6. Version 2 turned out bad. Go back to 1, then retire 2.
platformctl mcp versions weather-tools
platformctl mcp rollback weather-tools 1
platformctl mcp yank weather-tools 2

Note the order in step 6: roll off version 2 before yanking it. Yanking the running version is refused.

Note also what step 5 did not need to pass. geocode is a new tool, so nothing was read back for it — but had you been editing forecast instead, leaving --credential-key off would have preserved weather-api-key rather than dropping it.


platformctl secrets set for agent environment secrets

platformctl secrets set <agent> KEY=VALUE [KEY=VALUE...]
platformctl secrets set research-buddy DEMO_TOKEN=abc123
set 1 secret(s) for research-buddy

This is the one agent-scoped command in the group. It writes environment secrets onto a single agent, merging with what is already there, and rolls a new revision. There is no version history, and no way to read the values back — platformctl agents secrets get <agent> lists the key names only.

A malformed pair is rejected before anything is sent:

invalid KEY=VALUE pair: "DEMO_TOKEN"
The value is in your shell history

secrets set takes the value as a command-line argument, so it lands in your shell history file and in the process table while the command runs. That is the opposite of the project store's discipline below. Use it for the throwaway values it is meant for; put real credentials in the project store with secrets put, which has no --value flag for exactly this reason.

Full treatment, including how the merge protects platform-managed keys, is on the agents and functions page. See also secrets and environment variables.


The project secret store

Everything below writes and reads project secrets: named, versioned values owned by the project, which many agents can bind and which running workloads fetch at call time.

Two rules shape the whole surface, and both are worth stating plainly before the commands:

  1. A value is never in a list and never in a show. secrets reveal is the only command that produces plaintext, it is project admin only, and it writes an audit row before the value leaves the server.
  2. Every write is a new version. There is no update-in-place, which is what makes a rotation reversible.

A secret name is 1–63 characters of letters, digits, - or _, starting with a letter or digit. A value must not be empty and is capped at 64 KiB — enough for a certificate chain, small enough that the store does not become a file store.

The name crusoe-cloud-credential is reserved by the platform and is not managed here. It is invisible to secrets list, and every other command reports it as absent with a 404:

no such secret. "crusoe-cloud-credential" is a reserved platform name and is not managed through the project secrets API. If it is a Crusoe Cloud mapping, use GET/PUT/DELETE /v1/projects/{id}/crusoe-cloud instead.

See Crusoe Cloud integration for that surface.

secrets list

platformctl secrets list
platformctl secrets list

You should see:

NAME VERSION UPDATED
stripe-key 4 2026-08-12T18:30:02Z
weather-api-key 1 2026-08-02T11:15:44Z

store path prefix: projects/k7m3qz

Metadata only — name, current version, when it was last written. There is no value in this response and there cannot be, because a routine call that returns plaintext puts plaintext in every log and screenshot of it. Reading a value is a separate, explicit, audited act.

The trailing line names the store path prefix these secrets live under. It answers "which project am I actually looking at", which is the first thing worth knowing in an incident. It is printed only for the table output; -o json and -o yaml carry the items array, matching every other list in the CLI.

An empty store prints:

no project secrets

If the store is not wired up at all, you get a 503 that says so precisely, so you do not read a deployment gap as an empty store:

the project secret store is not configured: workload-api starts it only when BAO_ADDR and BAO_TOKEN are set in its environment, and they are not. This is a deployment gap, NOT an empty secret store - do not read it as 'this project has no secrets'.

secrets show

platformctl secrets show <name>
platformctl secrets show weather-api-key

You should see:

created_at 2026-08-02T11:15:44Z
current_version 1
name weather-api-key
updated_at 2026-08-02T11:15:44Z
versions [{"created_at":"2026-08-02T11:15:44Z","destroyed":false,"version":1}]

It is called show, not get, because it returns no value. There is no flag — here or on the server — that makes it return one.

When agents bind the secret, a used_by row appears listing each binding: the agent's slug, the environment variable name it arrives as, and the pinned version if the binding pins one. -o json is far easier to read for that.

The bindings are the reason to run this before a delete. Nothing in the database points at a secret — the values are not in the database at all — so a delete that breaks an agent would surface much later, as a deploy that fails on a missing environment variable. The server refuses the delete while any binding remains, and this is where you see which ones.

Bindings are not managed from the CLI

secrets show reports bindings, but there is no platformctl command that creates or removes one. Binding a project secret to an agent's environment variable is console or API only — see use secrets in workloads and the secrets API reference.

secrets put

Writes a new version of a project secret. This one command both creates and rotates.

platformctl secrets put <name> [--value-file <path>]
FlagDefaultWhat it does
--value-filestdinFile to read the value from, or - for stdin.
There is no --value flag, and that is the point

A credential passed as a command-line argument is in your shell history file, in the process table for as long as the command runs, and in any shell tracing or auditd rule watching argv. A file or a pipe is none of those things. The flag was left out on purpose; it is not an oversight and it will not be added.

Both supported forms:

printf %s "$TOKEN" | platformctl secrets put stripe-key
platformctl secrets put tls-chain --value-file ./chain.pem

The first time:

created stripe-key at version 1
the value is stored. Nothing - this API included - will show it back to you except an explicit, audited reveal.

Every write after that:

rotated stripe-key at version 2
the value is stored. Nothing - this API included - will show it back to you except an explicit, audited reveal.

The verb tells you which happened, and the version number is the durable fact — it is what a pinned binding names and what secrets reveal --version takes.

Trailing newlines differ between the two sources

This is intentional, and it matters for certificates.

  • --value-file is taken byte for byte. A PEM chain keeps its trailing newline, because a file is a deliberate artifact and its final byte is part of it.
  • stdin has exactly one trailing newline removed. That newline is almost always echo's rather than yours, and an API key with \n welded to the end fails at the far end of a deploy with an error that says nothing about a newline.

If you need stdin byte-exact, pass --value-file /dev/stdin.

Local errors

Running with a terminal on stdin and no --value-file does not hang waiting for a prompt that is never coming:

no value given: pipe it in (printf %s "$TOKEN" | platformctl secrets put NAME) or pass --value-file <path>

An empty pipe, or a pipe carrying only a newline:

the value read from stdin is empty, and a secret value must not be

An empty or unreadable file, reported before any connection is opened:

/tmp/empty.txt is empty, and a secret value must not be
reading --value-file: open /nope/nothing: no such file or directory

secrets versions

platformctl secrets versions <name>
platformctl secrets versions stripe-key

You should see:

VERSION CREATED CURRENT DESTROYED
4 2026-08-12T18:30:02Z yes no
3 2026-08-05T14:02:11Z no no
2 2026-07-28T08:19:37Z no yes

No values here either. A version number is what secrets reveal --version and a pinned binding take, so this is the command that tells you what to pass them. CURRENT folds in the response's current_version field so the live version is visible without cross-referencing two numbers.

secrets reveal

The only command that produces a value.

platformctl secrets reveal <name> [--version <n>] [--reason <text>]
FlagDefaultWhat it does
--versionthe current versionWhich version to reveal.
--reasonnoneWhy you are reading this. Recorded verbatim in the audit row.
platformctl secrets reveal stripe-key --reason "rotating into the payments vendor console"

You should see:

secret: stripe-key
version: 4

sk_live_EXAMPLE_NOT_A_REAL_KEY

this read is in the audit log with your identity, the version and the time.

Three things about this command:

It requires the project admin role. A member who can write the secret cannot read it back. Writes are reversible; a read is not.

It is audited, and the audit is fail-closed. Before the value is returned, the server records who asked, for which secret and version, when, from where, and the --reason you gave. If that record cannot be written, the value is not returned. There is no reveal without a trace. The server's own note about what it recorded is echoed after the value — the line quoted above is the server's text, not the CLI's.

The row shows up in the project's audit trail like any other action:

platformctl audit --limit 20
TIME ACTOR ACTION TARGET
2026-08-12T18:41:07Z you@example.com project.secret.reveal stripe-key

--reason is optional on purpose. A mandatory free-text field is a field everyone fills in with x. A real one is what makes the row worth reading six months from now. Write the real one.

The output always names the version that was actually served, because "latest" moves and whatever you paste somewhere durable does not. In the rare case the server cannot confirm the number after the read, the CLI says so rather than printing a version that does not exist:

version: current (the server could not confirm the number)

See break-glass and audit for how these records are reviewed.

secrets delete

platformctl secrets delete <name>
platformctl secrets delete stripe-key
deleted project secret stripe-key - every version is destroyed, and this is not recoverable

Project admin only. Every version is destroyed and none of it comes back. There is no confirmation prompt and no soft delete.

The server refuses with a 409 while any binding still names the secret:

stripe-key is still bound by 2 binding(s): research-buddy.STRIPE_KEY, checkout-agent.STRIPE_KEY. Remove them first - nothing in the database prevents this delete, so the failure would otherwise appear later, as an agent that will not start.

That refusal is load-bearing rather than paternalistic. Bindings do not reference the secret in the database — the values are not in the database — so nothing else would stop the delete and nothing would report it. Run secrets show <name> to see what still binds it.

secrets issue-token

Mints a short-lived, read-only token scoped to this project's secrets. It returns a token, never a value.

platformctl secrets issue-token [--secret <name>] [--ttl-seconds <n>]
FlagDefaultWhat it does
--secretevery secret in the projectScope the token to exactly one secret.
--ttl-seconds300, capped at 3600Token lifetime. Values above the platform's request-timeout cap are clamped down, not rejected.
platformctl secrets issue-token --secret weather-api-key --ttl-seconds 60

You should see (the token and the store address are elided here):


<the minted token>

expires in: 60s
read addr: <the secret store's internal address>
read path: secret/data/projects/k7m3qz/weather-api-key
this token is short-lived and scoped to read only. It reads secret VALUES directly from the store until it expires; it is never stored here.

The holder reads values directly from the store, for a bounded time, without going through the platform API. That is what a workload needs in order to fetch its own credentials at call time instead of holding them in its environment for the life of the process — it is the mechanism behind crusoe.secret(...) in MCP tools and agents.

The token is printed on its own line so a careless double-click selects the whole credential and nothing else, followed by the coordinates it reads through — a token with no address and path is not usable, and the server is the only thing that knows them.

Two things to get right:

  • Prefer the narrow scope. Without --secret the token reads every secret in the project. With it, exactly one. A leaked token is worth exactly what it can reach before it expires. Scoping to one secret is one flag.
  • Set the TTL to your invocation timeout. Then a secret fetched at any point in a request stays valid to the request's deadline, and the token dies the moment the request could no longer be running.

If the configured store cannot mint scoped tokens, you get a 501:

the configured secret store cannot issue scoped tokens

Worked example: create, rotate, reveal, and delete a secret

# Create. The value comes from a pipe, never an argument.
printf %s "sk_live_..." | platformctl secrets put stripe-key
# created stripe-key at version 1

# Rotate. Same command; the output tells you which happened.
printf %s "sk_live_the_new_one" | platformctl secrets put stripe-key
# rotated stripe-key at version 2

# What versions exist, and which one is live?
platformctl secrets versions stripe-key

# Read it back. Admin only, and this writes an audit row before it answers.
platformctl secrets reveal stripe-key --reason "verifying the rotation landed"

# Read the version you replaced, to compare.
platformctl secrets reveal stripe-key --version 1 --reason "confirming the old key before revoking it upstream"

# Who would break if this went away?
platformctl secrets show stripe-key

# Destroy it. Every version, unrecoverable, and refused while bindings remain.
platformctl secrets delete stripe-key

A PEM chain instead, where the trailing newline must survive:

platformctl secrets put tls-chain --value-file ./fullchain.pem