Skip to content

fix(app): stop external links from navigating the main webview away - #6000

Open
Guykaganovsky1 wants to merge 1 commit into
tinyhumansai:mainfrom
Guykaganovsky1:fix/external-links-hijack-webview
Open

fix(app): stop external links from navigating the main webview away#6000
Guykaganovsky1 wants to merge 1 commit into
tinyhumansai:mainfrom
Guykaganovsky1:fix/external-links-hijack-webview

Conversation

@Guykaganovsky1

@Guykaganovsky1 Guykaganovsky1 commented Sep 3, 2026

Copy link
Copy Markdown

Summary

Clicking a link in the app — for example to a site the agent just built — navigated the main webview to that page. The desktop shell is a single webview with no back button and no address bar, so this is one-way: the chat is gone until the app is restarted.

Chat bubbles already route their links through openUrl (AgentMessageBubble's MarkdownAnchor), but that is one component's discipline. Every other anchor the app renders inherits the webview's default navigation, and there is no shell-level guard to catch them: the main window is declared in tauri.conf.json, and on_navigation exists only on WebviewWindowBuilder, so a config-declared window has nowhere to attach one.

Related issue

None.

Changes

  • app/src/utils/externalLinkGuard.tsisExternalNavigation (pure predicate) plus installExternalLinkGuard, a document-level click guard that hands remote http(s) links to openUrl and prevents the navigation.
  • app/src/App.tsx — install it once, above the router.

The guard listens in the bubble phase deliberately. In the capture phase this document-level listener would run before the owning component's handler, so a chat link would be opened twice — once here and once by MarkdownAnchor — i.e. two browser tabs for one click. Bubbling lets the component go first; anything it already preventDefault-ed is skipped, and the default navigation has still not happened, so preventing it there is not too late. This was caught by the test suite, not by reasoning after the fact.

In-app hash routes, non-http schemes (mailto:, openhuman://, data:), modifier-clicks and non-primary buttons are all left alone.

API or behavior changes

Behavior only: an anchor that previously replaced the app now opens in the user's browser. No public API.

Validation

  • npx vitest run --config test/vitest.config.ts src/utils/externalLinkGuard.test.ts7 passed.
  • pnpm typecheck — clean.
  • pnpm lint — 0 errors (82 pre-existing warnings, none in the new files).

Tests

app/src/utils/externalLinkGuard.test.ts — external link is intercepted and opened outside; in-app hash routes pass through; non-http schemes ignored; an already-handled click is deferred to (the double-open regression); teardown removes the listener. jsdom's "Not implemented: navigation to another Document" in the teardown case is the un-guarded behaviour this fix exists to stop.

Checklist

  • The change is focused on one logical change
  • No new #[allow(...)], #[ignore], or relaxed lints
  • No secrets, tokens, or .env contents in the diff or the description

Summary by CodeRabbit

  • New Features

    • External links now open safely without navigating users away from the app.
    • In-app routes and non-web links continue to behave normally.
  • Bug Fixes

    • Prevented navigation to external pages that could leave users without a way back to the chat.

The desktop shell is a single webview with no back button and no address
bar, so a top-level navigation to a remote page is one-way: the chat is
gone until the app restarts. Clicking a link to a site the agent built
did exactly that.

Chat bubbles already route their links through `openUrl`
(`AgentMessageBubble`'s `MarkdownAnchor`), but that is one component's
discipline — every other anchor the app renders inherits the webview's
default navigation instead, and there is no shell-level guard: the main
window is declared in `tauri.conf.json`, and `on_navigation` exists only
on `WebviewWindowBuilder`, so a config window has nowhere to attach one.

Install a document-level click guard above the router. It listens in the
BUBBLE phase deliberately: in the capture phase it would run before the
owning component's handler and a chat link would open twice, once here
and once in `MarkdownAnchor`. Bubbling lets the component go first, and
the default navigation has still not happened, so preventing it there is
not too late.
@Guykaganovsky1
Guykaganovsky1 requested a review from a team September 3, 2026 10:44
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T10:49:09.772821Z 21b19c7 PR opened
🔒 Security Review Completed 2026-09-03T10:49:59.405599Z 21b19c7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

$0.0000 · 0 in / 0 out

@tinysweeper

tinysweeper Bot commented Sep 3, 2026

Copy link
Copy Markdown

How this change flows

0 changed behaviours across 2 relationships. 2 surrounding behaviours are shown (60 graph nodes walked). 17 further behaviours left out to keep the diagram readable.

flowchart LR
  n0["startInternetStatusListener"]:::impacted
  n1["snapshot"]:::impacted
  n0 -->|calls| n1
  n0 -->|uses| n1
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge.

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Sep 3, 2026
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: f7a16bfb-7805-45a3-8a28-79e66b0b324d

📥 Commits

Reviewing files that changed from the base of the PR and between 98b468f and 21b19c7.

📒 Files selected for processing (3)
  • app/src/App.tsx
  • app/src/utils/externalLinkGuard.test.ts
  • app/src/utils/externalLinkGuard.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The app now intercepts external HTTP(S) anchor navigation and forwards it to openUrl. In-app routes, modified clicks, non-left clicks, and already-handled events remain unchanged. The guard installs on app mount and supports teardown.

Changes

External link guard

Layer / File(s) Summary
Link detection and interception
app/src/utils/externalLinkGuard.ts, app/src/utils/externalLinkGuard.test.ts
The guard identifies external HTTP(S) URLs, prevents eligible navigation, calls openUrl, ignores handled or modified clicks, and supports teardown. Tests cover these behaviors.
Application session wiring
app/src/App.tsx
App installs the guard once on mount before the router renders.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 21b19

External HTTP(S) links now open in the system browser instead of replacing the app webview, while in-app routes and modified or handled clicks retain their existing behavior. The change is ready to merge.

Sequence Diagram(s)

sequenceDiagram
  participant Anchor
  participant ExternalLinkGuard
  participant openUrl
  Anchor->>ExternalLinkGuard: dispatch click
  ExternalLinkGuard->>ExternalLinkGuard: check href and click modifiers
  ExternalLinkGuard->>openUrl: open external HTTP(S) URL
Loading

Suggested reviewers: senamakel

Poem

A rabbit guards each clicking hare,
Remote links hop through openUrl’s lair.
App routes stay upon their track,
Teardown lets the guard step back.
The chat remains, with paths intact.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing external links from navigating the main webview away from the app.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21b19c7726

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +23 to +24
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
return url.origin !== appOrigin;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard same-origin paths that bypass HashRouter

When assistant text contains a relative Markdown link such as [docs](/docs), MarkdownText renders it as a plain anchor, but this predicate ignores it because it resolves to the app origin (or to Tauri's non-HTTP asset scheme). The application only routes #/… URLs, so following /docs performs a top-level asset navigation rather than an in-app transition and can still replace the main webview with an unavailable page. Allow only same-document hash routes, rather than treating every same-origin or non-HTTP URL as safe.

AGENTS.md reference: AGENTS.md:L175-L175

Useful? React with 👍 / 👎.

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

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant