Is Ripple Tool safe?

Medium risk

Ripple Tool sends raw mental-health search queries to Mixpanel and injects server-controlled HTML into search-engine pages.

When a user's search term matches a crisis keyword list, the extension captures the raw search text and transmits it to Mixpanel's analytics service (api-js.mixpanel.com) along with a session ID and extension version. The extension also fetches HTML from cms.ripplesuicideprevention.com on each triggered page and inserts it directly via innerHTML into the active search-engine tab, with no integrity check on the returned content. Both behaviors occur on major search engines including Google, Bing, Yahoo, DuckDuckGo, YouTube, Ecosia, and Ask.

Blue Tea Softwarev0.9.6.2Chrome Web Store
45Risk
Who publishes it

BLUE TEA LIMITED - no other listings under this identity

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Blue Tea Software
Declared legal entity
BLUE TEA LIMITED
Registered address
Unit D4, Fairoaks Airport, Chobham, Woking GU248HU, GB
Registered contact
David Savage

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Crisis Search Terms Forwarded to Mixpanel Analytics

Searching a crisis term on Google, YouTube, Bing, DuckDuckGo, Yahoo, Ecosia, or Ask sends your exact text to Mixpanel (Twilio).

Confirmed: a POST to api-js.mixpanel.com carried the raw term and a session ID, on every keyword match.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You search for a mental-health crisis term on Google, YouTube, Bing, DuckDuckGo, Yahoo, Ecosia, or Ask.

The extension monitors search engine pages and compares each query against a keyword list fetched from its server.

The extension did this

The extension posts the raw search text you typed to Mixpanel's analytics servers before showing the help overlay.

The event payload includes the verbatim search term, a session identifier from localStorage, the full search engine URL, and the installed extension version.

02EvidenceFIELD TABLE
Fields transmitted to Mixpanel in the 'Keyword Matched' event
FieldValueWhy it matters
Crisis search query
howtokillmyselfThe exact text you typed into the search engine, sent verbatim and in lowercase.
Session identifier
1ff58d43-69b3-4365-ae98-9f08da578b90A UUID made on your first keyword match, stored in localStorage. Persists across searches in the session.
Search page URL
https://www.google.com/search?q=how+to+kill+myselfThe full URL of the search engine page where you typed the query, including the query string.
Search term hash
7c222fb2927d828af22f592134e8932480637c0d05f5960d5c68840f7b93a6adA SHA-256 digest of the search term, included alongside the plaintext version.
Extension version
0.9.5The installed version of Ripple Tool, included with every analytics event.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://api-js.mixpanel.com/track
HTTP 200 OK, {"error":null,"status":1} (three POST requests observed during dynamic analysis)
Headers
Content-Typeapplication/x-www-form-urlencoded
Body
Raw POST body (URL form-encoded):

data=W3siZXZlbnQiOiJLZXl3b3JkIE1hdGNoZWQiLCJwcm9wZXJ0aWVzIjp7InNlYXJjaFRlcm1TdHJpbmciOiJob3d0b2tpbGxteXNlbGYiLCJzZXNzaW9uSWQiOiIxZmY1OGQ0My02OWIzLTQzNjUtYWU5OC05ZjA4ZGE1NzhiOTAiLCJleHRlbnNpb25WZXJzaW9uIjoiMC45LjUiLCJ0b2tlbiI6IjI2Y2E2NGJkZTUyNmVlNjRjMGQxNWUyODViZDA4ZTQ1IiwidHJpZ2dlcklkIjoxNywiY2F0ZWdvcnlJZCI6MSwiZnJvbVVSTCI6Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vc2VhcmNoP3E9aG93K3RvK2tpbGwrbXlzZWxmIiwibWVzc2FnZU9mSG9wZUlkIjpudWxsLCJpbWFnZUlkIjpudWxsfX1d

Base64-decoded (searchTermString and sessionId from dynamic analysis; triggerId, fromURL, messageOfHopeId, imageId are illustrative):

