Secrets
The idea
An agent often needs an API key: to call Stripe, to push to a registry, to reach its own model. If the key is in its environment, the agent (or a prompt injection) can print it, write it to a file, or send it anywhere.
With Kule, the agent never sees the value. It sees a placeholder, and the task's proxy swaps the placeholder for the real value only in requests to the hosts you named.
Keep a secret
kule secrets add STRIPE_KEY --host api.stripe.com
- The value is read from the terminal without echo, or from standard
input (
… < key.txt). Never from the command line, where it would land in your shell history. --hostnames where it may go (repeat it for several hosts).- By default a secret is for the folder you run the command in; use
--folder PATHfor another, or--everywherefor every folder.
kule secrets # list them, without values
kule secrets remove STRIPE_KEY
In the app: Settings → Secrets.
What a command sees
kule exec TASK -- sh -c 'echo $STRIPE_KEY'
# prints a placeholder, not your key
On the way to api.stripe.com, the proxy puts the value in place of the
placeholder in a credential header: Authorization (Basic
included), x-api-key, x-goog-api-key and the like. Everywhere else it
stays a placeholder:
- in the URL, in any other header, and in the body, so the value never reaches what could repeat it back;
- sent to any other host, it is a string that opens nothing.
A request to that host carrying credentials Kule does not hold for it is
refused (403). A response that repeats the value has it blanked out with
*, as far as the proxy can read it.
The connection log names the secrets that went out:
net: api.stripe.com:443 · tunneled · with STRIPE_KEY
To do this, the proxy looks inside TLS for the secret's hosts and no
others. It makes a certificate authority of its own for each run, valid
only for those hosts by its name constraints, with a key that never leaves
its memory, and points SSL_CERT_FILE, NODE_EXTRA_CA_CERTS,
REQUESTS_CA_BUNDLE and the like at it.
Agents' API keys
The built-in agents take ANTHROPIC_API_KEY, OPENAI_API_KEY and
GEMINI_API_KEY the same way: the agent's variable holds a placeholder of
the run, and the proxy fills in your key for the agent's own API host. A
profile you write can do the same under keys (see Agents).
.env files are masked too
Values in the copy's .env files (.env, .env.local, *.env, .envrc;
not .env.example) that look like secrets, by their name or their shape,
are replaced with placeholders:
- when the task is made, after each commit, rollback and undo, and before every run;
- the folder keeps the real ones: a commit puts them back into a settings file the agent changed, and merges with the real values;
- the task keeps the values it masked in its own
secrets.json, sealed; - a masked value that is also a kept secret gets that secret's placeholder, so it reaches the secret's hosts. Any other stays a placeholder.
The same masking applies to the home layer's clones of
.zshrc, .profile, ~/.env and the like.
Where secrets are stored
Secrets live in Kule's home, hidden from every sandbox. Their values are
sealed with a key that your system's keychain (macOS) or Secret
Service (Linux) keeps. A computer without one, or a Kule home other than
~/.kule, keeps the key in a file beside; Settings → Secrets says so.
What it does not do yet
- HTTP/2 (clients fall back to HTTP/1.1), and WebSocket upgrades.
- A placeholder split by a chunk edge, or in a body larger than 1 MiB.
- Tools on macOS that check certificates only through the system's own
services (pip 24.2 and later without
--use-deprecated=legacy-certs). Kule never adds its authority to the keychain.