The nastiest failure mode in a remote coding agent is not a bad patch.
It is a permission prompt that nobody can see.
You start a long-running job on a workstation, leave the desk, and check it from a phone later. The agent reaches a command that needs approval. If that request only exists as a modal in the desktop UI, the job has not technically failed. It has just stopped forever.
That is worse. A failed job is observable. A hidden wait looks healthy until someone notices no work has moved.
The fix starts with a small change in how you model approval.
A permission request is not UI state. It is durable state owned by the job that is doing the work.
The lifecycle should look more like this:
asked → persisted → surfaced → answered → applied → resolved
The desktop dialog, phone screen, CLI, or web controller is only one view over that state. Closing a window must not erase it. Reconnecting must not create a second request. Two controllers must not be able to resolve different requests because a stale button happened to be on screen.
This also changes what a remote-control protocol needs. A controller should be able to fetch job status with pending approvals, submit an answer for one request ID, and observe the resulting event. It should not become a filesystem or runtime proxy just to click “allow.”
At minimum, the pending request needs a stable request ID, its owning job/session, the requested action and resources, and enough ordering information to render concurrent requests deterministically.
The answer also needs an identity.
If request abc
is pending, an answer for xyz
must fail. Replaying the same answer for abc
should be harmless. Replaying a different answer under the same ID should not quietly overwrite the first decision.
That sounds fussy until a phone reconnects on a flaky network and retries the last command. Then it is the difference between idempotence and “the agent ran it twice.”
The mailbox also needs limits. A broken or hostile tool should not be able to fill an unattended host with an unbounded number of serialized approval requests.
Persisting an “approved” flag is not enough.
The running worker has to consume the answer, apply it to the exact pending request, record where the reply came from, and only then remove the request from the mailbox. If the worker crashes between those steps, recovery should be able to tell whether the answer is queued, applied, or fully resolved.
Reply source matters too. A user approval, an automatic policy, and a system rejection are not the same audit event, even if they all unblock the same future.
When the permission event stream disappears, the safe behavior is not to assume approval. Pending work should fail closed or be cancelled with an explicit reason. Otherwise a transport failure silently turns into broader authority.
Once the protocol exists, the phone UI really can be two buttons: approve and reject.
But those buttons are the last five percent. The hard part is making the request durable, routed to the correct session, replay-safe, auditable, bounded, and fail-closed.
There is also a boundary worth keeping explicit: an application-level tool approval is not an operating-system security grant. A remote controller can approve an agent action that the app is already capable of performing. It cannot legitimately manufacture Accessibility, Screen Recording, or similar host privileges that the operating system has not granted.
I am building BitFun, and this is how we ended up treating detached-job approvals: the target owns a persisted permission mailbox; the controller answers by request ID; the worker applies the reply with its source and marks it resolved. The same permission events can then reach desktop and remote-control surfaces without making the controller the runtime.
The implementation is open source here: https://github.com/GCWing/BitFun