Skip to main content
Some tools return a value that must reach a live caller but must never be written down — a signing secret a provider shows only once, a token, a password a person just typed. The secret-value envelope carries such a value through the platform so it is revealed exactly once, at the live edge that asked for it, and shows a placeholder on every surface that records. Where config and secrets covers the secrets a server reads (referenced by env-var name, never embedded), this page covers the secrets a tool produces — the outbound half.

The envelope

SecretValue, from tai42_contract.secrets, wraps a real value inside a tool’s return:
The wrapper is deliberately hard to leak:
  • reveal() is the only way out. wrapped.reveal() returns the real value; nothing else does.
  • Its text form is a placeholder. repr and str both yield SecretValue([secret]), so it cannot leak through a log line or an f-string.
  • It is not JSON-serializable — on purpose. Any un-audited path that tries to dump it raises TypeError and fails loudly, rather than quietly emitting the value. This fail-safe is what makes the wrapper safe to carry through code that was never told a secret is inside.
  • It compares and hashes by identity, not by value, so equality checks and set membership cannot become a timing or logging trap.

What each surface sees

The wrapper travels intact through the platform’s shared tool-result seam. Each door then decides for itself: a live edge reveals the real value once; every recording masks it to the [secret] placeholder. The one handoff of a real secret is therefore a live, synchronous call — a direct run-tool response, an MCP tool call, or a preset over one. A detached run that still executes in-process — a background submit, a hook or trigger fire — has no live caller to hand the value to, so it only records, and records the placeholder. A run relocated to a backend worker — a scheduled run, a backend-worker task — never reaches that in-process recorder: the wrapper does not serialize onto the wire, so the run fails loudly rather than carry a secret across the process boundary, by design. Plan a secret-producing tool to be called synchronously by whoever consumes the value. A model never sees a secret either: the adapter that feeds an agent’s model, checkpoint, and callback trace masks the wrapper before the result leaves it.
When a preset pins an output schema and a secret-bearing result violates it, the error keeps the failing JSON path but replaces the value with [secret]. The plaintext never rides a raised validation error into a log.

Sensitive questions

ask_user can collect a secret from a human. Pass sensitive=True and:
  • the answer comes back wrapped in a SecretValue, so the tool reaches it only through reveal();
  • the durable question record keeps only the answered status — the answer body is never persisted;
  • if the turn is killed on its timeout while a sensitive question is still pending, the timeout error names the interaction but shows [sensitive question] in place of the question text.
The question text, though, is always visible wherever the question is delivered and in the stored record. Never put a secret in the question itself — ask for the secret, do not restate one.

Author rules

  • Wrap at the return site. Construct the SecretValue where the value is produced and put it straight into the return. One wrap protects the value on every surface downstream; there is nothing else to remember.
  • Consume a secret where you receive it. Reveal it and act on it — persist it through the config API, hand it to the provider call that needs it — in the same live call that produced it. Do not pass a wrapper onward as a task or chain result: cross-process transit is unsupported and fails loudly (the wrapper will not serialize onto the wire), by design, so a secret cannot silently ride a background handoff.
  • Do not wrap shareable values. Ids, endpoint URLs, links, and other non-secret fields stay plain, so callers and models can read and act on them. Wrap only the value that must not be recorded.

Example

A generic tool that mints a one-time secret returns it wrapped, and writes it nowhere itself:
Run it from the Studio or the run-tool API and the response carries the real secret once. A second tool consumes it in the same synchronous call — revealing it only to write it into the env under a name the server later reads it back by:
The Stripe tools plugin ships this pattern for a real integration: creating a webhook endpoint returns the endpoint id plus its one-time signing secret wrapped in the envelope, so the secret reaches the caller that persists it but is written to no config, env, or file along the way. See config and secrets for the stored-secret side, presets for wrapping a secret-producing tool, and interactions for collecting one from a person.