Is Roam Highlighter safe?
Roam Highlighter sends copied bookmarks and highlighted page text, including page URLs and titles, to PostHog analytics.
Roam Highlighter's content script runs on HTTP, HTTPS, and FTP pages. When the user clicks the copy button, it sends a PostHog analytics event containing either the copied bookmark markdown with the page title, URL, and tags, or selected highlights converted to markdown.
Who publishes itDaniel Wirtz - no other listings under this identity, 1 shared hostname
Daniel Wirtz - no other listings under this identity, 1 shared hostname
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 1 hostname
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Copied highlights are sent to PostHog analytics
When you copy a bookmark or highlight with Roam Highlighter, it builds markdown from the page URL, title, tags, and, for highlights, the selected text.
The same click sends those fields to PostHog analytics at us.i.posthog.com.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You click Roam Highlighter's copy bookmark or copy highlight button on a page.
The widget button is wired to the same handler for bookmarks and highlighted text.
The extension copies markdown for you and also sends a PostHog event containing bookmark or highlight details.
For highlighted text, the event includes markdown generated from the selected page content.
- Current page URLhttps://www.example.com/research/browser-notes (illustrative)
Identifies the page you were viewing when you clicked the copy button.
- Page titleBrowser Notes for Research Workflows (illustrative)
Adds readable context about the page tied to the copied bookmark or highlight.
- Tags you entered#research #browser (illustrative)
Carries the tags you typed into the Roam Highlighter widget with the page record.
- Selected page textBrowsers can turn short reading sessions into structured notes. (illustrative)
Includes the page text you highlighted before clicking copy.
- Highlight count2 (illustrative)
Shows how many selected passages were included in the copy action.
- Display modedark (illustrative)
Adds widget context from your browser session to the analytics event.
The copy path initializes PostHog and sends bookmark or highlight properties
Readable PostHog initialization
static/js/content.jsfunction initPostHog(context) { if (context.posthog) { return context.posthog; } context.posthogConfig = array_config_default.a; context.posthog = Dp; Dp.init("phc_wOyG7qBKFJIOqpiBshW98SpEP9vFtRTEReJXwxTg6r3", { api_host: "https://us.i.posthog.com", persistence: "localStorage", disable_session_recording: true, loaded: function loaded(posthog) { if (false) { } } }); return context.posthog;}Readable copy handler
static/js/content.jsasync function copyHighlights() { var url = window.location.href; var author = metadata.author; var date = metadata.date; if (highlights.length === 0 || highlights.length === undefined) { copy_text_to_clipboard_default()("[" + title + "](" + url + ") " + tags); setWidgetState("success"); posthog.capture("copy_highlight_button_clicked", { type: "bookmark", markdown: "[" + title + "](" + url + ") " + tags, colorMode: colorMode }); setTimeout(function() { setWidgetState("active"); if (settings.autoopen) { chrome.runtime.sendMessage({ msg: "openroam" }, function(response) {}); } }, 1000); return; } var markdown = await composeMarkdown(highlights, tags, author, title, url, date); copy_text_to_clipboard_default()(markdown); setWidgetState("success"); var highlightCount = highlights ? Object.keys(highlights).length : 0; if (posthog) { posthog.capture("copy_highlight_button_clicked", { type: highlightCount === 0 ? "bookmark" : "highlights", highlightCount: highlightCount, highlights: markdown, colorMode: colorMode }); } setTimeout(function() { setWidgetState("active"); }, 1000); if (settings.autoopen) { chrome.runtime.sendMessage({ msg: "openroam" }, function(response) {}); }}Readable highlight formatter
static/js/content.jsasync function formatHighlightsToMarkdown(highlights) { var turndown = new turndown_browser_es({ emDelimiter: "__", headingStyle: "atx", codeBlockStyle: "fenced" }); var indentation = getIndentation(settings.parenttext); turndown.remove("hr"); turndown.remove("img"); turndown.addRule("list", { filter: "li", replacement: function replacement(content) { return indentation + content + " \n"; } }); turndown.addRule("code", { filter: "pre", replacement: function replacement(content) { return "```" + content + "```"; } }); if (settings.copyformatting) { } else { turndown.addRule("removeformatting", { filter: ["em", "i", "del", "del", "strong", "ins", "sub", "sup", "mark", "b"], replacement: function replacement(content) { return content; } }); turndown.addRule("headings", { filter: ["h1", "h2", "h3", "h4", "h5", "h6"], replacement: function replacement(content) { return "- " + content + " \n"; } }); } setHighlights(orderHighlightsToDocumentFlow(highlights)); var list = highlights.map(function(h) { return "<p>" + h.getRange().toHtml() + "</p>"; }).join("\n"); return turndown.turndown(list);}Readable markdown composer
static/js/content.jsasync function composeMarkdown(highlights, tags, author, title, url, date) { function ordinal(number) { number = Number(number); if (!number || Math.round(number) !== number) { return number; } var signal = number < 20 ? number : Number(("" + number).slice(-1)); switch (signal) { case 1: return number + "st"; case 2: return number + "nd"; case 3: return number + "rd"; default: return number + "th"; } } function roamFormat(date) { var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; var formatted = months[date.getMonth()]; formatted += " " + ordinal(date.getDate()); return formatted + ", " + date.getFullYear(); } var markdownHighlights = await formatHighlightsToMarkdown(highlights); var indentation = getIndentation(settings.parenttext); var indentedHighlights = markdownHighlights.split("\n").filter(function(l) { return l.trim(); }).map(function(l, i) { if (i === 0) { return "- " + l; } else if (!l) { } else { return indentation + "- " + l; } }).join("\n"); var placeholder = { $AUTHOR: author || " ", $URL: url, $TITLE: title, $TAGS: tags || " ", $HIGHLIGHTS: indentedHighlights, $TIME: Date().slice(16, 21), $DATE: roamFormat(new Date()), $PUBLISHDATE: date ? roamFormat(new Date(date)) : " " }; var markdown = settings.parenttext.replace(/(\$[A-Z]+)/g, function(matched) { return placeholder[matched] || matched; }); markdown.replace(/(\$[A-Z]+)/g, ""); return markdown;}- us.i.posthog.com
PostHog analytics ingest host receiving the copy_highlight_button_clicked event configured in the extension.
What it can do
Permissions this extension asks for, as declared in version 0.4.6. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Store data in your browser
storage
See the address and title of every tab you have open
tabs
Add items to the right-click menu
contextMenus
Where it sends data
Destinations our analysis observed Roam Highlighter contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- us.i.posthog.com
Roam Highlighter sends data to us.i.posthog.com. 24 other extensions we have analysed send data here.