feat(hyperlinks): route CLI open requests through the host link handler#334
Conversation
CLI commands that spawn `open <url|file>` / `xdg-open` / $BROWSER used to launch the OS default handler directly, invisible to the embedding host. Shell integration now installs a PATH shim (bin/boss-open, copied to `open` on macOS and `xdg-open`, and exported as $BROWSER) that forwards plain single-target opens to the terminal as OSC 1341;OpenTarget (base64). A new OpenTargetOSCListener classifies the target into HyperlinkInfo and dispatches it through the same onLinkClick path as Ctrl/Cmd+click, falling back to the system default when unhandled — so embedding hosts like BossConsole can show their link-open dialog, while standalone BossTerm behavior is unchanged. The shim only intercepts directly inside BossTerm sessions (TERM_PROGRAM=BossTerm, writable /dev/tty), skips tmux/screen, option flags, multi-arg invocations, directories and non-existing paths, and can be disabled with BOSSTERM_DISABLE_OPEN_INTERCEPT=1. Listeners are registered at session creation (TabController / EmbeddableTerminalState handler vars) so requests from background or uncomposed panes are still routed. Generated with [Claude Code](https://claude.com/claude-code)
Review: route CLI open requests through the host link handlerNice, cohesive feature. The shell shim's guard set is thoughtful (single-arg only, existing-file check, tmux/screen/ 1. Auto-open with no user interaction is a terminal-injection risk (highest priority)Unlike Ctrl/Cmd+click, Two concrete escalations:
Clickable OSC-8 hyperlinks are safe because they require a deliberate click; this path removes that gate. Suggestions: consider constraining the listener to a safe scheme allow-list ( 2. Handler invoked on the emulator thread, but it mutates UI/tab state
3.
|
…ch on main Review follow-ups (#334): - Token-gate the OSC path: sessions get a process-wide secret as BOSSTERM_OPEN_TOKEN; the shim echoes it inside the OpenTarget payload and OpenTargetOSCListener ignores requests without it. Displayed content (cat of a crafted file, curl output, log lines) can't know the token, so it can no longer trigger unprompted opens. - Constrain targets to an allow-list: only http/https URLs and existing absolute filesystem paths are honored. ssh://, mailto:, ftp://, custom schemes, and relative or non-existing paths are refused — closing the ssh:// auto-connect escalation through the SSH link wrapper. - Dispatch the handler on Dispatchers.Main so tab creation and snapshot state see the same threading as the Ctrl/Cmd+click path (scope injectable for tests). - classifyOpenTarget is now a strict validator rather than a general classifier mirroring toHyperlinkInfo; narrow URI catches. - Shim: pass through when base64 is unavailable or the token env is absent. - Tests: token verification, refused schemes (incl. ssh), and the unhandled -> system-opener fallback via an injectable opener. Generated with [Claude Code](https://claude.com/claude-code)
|
Addressed the review in 0a34c4f:
🤖 Generated with Claude Code |
Review: route CLI open requests through the host link handlerNicely designed change — the security model is the strongest part: gating auto-open behind a per-process Correctness / robustness
Minor
Nits
Overall this looks safe to ship — items (1) and (3) are the two I'd address before merge; the rest are optional. Nice work on the token gating and the shared |
…licitly The registration sites passed the handler as a trailing lambda, which Kotlin binds to the constructor's LAST parameter — dispatchScope since 0a34c4f added injectable parameters — breaking compilation on CI. Local verification missed it because the gradle exit code was swallowed by a pipe to tail. Generated with [Claude Code](https://claude.com/claude-code)
- Wrap the host handler invocation (and fallback) in try/catch so a throwing onLinkClick can't crash the main thread; logged via slf4j. - Move payload decode + classification into the coroutine with an IO hop: isFile/isDirectory no longer run on the emulator parse thread (nor on Main), so a hung mount can't stall output parsing. ioDispatcher is injectable so tests stay fully synchronous via Unconfined. - Constant-time token comparison (MessageDigest.isEqual) — informational hardening, no realistic timing oracle exists. - Shim: guard the absolutization cd with a passthrough fallback so an untraversable parent falls through to the real opener instead of emitting a bogus path. - installOpenShims logs through the class logger instead of System.err. Generated with [Claude Code](https://claude.com/claude-code)
|
Addressed the second review round in 7893bca:
Verified: gradle exit code checked directly this round ( 🤖 Generated with Claude Code |
|
Review: route CLI open requests through the host link handler Overall this is a well-designed, security-conscious feature. The token-based authentication (so displayed content cannot forge open requests), the deliberate allow-list in classifyOpenTarget, the constant-time token compare, running FS classification off the parse thread, and the shim conservative "only plain single-target open" heuristic all reflect careful threat modeling. Test coverage of classification + dispatch + malformed payloads is good. A few things worth considering: Behavior / correctness
Robustness (bin/boss-open)
Lifecycle
Minor
Nice work overall — the design mirrors the Ctrl/Cmd+click path cleanly and the KDoc is excellent. Items 1 and 3 are the two I would most want addressed before merge. Generated with Claude Code |
|
PR Review: Route CLI open requests through the host link handler Thorough, well-documented change. The two-commit hardening pass (token gate, allow-list, Main-thread dispatch) resolves the obvious injection vectors, and the design cleanly reuses the existing Security — solid, with two minor notes
Correctness / behavior
Design / quality
Docs
Tests
Overall a clean, security-conscious implementation. The 🤖 Generated with Claude Code |
Summary
CLI commands that spawn
open <url|file>/xdg-open/$BROWSER(e.g.claude /login,gh auth login) used to launch the OS default handler directly — the terminal never saw the request, so embedding hosts couldn't offer their link-open UX.bin/boss-openis extracted with the integration scripts and installed asopen(macOS),xdg-open, and$BROWSERat the front ofPATHfor BossTerm sessions (bash/zsh/fish). Plain single-target URL/existing-file invocations are forwarded to the terminal asOSC 1341;OpenTarget;<base64>; everything else (option flags, multi-arg, directories, app names, non-existing paths) passes through to the real binary unchanged.TERM_PROGRAM=BossTerm, writable/dev/tty), skips tmux/screen; kill switchBOSSTERM_DISABLE_OPEN_INTERCEPT=1; existing$BROWSERis never clobbered.OpenTargetOSCListenerclassifies the decoded target intoHyperlinkInfo(patternIdosc:open-target) and dispatches it through the sameonLinkClickpath as Ctrl/Cmd+click, falling back toHyperlinkDetector.openUrl(system default) when unhandled — standalone BossTerm behavior is unchanged.TabController.openTargetLinkHandler(set byTabbedTerminal's extracted sharedlinkOpenHandler, which also keeps the SSH-URL wrapping in one place) andEmbeddableTerminalState.openTargetLinkHandler— so requests from background/uncomposed panes are still routed.In BossConsole this makes CLI-originated opens show the terminal-link dialog (companion PR: risa-labs-inc/BossConsole#863 adds a System Default option there). Ships to terminal-tab via the usual auto-bump on the next BossTerm release.
Testing
OpenTargetOSCListenerTest(classification + dispatch + malformed payloads); fullcompose-uidesktopTest suite green.🤖 Generated with Claude Code