Found in the 2026-09-28 code audit (finding N-4).
Where: internal/tui/peer_messages.go:122-125, called synchronously from resolvePermissionWithReason at internal/tui/model.go:4402. Sink wired at internal/tui/run.go:34-45; coalesce.go:59-68 forwards to program.Send.
Problem: The decide closure calls m.runtimeMessageSink(peerDecisionMsg{...}) inside Update. In bubbletea v2.0.9 Send blocks on the unbuffered p.msgs channel, and the only reader is the event loop, which is currently running Update. Pressing Allow or Deny on a held cross-session message hangs the program, and Ctrl+C queues on the same channel. Tests install a capturing sink (peer_messages_test.go:135, :181), so they cannot see it.
Suggested fix: return a tea.Cmd that yields peerDecisionMsg instead of calling the sink from Update. Consider a debug guard against sink calls on the Update goroutine.
Tests: a regression test that drives Allow/Deny through a real tea.Program (or a blocking sink) rather than a capturing sink.
Found in the 2026-09-28 code audit (finding N-4).
Where:
internal/tui/peer_messages.go:122-125, called synchronously fromresolvePermissionWithReasonatinternal/tui/model.go:4402. Sink wired atinternal/tui/run.go:34-45;coalesce.go:59-68forwards toprogram.Send.Problem: The
decideclosure callsm.runtimeMessageSink(peerDecisionMsg{...})insideUpdate. In bubbletea v2.0.9Sendblocks on the unbufferedp.msgschannel, and the only reader is the event loop, which is currently runningUpdate. Pressing Allow or Deny on a held cross-session message hangs the program, and Ctrl+C queues on the same channel. Tests install a capturing sink (peer_messages_test.go:135,:181), so they cannot see it.Suggested fix: return a
tea.Cmdthat yieldspeerDecisionMsginstead of calling the sink fromUpdate. Consider a debug guard against sink calls on the Update goroutine.Tests: a regression test that drives Allow/Deny through a real
tea.Program(or a blocking sink) rather than a capturing sink.