Is Roam Highlighter safe?

High risk

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.

Daniel Wirtzv0.4.6Chrome Web Store
72Risk
Who publishes it

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.

Publisher
Daniel Wirtz

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.

roamresearch.com
Also called by 6 other listings, including Roam Reader, Readwise Exporter

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
What actually happens
You did this

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 did this

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.

Fields the copy handler passes to PostHog
  • Current page URL
    https://www.example.com/research/browser-notes (illustrative)

    Identifies the page you were viewing when you clicked the copy button.

  • Page title
    Browser 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 text
    Browsers can turn short reading sessions into structured notes. (illustrative)

    Includes the page text you highlighted before clicking copy.

  • Highlight count
    2 (illustrative)

    Shows how many selected passages were included in the copy action.

  • Display mode
    dark (illustrative)

    Adds widget context from your browser session to the analytics event.

Captured request
POSThttps://us.i.posthog.com/capture/
The code that does this

The copy path initializes PostHog and sends bookmark or highlight properties

Readable version

Readable PostHog initialization

static/js/content.js
function 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.js
async 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.js
async 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.js
async 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;}
Analytics destination
    • 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.

Updated 30 September 2026hponfflfgcjikmehlcdcnpapicnljkkc