Is Quillbot: AI Writing Assistant to Grammar Check, Paraphrase & Translate safe?
QuillBot sends behavioral telemetry including the URLs of pages where it is active to its own Snowplow collector.
When users interact with QuillBot, the extension's service worker transmits structured event data — including user ID, the host origin of the active tab, action names, build version, and session context — to collector.quillbot.com via Snowplow self-describing event payloads. A secondary collector at ol-collector.quillbot.com receives the same data. These events are sent automatically as users type or trigger extension features, without a separate disclosure beyond the extension's privacy policy.
Who publishes itQuillBot (Course Hero), LLC - 1 other listing from the same operator, 1 of them carrying a finding
QuillBot (Course Hero), LLC - 1 other listing from the same operator, 1 of them carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same operator - 1 listing
Published under a different store account, but sharing the registered address, contact or declared legal entity this one gave the store.
Shared hosts - 2 hostnames
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
Any quillbot.com or coursehero.com page can request your device ID
QuillBot answers messages from any quillbot.com, coursehero.com, or quillbot.dev page with your device ID and version, no prompt.
It also reads your login cookie.
That ID later showed in a Google ad-conversion request from quillbot.com.
A script on a quillbot.com, coursehero.com, or quillbot.dev page sends a browser message directly to the QuillBot extension.
This works from any script running on those origins, not only QuillBot's own web app code.
The extension's message handler replies with your device ID and the installed extension version.
No prompt or confirmation is shown to you before the reply is sent.
The externally_connectable message handler and the auth-cookie read, shipped vs deobfuscated
!function(e) {
Nn().runtime.onMessageExternal.addListener(function(t, n, r) {
switch (t.action) {
case In.EXTERNAL_MSG_DETECT:
jn("deviceID").then(function(e) {
r({
deviceID: e,
success: !0,
version: Nn().runtime.getManifest().version
})
});
break;
case In.EXTERNAL_MSG_INPUT_TEXT:
t.inputId && jn(t.inputId).then(function(e) {
r({
paraphraseInputText: e
}), Nn().storage.local.remove(t.inputId)
});
break;
// ... additional EXTERNAL_MSG_* cases follow, gated only by
// manifest.json's externally_connectable.matches allowlist
}
})
}Nn().cookies.getAll({
domain: In.QB_COOKIE_INCLUDE_URL, // "quillbot.com"
name: In.QB_ID_TOKEN_STRING // "useridtoken"
}).then(function() {
var e = u(AVe().m(function e(t) {
var n, r, i, o, a, s, u;
return AVe().w(function(e) {
for (;;) switch (e.n) {
case 0:
return e.n = 1, rre();
case 1:
if (n = e.v, !(t.length > 0)) { e.n = 3; break }
return o = t[0].value, e.n = 2, Zne();
// ... reads the cookie's value and stores it for API auth headers
}
})
}())
})| Field | Value | Why it matters | |
|---|---|---|---|
Device ID | ec5a8236-afcc-44a7-9d07-665cdfb65787 | A persistent identifier tied to this specific QuillBot installation, handed to the page on request. | |
Extension version | 4.100.0 | Confirms which QuillBot build is installed. | |
Login-token cookie (read internally) | useridtoken=<session JWT> | Your quillbot.com auth cookie, read by the service worker for QuillBot's own API calls; not confirmed sent externally in this test. |
- ad.doubleclick.net
Google's ad-conversion tracking network. Received the QuillBot-issued device ID as the conversion key on a request fired from quillbot.com.
Sends the same externally-connectable 'detect-extension' message any script on quillbot.com, coursehero.com, or quillbot.dev can send, and logs the device ID and version QuillBot replies with.
// Paste into the DevTools Console on a https://quillbot.com, https://coursehero.com,
// or https://quillbot.dev page. No QuillBot login or UI interaction is required.
const EXTENSION_ID = 'iidnbdjijdkbmajdffnidomddglmieko';
chrome.runtime.sendMessage(EXTENSION_ID, { action: 'detect-extension' }, (response) => {
if (chrome.runtime.lastError) {
console.log('No response:', chrome.runtime.lastError.message);
return;
}
console.log('QuillBot replied with:', response);
});
- 1Install and enable QuillBot.
- 2Open quillbot.com in a tab.
- 3Open DevTools, paste the script into Console.
- 4Press Enter.
- 5The response contains your device ID and version, sent without any prompt.
Browsing URLs sent to QuillBot analytics on every page visit
QuillBot sends the full URL of every page you visit to ol-collector.quillbot.com automatically.
Two POSTs (hostOrigin, hostUrl) fire within seconds of a new tab, plus your account or device ID, premium status, and build version.
You navigate to any web page in Chrome.
The extension sends the full URL and origin of the page you just visited to ol-collector.quillbot.com.
This happens on every navigation, no interaction with the QuillBot UI is required.
| Content-Type | application/json |
| Content-Encoding | gzip |
Gzip-compressed Snowplow payload_data array. Decompressed, each event includes hostOrigin, hostUrl, anon-id or user ID, authenticated, premium, appMode, extensionType, platform-version, and product fields.
| Field | Value | Why it matters | |
|---|---|---|---|
Page URL (hostUrl) | https://www.google.com/ | The exact URL of the page you are visiting, including path, query string, and any tracking parameters. | |
Page origin (hostOrigin) | https://www.google.com | The scheme and hostname of the page, identifying which website you visited. | |
Anonymous device ID (anon-id) | anon-7f3a1e24b9cd | A persistent identifier generated on install, stored in chrome.storage. Lets QuillBot link browsing across sessions without an account. | |
QuillBot user ID | usr_8c2d4b1f3a9e | Your QuillBot account ID, sent when you are logged in. Ties browsing events directly to your named account. | |
Authenticated status | true | Whether you are currently logged in to a QuillBot account. | |
Premium subscription flag | false | Whether your account holds a paid QuillBot subscription. | |
Extension build version | 4.85.0 | The installed version of QuillBot. |
Service worker code that captures the active-tab URL and configures the collector
// When an event fires and no hostOrigin was passed in the message,
// the service worker looks up the active tab's URL directly.
chrome.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
let urlContext = {};
if (!hostOrigin && tabs[0]?.url) {
const parsed = new URL(tabs[0].url);
urlContext = {
hostOrigin: parsed.origin, // e.g. 'https://www.google.com'
hostUrl: parsed.href // e.g. 'https://www.google.com/search?q=...'
};
}
const payload = {
authenticated, // true/false
'anon-id': anonId, // persistent anon device ID
premium, // paid plan flag
appMode, // 'Extension'
extensionType,
hostOrigin, // from caller or active tab
hostUrl, // from caller or active tab
product,
'platform-version': manifestVersion,
...urlContext
};
tracker.sendUserMetricEvent({ userID, action: eventName, value: payload });
});
// Collector endpoint (production) and buffer config:
const collectorUrls = { production: 'https://ol-collector.quillbot.com' };
const config = {
bufferSize: 1_000_000, // 1 MB before forced flush
flushIntervalMillis: 15_000, // flush every 15 seconds
namespace: 'com.quillbot.extension',
storageType: 'CHROME', // backed by chrome.storage
};- ol-collector.quillbot.com
QuillBot's Snowplow collector (namespace com.quillbot.extension). Receives every navigation event with the URL and account context; two POSTs observed per navigation.
- collector.quillbot.com
Secondary QuillBot analytics collector in the service worker's Snowplow tracker. Both collector.quillbot.com and ol-collector.quillbot.com are run by QuillBot (Course Hero Inc.).
QuillBot patches Google Docs canvas and reads full document text
On docs.google.com, QuillBot injects a MAIN-world script replacing five canvas methods and calling Google's undocumented self._docs_annotate_getAnnotatedText to read doc text, returned via qb-gdocs-fullText, not observed leaving the page.
You open a document or presentation on docs.google.com.
QuillBot injects a script into the Google Docs page context that replaces five built-in canvas drawing methods.
It also acquires a handle to read the document's full text and returns that text in response to an internal page event.
Canvas prototype methods replaced and the internal Docs text API acquired (main-world-injection.js)
// M is the QuillBot extension id 'oldceeleldhonbafppcapldpdifcinji'
window._docs_annotate_canvas_by_ext = M;
// Five canvas drawing primitives are replaced with wrappers that record
// stroke geometry (used to draw grammar underlines over the Docs canvas):
const origMoveTo = CanvasRenderingContext2D.prototype.moveTo;
CanvasRenderingContext2D.prototype.moveTo = function (x, y) {
lastPoint.set(this, { x, y });
origMoveTo.call(this, x, y);
};
// ...lineTo, fillRect, clearRect and stroke are wrapped the same way...
// Acquire a handle on the live document text via the undocumented Docs API:
async function initializeAnnotateAPIGetter(extId) {
if (typeof self._docs_annotate_getAnnotatedText !== 'function') return;
const annotated = await self._docs_annotate_getAnnotatedText(extId);
if (annotated && typeof annotated.getText === 'function')
getFullTextFn = annotated.getText.bind(annotated); // whole document
if (annotated && typeof annotated.getSelection === 'function')
getSelectionFn = annotated.getSelection.bind(annotated); // current selection
}| Field | Value | Why it matters | |
|---|---|---|---|
Full document text | (entire document body) | The complete text of the open Google Doc, obtained via getText() and returned in a qb-gdocs-fullText event on request. | |
Current text selection | ["selected sentence"] | The text you currently have highlighted, obtained via getSelection() in response to a qb-gdocs-extractSelections event. | |
Extension-ID page flag | oldceeleldhonbafppcapldpdifcinji | window._docs_annotate_canvas_by_ext set to the QuillBot extension id, signalling to Google Docs which extension is annotating the canvas. |
Document text returned to the content script via a custom DOM event
// The content script (isolated world) dispatches 'qb-gdocs-getFullText'.
// The injected page-world script answers by reading the whole document
// and dispatching 'qb-gdocs-fullText' with the text as the event detail:
document.addEventListener('qb-gdocs-getFullText', () => {
const fullText = getDocumentText(); // getText() or KX_kixApp walk
dispatchCustomEvent(document, 'qb-gdocs-fullText', { text: fullText });
});Reading document text is consistent with QuillBot's stated grammar-checking and paraphrasing function, which needs the document contents to operate inside Google Docs. This analysis confirmed the text-extraction capability is installed and exercisable via the page event; it did not observe the extracted Google Docs text being transmitted to a remote server in this run.
+4 more findings not shown
Where it sends data
Destinations our analysis observed QuillBot 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.
- collector.quillbot.com
QuillBot sends data to collector.quillbot.com. One other extension we have analysed sends data here.
- ol-collector.quillbot.com
QuillBot sends data to ol-collector.quillbot.com. No other extension we have analysed sends data here.