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. |
leak.ignore | Marking a secret not secret, so leak checks skip its values. |
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. |
session.trust | Trusting a terminal sign-in with everyday secrets. See Trust a terminal. |
access.elevate, access.approve | Asking for time-boxed admin, and approving someone's request. |
access.eligibility | Letting someone ask for time-boxed admin, or changing their cap or who approves. |
access.accept_risk | Accepting a finding from the access review so it stops counting. |
engagement.authorise | An owner authorising a security engagement: the scope, window and tester networks they approve are the ones that run. |
compliance.exclude | Taking a control out of scope in compliance evidence; the approval names the control and the reason. |
compliance.attest | Attesting to compliance evidence held outside TillDev; the approval covers the statement, reference and validity as written. |
customer_key.attach, customer_key.detach | Putting a product under a customer key, or taking it off. |
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.
What a command does while it waits
A command that needs an approval asks for one and waits for your answer. What it does while waiting depends on who is running it.
| Run by | What happens |
|---|---|
| You, in a terminal | Prints the link, opens it in your browser and waits up to 10 minutes. Approve it and the command carries on. |
| A coding agent | Tells the agent, in plain words, to ask you to approve, opens the link in your browser and waits up to 90 seconds. Approve in time and the command carries on. Otherwise the agent runs it again after you approve. |
| A script or CI job | Exits with code 3 and the link straight away, so nothing hangs. |
$ 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/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 onThe CLI knows it is being run by a coding agent from the markers Claude Code, Codex, Gemini CLI, Cursor and opencode set in the shells they run commands in. For another agent, set TILLDEV_AGENT_MODE=1, or a name such as TILLDEV_AGENT_MODE=aider; TILLDEV_AGENT_MODE=0 turns it off. The CLI gives the agent's name with each request, and the approval page shows it. It is what the CLI reports, not something the vault can prove, so the page says so.
$ tilldev secrets get STRIPE_KEY --env prod # run by a coding agent
This needs the user's approval before it runs: Reveal STRIPE_KEY (shop / prod, version 4)
Ask the user to approve it at https://tilldev.dev/approve/5b0c… (it is also waiting on their TillDev dashboard).
This command waits up to 90s and then carries on by itself. If it stops first, run the same command again once they approve: it goes through without asking again.
Approved; carrying on.You don't have to watch the terminal or your email. While the console is open, a request from a terminal shows up in the corner of whatever page you are on, the tab title counts what is waiting, and Approvals in the sidebar shows a badge. Choose Notify me on this computer on the Approvals page to get a desktop notification while the tab is in the background.
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. A repeat allowed for the program with any arguments is used by other runs of that program too.
# No terminal and no agent (CI, a cron job): exit code 3 and the link, at once.
tilldev secrets get STRIPE_KEY --env prod --json
echo $? # 3
# Wait for the answer without running anything: exit 0 approved, 1 denied or expired, 3 still waiting.
tilldev approvals wait 5b0c… --timeout 300
# Then run the same command again. The approval covers exactly that command,
# or, if you allowed the program with any arguments, other runs of it.
tilldev secrets get STRIPE_KEY --env prod --json
# Or make any run wait, 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 open <id> # one request, on its own page
tilldev approvals wait <id> # wait for your answer; exit 0, 1 or 3
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 holdsTrust a terminal with everyday secrets
Approving each reveal gets in the way when you, or an agent you are working with, read the same config all day. You can trust one terminal sign-in for a while instead. While it is trusted, that sign-in reveals and uses secrets below tier 7 without asking. Each use is still in the audit log, with the agent the CLI named.
tilldev approvals trust # ask to trust this sign-in; you choose for how long
tilldev approvals trust status # trusted or not, until when, how many uses
tilldev approvals trust end # stop trusting it now (tilldev logout does too)| For how long | You choose when you approve: 1 hour, 8 hours, 1 day or 7 days. |
| What it covers | secret.reveal and secret.resolve for secrets below tier 7. Nothing else. |
| What still asks | Restricted secrets (tier 7 and up), sealed secrets, and every other action in the table above. |
| Which sign-in | Only the one that asked. Each tilldev login is its own sign-in, and a browser session can't be trusted. |
| Ending it | tilldev approvals trust end, tilldev logout, or Stop under Approvals → Allowed to repeat. |
To keep an agent off your sign-in altogether, give it its own agent token: tilldev secrets agents create --save saves one for the agent that is running, and the CLI uses it whenever that agent runs a command. An agent token can spend through grants but never reveal.
Allow one command, or one program, for a while
Two actions can be allowed to repeat when you approve them, and a trusted terminal always lasts for the window you choose. 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, or, if you choose with any arguments when approving, the program the command starts with: runpod.sh with any pod, bash deploy.sh with any target. Useful for a dev server, a script whose arguments change each run, 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). |
docker with any arguments is every docker command. For the window you choose, anything on that machine holding your session and the stored claim can do the same thing again. Allow repeats only on a machine you trust, and keep the window short.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; a repeat approved with repeat_scope: program is bound to the request's program instead of its command. 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/approve/5b0c…",
"code": "STEP_UP_REQUIRED",
"request_id": "…",
"approval": {
"id": "5b0c…",
"claim": "q3V…",
"url": "https://tilldev.dev/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, step_up.policy_off and step_up.trust_ended. Each record says whether the request came from a browser, a terminal sign-in or an API key, how it was approved, and the agent the CLI named.