DEV Community

Bob Lee
Bob Lee

Posted on

A Remote Coding Agent Can Deadlock on a Local Permission Dialog

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 permission prompt is protocol state

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

What needs to survive a disconnect

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.

Resuming the worker is a separate step

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.

The UI is the easy part

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

Top comments (2)

Collapse
 
reidmarlow profile image
Reid Marlow

The request ID is the part I would not let anyone hand-wave away here. Without it, a retry from the phone and a resumed worker can both look like the same approval, and then the audit log is just a nicer race condition.

Collapse
 
alexshev profile image
Alex Shev

Local permission dialogs are a good example of hidden state outside the agent loop. The remote runner can be perfectly healthy while the real task is waiting on a human-visible gate. I would treat those gates as first-class blockers, not timeout noise.