TILLSECRETS · APPROVALS

An agent can ask. Only you can approve.

Beta

Whoever 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.

01Scope

What waits for you

ActionWhen
secret.revealShowing a value: Reveal in the console, or tilldev secrets get.
secret.resolvePutting values into a command: tilldev secrets exec --env-file. May repeat for a time you choose.
ssh.signSigning 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_onceThe 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.allowAdding a host a secret may be sent to, or changing its methods or paths. Also fulfilling a secret request that allows hosts.
grant.createConsenting to an agent spending a secret.
presign.createPresigning a call.
token.createMinting a service token (ts_…).
agent.createMinting an agent token (ta_…).
identity.createLetting a workload sign in without a token: a workload identity.
identity.updateChanging what a workload identity trusts, its keys, token lifetime or host key. A new name alone does not ask.
sync.create, sync.enableAdding a sync target, or resuming a paused one.
tier.lowerMoving a secret or environment from restricted (7+) to below 7.
leak.ignoreMarking a secret not secret, so leak checks skip its values.
secret.sealSealing a secret so nobody can reveal it. See Sealed secrets.
release.request, release.approveAsking to reveal or unseal a sealed secret, and another owner approving it.
anomaly.resolveResolving a spend anomaly, so an agent can't silence its own alarm.
canary.viewSeeing which secrets are canaries. Every time.
canary.retire, canary.configureRetiring a canary, or changing where canaries are made.
repos.eraseErasing every TillForge repository in the organization and destroying its repository key. Always single-use.
session.trustTrusting a terminal sign-in with everyday secrets. See Trust a terminal.
access.elevate, access.approveAsking for time-boxed admin, and approving someone's request.
access.eligibilityLetting someone ask for time-boxed admin, or changing their cap or who approves.
access.accept_riskAccepting a finding from the access review so it stops counting.
engagement.authoriseAn owner authorising a security engagement: the scope, window and tester networks they approve are the ones that run.
compliance.excludeTaking a control out of scope in compliance evidence; the approval names the control and the reason.
compliance.attestAttesting to compliance evidence held outside TillDev; the approval covers the statement, reference and validity as written.
customer_key.attach, customer_key.detachPutting a product under a customer key, or taking it off.
step_up.offTurning 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.

02Setup

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.

Why confirm the authenticator
Anyone holding your session could have turned on sign-in 2FA with their own app. A code counts for approvals only after you confirm it with proof the session does not have. Turning 2FA off and on again, or an admin resetting it, clears the confirmation.

Adding or removing a way to approve sends an email to you. Every request, approval, denial and use is in the audit log.

03Phone

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.

  1. On the approval page, choose Send to my phone. The page shows a two-digit number.
  2. Open Approvals in Till Authenticator.
  3. It shows the request: what it does, the workspace, where it was asked from, and any repeat window you chose on the page.
  4. 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.

A request you did not send
Something holding your session can send a request to your phone too. Approve only what you sent from an approval page in front of you. If one arrives that you did not send, deny it.

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.

04CLI

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 byWhat happens
You, in a terminalPrints the link, opens it in your browser and waits up to 10 minutes. Approve it and the command carries on.
A coding agentTells 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 jobExits with code 3 and the link straight away, so nothing hangs.
terminal
$ 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 on

The 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.

what the agent reads
$ 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.

scripts
# 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 prod
manage
tilldev 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 holds
05Trust

Trust 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.

trust
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 longYou choose when you approve: 1 hour, 8 hours, 1 day or 7 days.
What it coverssecret.reveal and secret.resolve for secrets below tier 7. Nothing else.
What still asksRestricted secrets (tier 7 and up), sealed secrets, and every other action in the table above.
Which sign-inOnly the one that asked. Each tilldev login is its own sign-in, and a browser session can't be trusted.
Ending ittilldev approvals trust end, tilldev logout, or Stop under Approvals → Allowed to repeat.
What a trust allows
Anything on that machine running as you with that sign-in, including an agent, can read those secrets until the trust ends. Trust a sign-in only on a machine you control, and keep the most sensitive secrets at tier 7 or above.

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.

06Repeat

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.

ActionRepeat forWhat repeats
secret.resolve1, 7 or 30 daysThat 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.sign15 minutes, 1 or 8 hours, 1 or 7 daysLogins with that key to the same server as the same user, or signatures with that key in the same namespace (git, for example).
What a repeat allows
The server sees a command as text and cannot see what really runs, and it cannot tell which program on your machine asked the SSH agent. The program is how the command starts, as the CLI read it: the executable, or the interpreter and its script, so 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.

07API

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>.

428 body
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 + repeat
# 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"
08Workspace

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.

Audit
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.