Agent API
An agent asks. A human signs. Not the other way round.
Every assistant that touches your messages today does it with your full standing permission. Nex inverts that: an automated process starts with nothing, has to ask for named permissions with an expiry, and gets exactly what a human decides to hand over.
A new process holds nothing
Before any of the interesting parts, the boring one: an agent that has not been approved cannot do anything at all, and says so in a way that points at the next step.
nex send 4f0c7ab2… "this must not be sent"REFUSED: `message.send` requires the scope `message:send` and this process holds no token.Tokens are never written to disk (contract §6.1), so a fresh `nex` invocation starts with nothing.Next: nex session then auth request --scopes message:send --waitA human must approve it before anything is minted.[exit 3]Request → approve → narrowed grant
The agent asks for read access to every conversation and the ability to send. The human gives it the reading half and keeps the sending half.
nex auth request --scopes conversation:*:read,message:send --ttl 300 --waitapproval requested request_id 7c41f9d0a2be5163d84e0fb27a95c3ea user_code H4KD-27QF expires in 4m 58s asking for ! conversation:*:read Read the messages in ANY conversation ! message:send Send messages as you A human must now run: nex approve 7c41f9d0a2be5163d84e0fb27a95c3ea and can narrow it: nex approve 7c41f9d0a2be5163d84e0fb27a95c3ea --scopes conversation:listwaiting for a human… (polling every 2s)nex approve listrequest 7c41f9d0a2be5163d84e0fb27a95c3ea from claude-code user_code H4KD-27QF <- must match what the agent printed expires in 4m 41s agent key 3f8a2c19… asking for ! conversation:*:read Read the messages in ANY conversation ! message:send Send messages as younex approve 7c41f9d0a2be5163d84e0fb27a95c3ea --scopes conversation:*:readAPPROVED 7c41f9d0a2be5163d84e0fb27a95c3ea signed by 9d1b47e0… valid for 5m 00s granted ! conversation:*:read Read the messages in ANY conversation withheld (the agent asked, you said no) ! message:send Send messages as youAPPROVED jti b39d1c60f4a72e58ad0c9b31e7f52d84 expires in 4m 59s granted ! conversation:*:read Read the messages in ANY conversation withheld by the human ! message:send Send messages as you token held in memory only; it dies with this process.history 4f0c7ab2… 1753876412031 a71e3d95… c04b18ff… ratchet-proof-the-quick-brown-foxThe withheld half is genuinely withheld
This is the moment worth watching. The read succeeds. The send does not — first because the client knows the token cannot cover it, and then, when that check is bypassed, because the daemon refuses it.
send 4f0c7ab2… "shipping now"REFUSED locally: `message.send` needs the scope `message:send`, and the tokenthis process holds carries only: conversation:*:readAsk for a wider grant with `auth request --scopes message:send` — a humanstill has to approve it.(Add --force to send it anyway and see the daemon refuse it itself.)the client check is a courtesy. --force skips it, so the daemon has to say no itself:send 4f0c7ab2… "shipping now" --forceREFUSED by the daemon: token scopes do not cover `message:send` (code -32001)Revocation bites on the next call
Not at the next login, not when the token expires — on the next call.
nex tokens revoke --allrevoked 1 token(s). Every agent must now be re-approved.the agent now repeats the exact read that succeeded a moment ago:history 4f0c7ab2…REFUSED by the daemon: token has been revoked (code -32001)The permissions an agent can ask for
| Scope | What it allows |
|---|---|
| conversation:list | List conversations — titles and counts, no message bodies |
| conversation:<id>:read | Read the messages in one named conversation |
| ! conversation:*:read | Read the messages in ANY conversation |
| ! conversation:*:write | Create or modify ANY conversation |
| ! message:send | Send messages as you |
| media:<id>:fetch | Fetch media attachments |
| ! admin:* | Administer this daemon — devices, keys, settings |
| ! * | EVERYTHING — unrestricted access to this daemon |
Why this holds up
The human can grant less than was asked for
Approval is not a yes/no button. The approver names the scopes to hand over, and anything the agent asked for that is not on that list is withheld — and shown to both sides as withheld, so nobody discovers it later through a mysterious failure.
Tokens are never written to disk
A minted token lives in the memory of the process that asked for it. A fresh invocation of the CLI holds nothing and is refused immediately. There is no credential file to leak, copy or forget about.
Every grant expires
A token carries a lifetime in seconds — the transcripts below use five minutes — and an inactivity window. An agent that goes quiet and comes back has to ask again.
The gate is in the daemon, not the client
The CLI checks scopes before it makes a call it knows will fail, purely so the error message is useful. Skip that check with --force and the daemon refuses it itself. The client-side check is a courtesy; the server-side one is the security boundary.
Holding the token is not enough
Each call is signed with a key the token is bound to, so a stolen token alone cannot be replayed by another process. Approvals themselves are signed by an enrolled human key that the daemon knows about.
Revocation is immediate
Revoking does not wait for expiry, log the agent out, or take effect next session. The very next call the agent makes is refused, with a distinct error naming revocation specifically.