QR cross-device login delivers the completed session to whoever holds the token, with no binding between who approved and who receives it. An attacker starts a QR login, sends the real /qr-login?token=... URL to a victim, and polls status to receive the victim's session once the victim approves the passkey on the legitimate origin. Passkey phishing bypass → account takeover.
qr_login_status_handler returns row.session_id to any caller with the token (src/gateway/src/handlers.rs:1877). Initiator IP is recorded at begin (handlers.rs:1813) and approver IP at authenticate (handlers.rs:1962), but neither is enforced. The approval page shows a generic passkey prompt with no requester context (frontend/src/views/QrLogin.vue).
Distinct from #336 (split requester/requestee tokens): #336 stops requestee/eavesdropper token reuse but not this — here the attacker is the legitimate initiator holding the poll secret.
Fix — gate the handoff behind explicit, contextualized approver consent + a verification code:
qr_login_begin_handler(handlers.rs:1798): generate a short verification code, store it on the token row, return it to the CLI; CLI prints it.- Expose requester context (code, initiating IP/approx location, timestamp, "grants a terminal/CLI session") to the approval page — not the session.
QrLogin.vue: render that context + code and require an explicit "I started this login" confirmation before triggering WebAuthn.- Keep #336 (token split + burn-on-read) underneath.
Effect: removes the silent one-tap ATO — a user who did not start a CLI login has no matching code and no reason to confirm.