TILLSECRETS · SECRETLESS BROKER

Use a secret without knowing it.

Beta

Using a secret with an AI agent usually means putting the plaintext where the agent can read it — a chat message, a file it opens — so the value enters the model's context, the transcript, and every log. The broker inverts that: the agent works with a stable, non-sensitive reference; TillSecrets materialises the real value only at the outbound egress boundary, with your consent, against an allow-listed destination, and audits every use. The value never touches the model, the chat, the agent's memory, or a log.

01The handle

References

A reference is a URI-shaped handle that is safe to appear anywhere — chat, prompt, file, PR, log — because it is not the value and reveals nothing beyond the name (and the name is already governed by the Secrets-layout egress rules). It resolves only inside the broker, and only for an entitled actor with consent; it is inert everywhere else.

reference forms
secretref://<project>/<env>/<KEY>      # canonical
{{ts:<project>/<env>/<KEY>}}           # terse, for env / template files

# e.g.
secretref://infra/prod/NEON_DATABASE_URL

project and env are your TillSecrets slugs; KEY is the secret's env-var name. Only static secrets are brokerable today (dynamic leases resolve through their own leasing flow).

02What happens at spend

The fail-closed chain

Every spend — a generic HTTP request or a catalog action — passes the same checks, in order, and stops closed at the first failure:

  1. SSRF — the destination must be a concrete, public HTTPS host (never a private / metadata address).
  2. Resolve — the reference must name a real secret in your org.
  3. Entitlement — the acting human must be able to read the secret at its sensitivity tier (a restricted, prod-grade secret needs owner; others need admin).
  4. Destination allowlist — the host must be on the secret's own allowlist. Enforced above consent — see below.
  5. Consent — a broker grant must cover (secret, agent, host) and still be usable.
  6. Claim — one use of the grant is claimed atomically; a revoked/exhausted grant fails closed here.
  7. Inject — the plaintext is decrypted in the broker and substituted into only the bytes that leave.
  8. Response-scrub — any echo of the value in the reply is re-tokenised back to its reference before the caller sees it.
  9. Audit — the spend is recorded (reference, destination, agent, approving human) — never the value.
The value is never returned
The plaintext exists only for the microseconds of the outbound request and is never handed back to the agent. For the strongest posture, run the broker on your own machine via the local-agent bridge, so a high-value secret never leaves your host at all.
04Local dev

secrets exec

On your own machine, secrets exec materialises references into a child process's real env at launch — never printed, never in your shell's env. Keep a .env.refs of references (safe to commit) instead of a .env of secrets:

.env.refs
# .env.refs — values are references, not secrets. Safe to commit.
DATABASE_URL=secretref://infra/prod/NEON_DATABASE_URL
STRIPE_SECRET_KEY=secretref://payments/prod/STRIPE_SECRET_KEY
run it
# Run a command with the references materialised into ITS process env only —
# never printed, never in your shell's env. Fail-closed: if a reference can't
# resolve (or you're not entitled), the command does not run.
tilldev secrets exec --env-file .env.refs -- npm run migrate

# Inspect what a file references (metadata only — no values ever):
tilldev secrets refs .env.refs

Each materialisation is entitlement-checked and audited to a local destination. If any reference can't resolve, the command does not run (pass --allow-partial to override).

05Strongest posture

The local bridge

The broker's safest home is the local-agent bridge. When the bridge runs your agent, it stands up a loopback detokenising endpoint on 127.0.0.1 and injects two variables into the agent's environment: TILLDEV_BROKER_URL and a per-run TILLDEV_BROKER_TOKEN. The agent posts an outbound request that carries a reference to $TILLDEV_BROKER_URL/spend — the same shape as the cloud endpoint — and the real value is injected into the outbound bytes on your machine.

spend through the local bridge
# On the local-agent bridge, the shim runs on 127.0.0.1 and the agent's env
# carries TILLDEV_BROKER_URL + TILLDEV_BROKER_TOKEN. The agent posts an outbound
# request that carries a reference to the LOCAL endpoint — same shape as the cloud
# /broker/spend — and the value is injected + spent on THIS machine.
curl -X POST "$TILLDEV_BROKER_URL/spend" \
  -H "authorization: Bearer $TILLDEV_BROKER_TOKEN" \
  -H "content-type: application/json" \
  -d '{
    "method": "POST",
    "url": "https://api.stripe.com/v1/charges",
    "headers": { "Authorization": "Bearer secretref://payments/prod/STRIPE_SECRET_KEY" },
    "body": "amount=2000&currency=usd"
  }'
# The value never enters the agent's context, and the request body never leaves
# your host — only the destination host + the reference reach TillDev to authorise.

