Skip to content

Secrets

Kelly Ferrone edited this page Sep 12, 2026 · 4 revisions

Secrets

A secret is a named set of keys — nextcloud with username and password, say — that an operator put on the server. A caller can use one and can never see one. No tool, resource or report returns a value.

That is the whole point. A password an agent holds is in its transcript, its tool call, and whatever its host logs. A password it merely names is in none of them.

Putting secrets on the server

Set SECRETS_DIRS to a directory — or several, separated like PATH. Inside, one directory per secret and one file per key, which is exactly the shape Kubernetes mounts a Secret in:

/secrets/
└── nextcloud/
    ├── username
    ├── password
    └── .meta.yaml        # optional

The file name is the key and the file's contents are the value. .meta.yaml describes the secret without changing it:

description: Admin login for the homelab Nextcloud
allowed_urls:
- https://nextcloud.example.com

Unset SECRETS_DIRS means the feature is off, and list_secrets says so rather than returning an empty list that looks like an empty catalogue.

What a caller sees

list_secrets — or the secret://secrets resource — returns names, keys and sites. Never values.

Field Means
keys what may be named as key
allowed_urls the only sites it may be typed on, as exact origins
restricted: false no sites declared: usable anywhere
allowed_urls_rejected its site list is broken, so it cannot be used at all until an operator fixes it

Binding one

Give write a secret instead of text:

write(css="#password", secret={"name": "nextcloud", "key": "password"})

The result carries "text_from": "secret" and no value. The same argument goes in a flow step's args — see Flows — where the step's line in the report reads text=<hidden>.

Five rules, each refused rather than guessed at:

  • Only write takes a secret, and the rule is structural rather than written down: no other action has a secret argument, so there is nowhere to put one. It is the action that types a value into a field and does nothing else with it.

  • A secret is never part of a string. Flow parameters are written ${name} into any argument; a secret is not, and could not be. So no flow can send one somewhere by writing it into a URL or a script — a secret reaches exactly one place, the field it is typed into.

    That is not the same as the value being safe once typed, and the difference is worth knowing. A write with submit: true can land the browser on ?q=<what was typed>, which is why the server scrubs a typed secret out of every report and error and refuses to store that page as somewhere to return to. See What this does not protect.

  • A secret's name may not come from a parameter. secret: {name: ${which}} is refused: choosing which credential gets typed is not a decision a caller's parameters may make.

  • Never text as well. Two values, one field, and picking one would be a guess that is wrong silently.

  • Navigate first, in its own step. A write binding a secret may not also take a url, because the site check reads the page the browser is on at the moment of typing — a write that navigated would be checked against the page it was leaving.

Allowed sites

A restricted secret is typed only when the browser is on one of its allowed_urls, compared as an exact origin: scheme, host and port. https://nextcloud.example.com.evil.test is not a match, and neither is plain http://. A refusal names the page the browser was on and the sites it would have accepted.

A parameter or a secret?

A parameter is for what varies between runs; a secret is for what must not be seen. An email address is a parameter. Its password is a secret.


Flows · write · Deployment

Clone this wiki locally