Is Linguix: Grammar Checker and AI Writing App safe?

Medium risk

Linguix sends text fragments surrounding grammar corrections to Amplitude analytics on any website.

When a user accepts a grammar or writing suggestion, Linguix captures up to 100 characters of surrounding text (the 'context' field) along with the rule triggered and the replacement made. This data is transmitted to Amplitude via its HTTP API using a hardcoded project key. The user's Linguix account ID or a generated UUID is attached to each event as the Amplitude userId.

Linguixv1.6.146Chrome Web Store
45Risk

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

Typed writing context is sent to Amplitude after accepted suggestions

When you accept a Linguix suggestion, the extension logs a ReplacementClicked event to Amplitude.

Fields come from the rule, nearby context, replacement text, and source; the user ID comes from the Linguix account ID or a stored UUID.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You accept a suggested writing replacement on a webpage.

This can happen inside forms or editable page content where the extension is active.

The extension did this

The extension logs an analytics event that includes the nearby writing context.

The event is named ReplacementClicked and is routed through the Amplitude SDK.

02EvidenceFIELD TABLE
Fields passed into the ReplacementClicked analytics event
FieldValueWhy it matters
Rule that fired
MORFOLOGIK_RULE_EN_US (illustrative)Shows which writing rule matched the text you were editing.
Nearby typed text
Please reviev the onboarding notes before Friday. (illustrative)Can reveal part of what you were writing on the page when you accepted the suggestion.
Replacement text
review (illustrative)Shows the corrected word or phrase you chose from the suggestion popover.
Analytics user identifier
8f2c6b7e-41a6-4e28-9e5f-1d43d6a9c2b4 (illustrative)Lets the analytics provider link the event to the same extension account or stored browser profile over time.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://api2.amplitude.com/2/httpapi
04EvidenceCODE COMPARE
The code that does this

The accepted replacement is converted into an Amplitude analytics event

What it actually does
Content script emits the replacement detailsext-content/main.js
replace(t, n) {
  this.prevent(n), !this.authUser.actionRequired() || this.authUser.actionRequired() !== z.TRIAL_ENDED || this.authUser.isPremium() ? (this.authUser.actionRequired() && this.authUser.actionRequired() === z.TRIAL_ENDS_SOON && this.alert.isPremium && chrome.storage.local.set({
    [z0]: this.premiumReplacesDoneToday + 1
  }), this.checkable.replace(this.alert.id, t), this.auth.isGuest() && chrome.storage.local.get("guestReplacementCount").then(a => {
    chrome.storage.local.set({
      guestReplacementCount: (a.guestReplacementCount || 0) + 1
    })
  }), chrome.storage.local.get("totalReplacementCount").then(a => {
    chrome.storage.local.set({
      totalReplacementCount: (a.totalReplacementCount || 0) + 1
    })
  }), this.onMessage.emit({
    type: s1,
    message: {
      sessionId: this.checkable.id,
      rule: this.alert.rule,
      context: this.alert.context,
      replacement: t,
      from: "alert",
      sentence: this.alert.wholeSentence
    }
  })) : this.goPremiumTrialExpired()
}
Background worker forwards the action to analyticsext-background/main.js
this.onLogReplacement$ = ge(() => this.withOnlineCheckerAction(Gu).pipe($e(({
  action: M,
  checker: Y
}) => {
  Y.ensureIsConnected(), Y.logReplacement(M.rule, M.context, M.replacement, M.sentence), Cr.replacementClickedEvent(M.rule, M.context, M.replacement, M.from)
})), {
  dispatch: !1
})
Analytics wrapper maps fields into ReplacementClickedext-background/main.js
class Cr {
  static init(n) {
    S0("050aec1607a54971d0794e2397b6979b", n)
  }
  static rephraseRequestedEvent(n, r, a, u) {
    Cr.logEvent("RephraseRequested", {
      type: n,
      limitReached: r,
      domain: a,
      isSimplifier: u
    })
  }
  static replacementClickedEvent(n, r, a, u) {
    Cr.logEvent("ReplacementClicked", {
      rule: n,
      context: r,
      replacement: a,
      from: u
    })
  }
  static logEvent(n, r) {
    Yi(n, r)
  }
}
Report helper bounds context around the flagged textext-background/main.js
constructReport(n, r, a = 100) {
  if (!this.currentData) return;
  const p = this.currentData.text.split("\n")[n.lineNumber];
  if (!p) return;
  const C = p.substring(Math.max(0, n.offset - a), n.offset + n.length + a);
  return {
    id: ts.generateUuid(),
    type: r,
    context: C
  }
}
05EvidenceTHIRD PARTY LIST
External analytics destination
  • api2.amplitude.com

    Amplitude HTTP API endpoint that receives the ReplacementClicked analytics event.

Data recipients

api2.amplitude.com
Updated 17 September 2026ndgklmlnheedegipcohgcbjhhgddendc