Teaching an agent to ask permission on my phone

Teaching an agent to ask permission on my phone

My assistant can delete things. That is the whole point of it, and also the reason I do not want it deciding alone.

So it asks. The trouble was where it asked: a Telegram thread I was already using for something else, arriving mid-answer, while I was typing. I would see it, mean to answer it, and scroll past. Twice I found a request still waiting hours later because it had gone by in the flow of a conversation.

An approval prompt that gets scrolled past is not a safety feature. It is a delay with extra steps, and eventually it teaches the agent to wait forever or, worse, to assume.

So we moved the question to the place I actually look at without being asked: iMessage.

The shape of it is simple and that is deliberate. When a command needs my consent, the agent sends a card to my phone — what it wants to do, what it touches, what happens if I say yes. To approve, I reply with a short code plus a digit. Not “yes”, because “yes” is a word that appears in ordinary conversation, and a system that reads “yes” out of a chat window is not checking anything.

The reply is matched back to the exact request by digest. If I approve something in one thread, that approval covers that request and nothing else. A stale card cannot be replayed, a code cannot be moved to a different action, and a reply that does not match is not an approval — it is a message that did not match.

The plumbing is where the afternoon went. The Mac mini sends through Messages.app via AppleScript, which is fine and boring. Reading the replies is not. In chat.db, almost every recent message has a null text column, because the text is no longer stored where a decade of tutorials says to look. The real content lives in an attributedBody blob, a typedstream serialisation that has to be decoded before you can find out whether I typed a code or asked a question. Every naive version of this reads null, decides there was no reply, and waits.

It ended the week live, and the verification is the part I care about more than the demo. Card sent at 12:08:54. Reply resolved 73 seconds later, tagged via: mac, digest_ok: true — the approval had come back through the Mac, and it had matched the request it was issued for.

The rule underneath all of it is that the thing fails closed. If the card cannot be sent, nothing runs. If the reply cannot be parsed, nothing runs. If the digest does not match, nothing runs. There is no branch where a missing answer is read as consent, which is the failure mode that turns every approval gate into decoration.

It took an afternoon to build and it changed how the whole system feels to use. The agent stops being a thing that acts and tells you afterwards. It becomes a thing that asks, in a place you will actually see, with an answer that cannot be faked by a word you typed to someone else.