[
  {
    "event": "Keyword Matched",
    "properties": {
      "searchTermString": "howtokillmyself",
      "searchTermDigest": "7c222fb2927d828af22f592134e8932480637c0d05f5960d5c68840f7b93a6ad",
      "triggerId": 17,
      "categoryId": 1,
      "fromURL": "https://www.google.com/search?q=how+to+kill+myself",
      "messageOfHopeId": null,
      "imageId": null,
      "sessionId": "1ff58d43-69b3-4365-ae98-9f08da578b90",
      "extensionVersion": "0.9.5",
      "token": "26ca64bde526ee64c0d15e285bd08e45"
    }
  }
]
04EvidenceCODE COMPARE
The code that does this

Analytics function in ripple_analytics.js that fires on every crisis keyword match

What it actually does
// Fires in ripple_analytics.js after matchingTrigger() confirms a crisis keyword match.
// trigger.searchTermString = raw lowercase text the user typed (e.g. "howtokillmyself").
// trigger.searchTermDigest = SHA-256 of that string (computed in storage.js).
// sessionId = UUID in localStorage created at first match; persists across the session.
export const trackKeyword = async (trigger, shadowRoot = null) => {
  // DOM lookup for optional Mixpanel impression IDs — null if overlay not yet rendered.
  let rippleMainContainer = shadowRoot
    ? shadowRoot.querySelector("#ripple-main-container")
    : document.getElementById("ripple-main-container");
  const messageOfHopeId = rippleMainContainer?.dataset.mixpanelMessageOfHopeId || null;
  const imageId = rippleMainContainer?.dataset.mixpanelImageId || null;

  const extensionVersion = chrome.runtime.getManifest().version; // "0.9.5"
  const sessionId = localStorage.getItem("ripple_session_id");    // UUID, e.g. "1ff58d43-..."

  // The raw crisis search term is sent here, verbatim, as searchTermString.
  // The Mixpanel SDK base64-encodes the event array and POSTs it to api-js.mixpanel.com.
  // The hardcoded token in config.js routes all events to a fixed Mixpanel project.
  mixpanel.track("Keyword Matched", {
    searchTermString: trigger.searchTermString,  // raw text the user typed — sent in plaintext
    searchTermDigest: trigger.searchTermDigest,  // SHA-256 of the same text
    triggerId: trigger.trigger_id,
    categoryId: trigger.category_id,
    fromURL: trigger.fromUrl,                    // full search engine URL including query string
    messageOfHopeId,
    imageId,
    sessionId,                                   // localStorage UUID
    extensionVersion
  });
  // Mixpanel token 26ca64bde526ee64c0d15e285bd08e45 is hardcoded in config.js:3.
};
05EvidenceTHIRD PARTY LIST
Recipient of crisis search analytics
  • api-js.mixpanel.com

    Mixpanel ingestion endpoint (Twilio Inc., TWLO). Raw crisis search terms are stored under token 26ca64bde526ee64c0d15e285bd08e45.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Typed ChatGPT/Claude/Gemini/Copilot Prompts Sent to Mixpanel on Keyword Match

Ripple Tool watches for crisis keywords and shows a help panel on a match.

Its script now also runs on ChatGPT, Gemini, Claude.ai, and Copilot, reading your prompt on Send.

A match sends Mixpanel the plaintext words.

Confirmed on ChatGPT.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You type a message containing a mental-health or self-harm phrase into ChatGPT, Claude, Gemini, or Copilot's chat box and submit it.

This extension's content script now runs on those chat sites in addition to search engines.

The extension did this

Reads chat input text, checks it locally against a keyword list, and on a match sends the matched words, not just a hash, to Mixpanel's API.

The same request also includes a session ID and the extension's version number.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://api-eu.mixpanel.com/track/?verbose=1&ip=1&_=1788111741390
HTTP 200. Captured after typing 'how to kill myself' into ChatGPT's prompt box (chatgpt.com, a manifest include_glob match) and pressing Enter.
Body
data=[{"event":"Keyword Matched","properties":{"searchTermString":"howtokillmyself","searchTermDigest":"1ad28fed...","triggerId":135,"categoryId":1,"sessionId":"ffa5497d-f6ba-4519-8212-ebf045f94078","extensionVersion":"0.9.6.2","token":"<redacted>"}}]
03EvidenceCODE COMPARE
The code that does this

