Skip to content

[FEATURE]: Remote approval of permission requests from mobile / second device #39628

Closed

Description

@yukihamada

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Problem

Long-running sessions frequently block on permission prompts (file edits, bash commands, MCP calls). If the user steps away from the terminal, the session stalls until they return. The desktop app improved local UX, but there is still no way to approve a pending permission request from a phone or a second machine.

Proposal

Allow a pending permission request to be approved remotely:

  1. Server: when a permission prompt is pending, expose it via the existing headless server (opencode serve) event stream with a unique approval ID and a short-lived, single-use approval token.
  2. Optionally show a QR code / short URL in the TUI when a prompt appears (e.g. https://<host>:<port>/approve/<token>), so the user can scan it with a phone on the same network.
  3. The approval page is a minimal, local-only web view: shows the exact action (command / diff) and Approve / Deny buttons. No cloud relay required for the basic version — LAN/Tailscale only.
  4. Security: token is single-use, expires with the prompt, and the server binds to localhost by default (opt-in --host for LAN). Approval requires the token, so port scanning alone is not enough.

Benefits

  • Unblocks human-in-the-loop workflows when away from the desk.
  • Fits the existing headless-server architecture; no new daemon.
  • Opt-in and local-first, so the security surface stays small.

Out of scope (first iteration)

  • Cloud relay / push notifications.
  • Approving from a different user account.

Happy to implement this if the core team agrees with the direction.

Activity

  1. bertshim commented on Aug 20, 2026

    @bertshim

    The scoping choice here is the part I keep turning over. Out of scope says cloud relay, and the approval page is LAN/Tailscale only. That covers being in the next room, which is real and probably the common case. But the runs that actually sat stalled on me were the ones where I was on cellular and nowhere near that network, which is the same situation the problem statement opens with. If Tailscale is the intended answer for that, it might be worth saying so in the issue, since standing up a tailnet is a pretty different ask than scanning a QR code.

    The other thing I'd think about early is what the approval page actually shows. Deciding on a bash command or an edit means having the command or the diff in front of you, and getting that onto a phone screen without it turning into a blind yes seems like what decides whether people read it or just tap approve on everything.

    And related to the #43493 thread that got linked here: worth settling whether every pending prompt notifies. In a session that asks a lot, per-prompt pushes get muted fast, and then you're back to the stall with an extra step in front of it.

  2. csanadymiklos commented on Sep 10, 2026

    @csanadymiklos

    The cellular case @bertshim raises is the one I ended up building for, so: a data point, not a suggestion.

    Disclosure first — I maintain Lauther (lauther.app), so treat this as a maker's words. It is precisely the tradeoff this issue scopes out: a cloud relay, so it works on cellular with no tailnet, and correspondingly it is not local-first and not self-hostable. If the LAN/QR version lands it covers the next-room case and this is irrelevant. I'm writing because the "away from the network entirely" half looks like it stays open either way.

    Two things that mattered more than I expected, in case they save anyone the same iteration:

    Silence must not read as yes. The blocking call returns three outcomes rather than two — APPROVED, DENIED, and NO ANSWER — and the tool description tells the model in words that a timeout means stop, do not retry, do not find another way. Two states plus a timeout is the easy design, and it quietly turns "I was in a tunnel" into consent.

    One pending action should be one notification. On the muting point: the relay long-polls for 25 seconds at a time, and past that it re-asks about the same pending request instead of sending a new one. A ten-minute wait is one question on the phone, not one every half minute. That does not fix an agent that asks about everything — nothing downstream can — but it stops the waiting itself from generating the noise.

    On what the approval screen shows: agreed this decides the whole thing. A short question plus a separate details blob for the command or diff is what made it readable on a lock screen instead of a blind tap.

    Happy to go into how the relay half works if it's useful while you scope this, and equally happy for it to be irrelevant if opencode stays local-only.

  3. bertshim commented on Sep 10, 2026

    @bertshim

    The three-outcome shape matches what bit me, from the other side of it. NO ANSWER stopping the model is the important half. What I got stuck on afterwards was finding out what actually happened, because the same layer that blocked the action also blocked the read I'd have used to check, so the only thing left was looking at the screen. Worth deciding early whether the timeout path guarantees the operator a way to observe final state, since the retry decision needs that and the agent isn't the one making it.

  4. csanadymiklos commented on Sep 12, 2026

    @csanadymiklos

    Good question, and I think it's the one that decides whether the timeout path is usable rather than merely safe.

    Concretely, in the thing I built: the ask is a durable record, not a transient push. The call returns an id, the pending question keeps existing after the wait window closes, and it stays answerable — so "was this ever decided, and which way" is a lookup on that id afterwards, from either side. Someone opening the app late finds the question still sitting there rather than discovering it evaporated.

    But I'd pull apart two things your comment holds together, because only one of them is the approval layer's job:

    1. Did the human ever answer, and what did they say. An approval channel can and should guarantee this, and it's cheap — a stable id plus a record that outlives the wait window.
    2. What the run actually did after NO ANSWER. It can't. The approval layer knows the answer was "nothing"; it has no idea what the harness then chose to do. And if the same layer that blocked the action also blocked the read, as you describe, no notification service fixes that — the harness has to emit that state itself.
      Which I think reinforces your point about who owns the retry decision. The operator needs (2) to make it, and (2) can only come from opencode, not from whatever channel carried the question.
  5. bertshim commented on Sep 15, 2026

    @bertshim

    Agreed on the split, and (2) is the half I'd underestimated. In my case the harness knew what it had done, it just had no way to say so that wasn't behind the same block. I fell back on watching the screen, which only worked because I happened to be there. A record the harness writes itself after NO ANSWER would have covered the times I wasn't.

  6. neriousy commented on Oct 19, 2026

    @neriousy
    Member

    V2 serves its web UI to a phone or second device, where you can see pending permission requests and choose Deny, Allow always or Allow once. Run opencode pair to print single-use connection links that expire after a few minutes; to reach the server from other devices on your network run opencode service set hostname 0.0.0.0 first, or use opencode pair --remote to connect from anywhere. To get v2: npm i -g @opencode/cli@latest or curl -fsSL https://opencode.ai/v2/install | bash (desktop: update the app). If you still see this on v2, please open a new issue with the details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions