> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tai42.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Secret values

> Wrap a value a tool returns so it reveals once to a live caller and masks everywhere it is recorded.

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](/concepts/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:

```python theme={null}
from tai42_contract.secrets import SecretValue

return {"endpoint_id": endpoint_id, "secret": SecretValue(raw_secret)}
```

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.

| Surface                                                                            | What it gets                                 |
| ---------------------------------------------------------------------------------- | -------------------------------------------- |
| A live run-tool HTTP response                                                      | the real value, once                         |
| An MCP `tools/call` result                                                         | the real value, once                         |
| A [preset](/concepts/presets) wrapping such a tool                                 | the real value, once                         |
| A model- or agent-visible tool result                                              | `[secret]`                                   |
| A run record, trace, or monitor span                                               | `[secret]`                                   |
| A detached run that stays in-process — a background submit, a hook or trigger fire | records `[secret]`                           |
| A run relocated to a backend worker — a scheduled run, a backend-worker task       | fails loudly: the wrapper will not serialize |

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.

<Note>
  When a preset pins an [output schema](/concepts/presets) 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.
</Note>

## Sensitive questions

[`ask_user`](/concepts/interactions) 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:

```python theme={null}
from tai42_contract.app import tai42_app
from tai42_contract.secrets import SecretValue


@tai42_app.tools.tool
async def mint_signing_secret(label: str) -> dict:
    """Mint a signing secret and return its id and one-time value."""
    created = await provider_mint(label)
    return {
        "secret_id": created["id"],
        "secret": SecretValue(created["value"]),
        "label": label,
    }
```

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:

```python theme={null}
@tai42_app.tools.tool
async def store_signing_secret(label: str) -> dict:
    minted = await tai42_app.tools.run_tool("mint_signing_secret", {"label": label})
    tai42_app.config.config_manager.write_env(
        {"VENDOR_SIGNING_SECRET": minted["secret"].reveal()}
    )
    return {"secret_id": minted["secret_id"], "stored": True}
```

The [Stripe tools plugin](/plugins/tai42/tools-stripe) 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](/concepts/config-and-secrets) for the stored-secret
side, [presets](/concepts/presets) for wrapping a secret-producing tool, and
[interactions](/concepts/interactions) for collecting one from a person.