Where the plaintext prompt text is captured and sent, despite a local hash-based match

What it actually does
extractLLMText() reads the live DOM text of the chat inputjs/spa-handler.js
extractLLMText(element) {
    if (!element) return "";

    const method = this.domainConfig.textExtractionMethod || "contenteditable";
    let text = "";

    switch (method) {
      case "prosemirror":
        text = this.extractProseMirrorText(element);
        break;
      case "contenteditable":
        text = element.innerText || element.textContent || "";
        break;
      case "input":
        text = element.value || "";
        break;
      default:
        text = element.innerText || element.textContent || element.value || "";
    }

    const placeholder = this.domainConfig.placeholderText;
    if (placeholder && text === placeholder) {
      text = "";
    }

    return text.trim();
  }
matchingTrigger() hashes the text for matching but keeps the plaintextjs/queries.js
export const matchingTrigger = async (searchTerm, triggers) => {
  const searchTermString = searchTerm.join(" ").toLowerCase();
  const searchTermDigest = await digestMessage(searchTermString);
  let matchedObject = triggers[searchTermDigest];

  if (matchedObject) {
    return {
      match: true,
      locale: matchedObject?.locale,
      category_id: matchedObject?.category_id || Category.SUICIDE,
      severity: matchedObject?.severity || DEFAULT_SEVERITY,
      trigger_id: matchedObject?.trigger_id || null,
      searchTermDigest,
      searchTermString,
      fromUrl: true
    };
  }
  ...
}
trackKeyword() sends the plaintext searchTermString, not just the digestjs/ripple_analytics.js
export const trackKeyword = async (trigger, shadowRoot = null) => {
  const mainDocument = window.top?.document || document;
  let rippleMainContainer = shadowRoot
    ? shadowRoot.querySelector("#ripple-main-container")
    : mainDocument.getElementById("ripple-main-container");
  const messageOfHopeId =
    rippleMainContainer?.dataset.mixpanelMessageOfHopeId || null;
  const imageId =
    rippleMainContainer?.dataset.mixpanelImageId || null;
  const extensionVersion = chrome.runtime.getManifest().version;
  const sessionId = localStorage.getItem("ripple_session_id")

  mixpanel.track("Keyword Matched", {
    searchTermString: trigger.searchTermString,
    searchTermDigest: trigger.searchTermDigest,
    triggerId: trigger.trigger_id,
    categoryId: trigger.category_id,
    fromURL: trigger.fromUrl,
    messageOfHopeId,
    imageId,
    sessionId,
    extensionVersion
  });
};
04EvidenceFIELD TABLE
Fields sent to Mixpanel on a 'Keyword Matched' event
FieldValueWhy it matters
The words you typed
howtokillmyselfThe plaintext words from your ChatGPT/Claude/Gemini/Copilot prompt that matched a crisis keyword -- not a hash, the actual text.
Session ID
ffa5497d-f6ba-4519-8212-ebf045f94078A per-browser identifier stored in localStorage that ties this event to your other activity in the same session.
Category and trigger IDs
categoryId: 1, triggerId: 135Internal IDs identifying which keyword category (e.g. suicide/self-harm) and which specific trigger phrase matched.
Extension version
0.9.6.2The installed version of the extension, sent with every event.
05EvidenceTHIRD PARTY LIST
Where the plaintext crisis phrase goes
  • api-eu.mixpanel.com

    Mixpanel's EU ingestion endpoint. Receives 'Keyword Matched'/'no_help_items_listed' events with the plaintext searchTermString on every matched prompt or search.

What it can do

Permissions this extension asks for, as declared in version 0.9.5. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 0.9.6.2, which we have not unpacked yet.

  • Store data in your browser

    storage

Where it sends data

Destinations our analysis observed Ripple Tools 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.

  • api-js.mixpanel.comwidely used

    Ripple Tools sends data to api-js.mixpanel.com. A widely used service: 47 other extensions we have analysed send data here.

  • cms.ripplesuicideprevention.com

    Ripple Tools sends data to cms.ripplesuicideprevention.com. No other extension we have analysed sends data here.

Updated 30 September 2026gikbaopejleepnfocphkejjbgaifnico