Only the destination host and the non-sensitive references reach TillDev, which runs the full chain and authorises the spend as bridge:<device> (audited as a bridge.spend). The plaintext and your actual API traffic never transit the cloud — the leakage surface for that class of task drops to near-zero. The shim is on by default; disable it per device with TILLFORGE_BRIDGE_BROKER=off.

06Curated executors

Catalog actions

Some privileged actions don't map to a single HTTP request — a database migration, for instance, speaks the Postgres wire protocol. A catalog action is a curated executor that performs the action itself and returns a result (rows applied, a deployment id) — never the materialised secret. It runs the same authorize → consent → allowlist → audit chain.

apply a migration by reference
# Catalog action: apply a migration to a database named ONLY by reference.
# The connection string never reaches your terminal or the agent.
tilldev secrets migrate \
  --ref secretref://infra/prod/NEON_DATABASE_URL \
  --file packages/db/drizzle/0043_tillsecrets_broker.sql
Connection-string actions
For a connection secret (the destination host lives inside the value), the secret must have exactly one concrete allow-listed host. The broker checks consent against that host before revealing, then verifies the revealed connection string points at it — a drifted value is refused, unused.

The second flagship action deploys a Cloudflare Worker: the agent supplies the script and names the API token only by reference. Because the destination is a real, fixed HTTPS host, it runs the standard chain against api.cloudflare.com (which must be on the token secret's allowlist), uploads the Worker, and returns the deployment result — the token is spent, never seen.

deploy a Worker by reference
# Catalog action: deploy a Cloudflare Worker with the API token named ONLY by
# reference. The token never reaches your terminal or the agent — the broker
# reveals it against api.cloudflare.com (which must be on the token's allowlist),
# uploads the Worker, and returns the deployment result.
tilldev secrets deploy-worker \
  --ref secretref://infra/prod/CLOUDFLARE_API_TOKEN \
  --account 0123456789abcdef0123456789abcdef \
  --name my-worker --file dist/worker.js --compat-date 2026-07-01

Anything outside the catalog falls back to the generic broker: post the outbound request to /api/secrets/broker/spend with the reference in a header or the body, and the broker injects the value toward the one allowed host.

POST /api/secrets/broker/spend
# The server-side detokenising proxy. Post an outbound request that carries a
# reference; the broker injects the real value into ONLY the bytes that leave and
# returns a scrubbed response. The value never comes back to the caller.
curl -X POST https://tilldev.dev/api/secrets/broker/spend \
  --cookie "$SESSION" -H "content-type: application/json" \
  -d '{
    "method": "POST",
    "url": "https://api.stripe.com/v1/charges",
    "headers": { "Authorization": "Bearer secretref://payments/prod/STRIPE_SECRET_KEY" },
    "body": "amount=2000&currency=usd",
    "agent_key": "ask-x"
  }'
07From your code

The broker SDK

The @tillstack/secrets-node and @tillstack/secrets-edge packages ship a first-class broker client so application and agent code can spend references without hand-rolling the wire calls. secretref() builds validated handles; broker.fetch() returns the scrubbed response or a structured refusal — the value never enters your process.

@tillstack/secrets-node
import { createBroker, secretref } from '@tillstack/secrets-node'

// auto mode: uses the LOCAL bridge when running under the TillForge daemon
// (value never leaves the machine), else the cloud broker with your token.
const broker = createBroker({ agentKey: 'billing-job' })

const res = await broker.fetch('https://api.stripe.com/v1/charges', {
  method: 'POST',
  headers: { authorization: `Bearer ${secretref('payments', 'prod', 'STRIPE_SECRET_KEY')}` },
  body: 'amount=2000&currency=usd',
})

if (!res.ok) throw new Error(`refused: ${res.refusal?.code}`)
// res.body is the scrubbed reply — STRIPE_SECRET_KEY never entered this process.
Same client, strongest posture for free
In auto mode the Node client routes through the local bridge whenever it runs under the TillForge daemon (the daemon injects TILLDEV_BROKER_URL), so the value is spent on your machine and never reaches the cloud — no code change. Off the bridge, it uses the cloud broker with a session or tp_ API key plus an agent identity (never the ts_ service token — the broker acts on a human's entitlement, not a machine's).
08Evidence

Every spend is audited

The broker keeps an append-only spend ledger: one row per attempt — successful or refused — with the reference, destination, agent identity and approving human, but never the value. Review it with tilldev secrets broker spends or in the Secrets console. It is the compliance backbone of “the agent used this key, against this host, at this time, with this consent.”

Refusals are recorded too
A refused spend (needs consent, off-allowlist, not entitled, SSRF, drifted host…) is logged with its reason, so an agent probing for a secret it can't use leaves a trail.