An agent can ask. Only you can approve.
BetaWhoever holds your session can do what you can, and that includes an AI agent running the CLI on your login. With approvals on, the actions that expose or send out a secret wait until you approve them with a passkey, your phone or a confirmed authenticator code. None of them is something the session can supply, so the agent can ask but cannot approve.
What waits for you
| Action | When |
|---|---|
secret.reveal | Showing a value: Reveal in the console, or tilldev secrets get. |
secret.resolve | Putting values into a command: tilldev secrets exec --env-file. May repeat for a time you choose. |
ssh.sign | Signing with an SSH key held in the vault, through the SSH agent: a login to a server the SSH client proved, or a git or other signature. May repeat for a time you choose. |
ssh.sign_once | The same, when the server is not proven, the agent is forwarded to another machine, or the data is not a login or signature. Always single-use. |
destination.allow | Adding a host a secret may be sent to, or changing its methods or paths. Also fulfilling a secret request that allows hosts. |
grant.create | Consenting to an agent spending a secret. |
presign.create | Presigning a call. |
token.create | Minting a service token (ts_…). |
agent.create | Minting an agent token (ta_…). |
identity.create | Letting a workload sign in without a token: a workload identity. |
identity.update | Changing what a workload identity trusts, its keys, token lifetime or host key. A new name alone does not ask. |
sync.create, sync.enable | Adding a sync target, or resuming a paused one. |
tier.lower | Moving a secret or environment from restricted (7+) to below 7. |
secret.seal | Sealing a secret so nobody can reveal it. See Sealed secrets. |
release.request, release.approve | Asking to reveal or unseal a sealed secret, and another owner approving it. |
anomaly.resolve | Resolving a spend anomaly, so an agent can't silence its own alarm. |
canary.view | Seeing which secrets are canaries. Every time. |
canary.retire, canary.configure | Retiring a canary, or changing where canaries are made. |
repos.erase | Erasing every TillForge repository in the organization and destroying its repository key. Always single-use. |
step_up.off | Turning approvals off for the workspace. |
Machines are not slowed down. A service token's pull, a broker spend through a grant, and an agent token work as before; the approvals happened when the token, identity, grant or destination was created. The one exception is SSH signing: each signature asks the person who made the token, as it would ask you. A platform API key (tp_…) cannot approve anything, so it gets 403 on these actions.
Add a way to approve
Open Approvals in the console sidebar (or tilldev approvals open). First prove it is you, with your password, a link sent to your email, an existing passkey or a confirmed code. Then add a passkey, pair your phone, or confirm the authenticator app you already sign in with.
Adding or removing a way to approve sends an email to you. Every request, approval, denial and use is in the audit log.
Approve from your phone
Under Approvals → Phone, choose Pair a phone and scan the code with Till Authenticator (tap +), or open the link on the phone itself. The code works once, for 10 minutes. The phone makes a signing key that stays in its secure storage; the vault keeps only the public half.
- On the approval page, choose Send to my phone. The page shows a two-digit number.
- Open Approvals in Till Authenticator.
- It shows the request: what it does, the workspace, where it was asked from, and any repeat window you chose on the page.
- Pick the number from the page and unlock with Face ID, a fingerprint or your passcode. The page carries on by itself.
Picking another number denies the request at once, so there is no second guess. The phone signs its decision, including the repeat window, and the vault checks that signature. The phone has 2 minutes to answer; send it again if it lapses.
Remove a phone under Approvals → Phone (this needs the same proof as adding one), or unpair it from Till Authenticator. Pairing and removing each send you an email.
In a terminal, it waits
A command that needs an approval prints a link, opens it in your browser (unless --no-open), and carries on once you approve.
$ tilldev secrets exec --env-file .env.refs -- npm run migrate
! Needs your approval: Put DATABASE_URL, STRIPE_KEY into `npm run migrate`
https://tilldev.dev/acme/approve/5b0c…
Waiting while you approve it in the browser, or on your phone from there. Ctrl+C stops waiting; the request expires in 10m.
# approve with your passkey, and the command carries onWithout a terminal it exits with code 3 and the link straight away, so a script or agent can hand the link to you. The CLI keeps the claim in ~/.tilldev/approvals.json (mode 0600), so running the same command again uses your approval. Running it again before you approve points at the same request rather than opening another.
# No terminal (CI, an agent's shell): exit code 3 and the link, at once.
tilldev secrets get STRIPE_KEY --env prod --json
echo $? # 3
# Approve it, then run the same command again. The approval covers exactly that command.
tilldev secrets get STRIPE_KEY --env prod --json
# Or wait anyway, up to 600 seconds.
TILLDEV_APPROVAL_WAIT=300 tilldev secrets get STRIPE_KEY --env prodtilldev approvals # your requests from the last 30 days
tilldev approvals --pending # only the ones waiting
tilldev approvals open # the Approvals page: passkeys, phone, authenticator, what may repeat
tilldev approvals deny <id> # refuse a request, or stop one that may repeat
tilldev approvals policy # on or off for the workspace
tilldev approvals forget # delete the approval claims this machine holdsAllow one exact command for a while
Two actions can be allowed to repeat when you approve them. Everything else is single-use and must be used within 5 minutes of approving.
| Action | Repeat for | What repeats |
|---|---|---|
secret.resolve | 1, 7 or 30 days | That exact command with the same references and agent key. Useful for a dev server or an MCP server you start many times a day. |
ssh.sign | 15 minutes, 1 or 8 hours, 1 or 7 days | Logins with that key to the same server as the same user, or signatures with that key in the same namespace (git, for example). |
Approvals → Allowed to repeat lists them with a use count; Stop, or tilldev approvals deny <id>, ends one at once.
428, then repeat with the approval
A gated call answers 428 STEP_UP_REQUIRED and does nothing. The body carries the approval. Only the caller that asked holds its claim; keep it out of logs. Repeat the same request with x-tilldev-approval: <id>.<claim> once it is approved. The approval is bound to that person, workspace, action and every value in the request, so it cannot be spent on something else. See the API reference.
An agent token cannot list approvals, but it can poll the one it asked for: send the same x-tilldev-approval header with GET /api/step-up/<id>.
HTTP/1.1 428 Precondition Required
{
"error": "Needs your approval: Reveal STRIPE_KEY (shop / prod, version 4). Approve it at https://tilldev.dev/acme/approve/5b0c…",
"code": "STEP_UP_REQUIRED",
"request_id": "…",
"approval": {
"id": "5b0c…",
"claim": "q3V…",
"url": "https://tilldev.dev/acme/approve/5b0c…",
"action": "secret.reveal",
"summary": "Reveal STRIPE_KEY (shop / prod, version 4)",
"expires_at": "2026-09-29T12:10:00Z"
}
}# Poll until approved (or denied / expired), then repeat the same request with the header.
curl -s https://tilldev.dev/api/step-up/$ID -H "authorization: Bearer $TOKEN" | jq -r .status
curl -s -X POST https://tilldev.dev/api/secrets/secrets/$SECRET/reveal \
-H "authorization: Bearer $TOKEN" \
-H "x-tilldev-approval: $ID.$CLAIM"On by default
Approvals are on for every workspace. An owner or admin can turn them off under Approvals → Workspace setting or with tilldev approvals policy off. Turning them off needs an approval itself, so a stolen session cannot switch them off first.
step_up.approved, step_up.denied, step_up.used, step_up.passkey_added, step_up.passkey_removed, step_up.device_added, step_up.device_removed, step_up.totp_confirmed, step_up.policy_on and step_up.policy_off.