Is HyperWrite: AI Writing Assistant and Autocomplete App safe?

High risk

HyperWrite is high risk. An AI-context path asks the background for open-tab context: it lists tabs and returns up to 20 URL/title pairs after filtering. The content script attaches those to page context before the same typeahead request used for AI suggestions.…

Otherside AIv4.2.7.3Chrome Web Store
78Risk
Who publishes it

Otherside AI - 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
Otherside AI
Registered address
116 Brandywine Ln, Melville, NY 11747-5339, US
Registered contact
Matthew Shumer

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

Publishers can request a review.

Findings

Open tab URLs and titles feed HyperWrite context

An AI-context path asks the background for open-tab context: it lists tabs and returns up to 20 URL/title pairs after filtering.

The content script attaches those to page context before the same typeahead request used for AI suggestions.

Severity
High unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You use a HyperWrite AI feature that asks for more context.

The content script requests tab context only in the AI-context path shown in the code.

The extension did this

The extension reads open-tab URLs and titles and attaches them to the page context.

The background component returns up to 20 filtered tab records to the content script.

Tab context fields returned to the content script
  • Open tab URL
    https://docs.example.com/document/d/1Q4-plan/edit (illustrative)

    This can show another page you have open, including the site and path.

  • Open tab title
    Q4 planning notes - Example Docs (illustrative)

    The page title can summarize what the other tab is about.

  • Tab limit
    20 tabs

    The extension can include multiple open pages in one context request.

  • Category filtering
    banking.example and 127.0.0.1 are filtered patterns (illustrative)

    Some banking, health, government, utility, dating, legal, and private-network URLs are filtered before tab records are returned.

Captured request
POSThttps://typeaheads.hyperwriteai.com/typeaheadnsorted
The code that does this

Background handler enumerates tabs and returns URL/title pairs

Readable version

Readable deobfuscated excerpt

deobfuscated/background.bundle.js
const Pf = [/\b(bank|banking|chase|wellsfargo|bofa|bankofamerica|citibank|citi|usbank|pnc|capitalone|tdbank|suntrust|bb&t|truist|regions|keybank|huntington|ally|discover|americanexpress|amex|paypal|venmo|cashapp|zelle|coinbase|binance|kraken|robinhood|etrade|schwab|fidelity|vanguard|merrill|morganstanley|goldmansachs|jpmorgan)\b/i, /\b(healthcare|health|medical|medicare|medicaid|hospital|clinic|doctor|physician|patient|mychart|epic|cerner|kaiserpermanente|mayoclinic|clevelandclinic|johnshopkins|mountsinai|cedarssinai|pharmacy|cvs|walgreens|express-scripts|caremark|anthem|bluecross|blueshield|aetna|cigna|humana|unitedhealth|centene)\b/i, /\.gov$/i, /\b(irs|dmv|ssa|socialsecurity|va|veterans|usps|fedex|ups|passport|immigration|uscis|ice|tsa|fbi|cia|nsa|dhs|state\.gov|treasury|justice|defense|dod|medicare|medicaid|tax|taxes|turbotax|hrblock|jackson-hewitt)\b/i, /\b(insurance|geico|progressive|statefarm|allstate|nationwide|farmers|travelers|liberty|mutual|usaa|metlife|prudential)\b/i, /\b(electric|gas|water|utility|utilities|comcast|xfinity|spectrum|att|verizon|tmobile|sprint|cricket|mint|billing|payment)\b/i, /\b(dating|tinder|bumble|hinge|match|eharmony|okcupid|therapy|counseling|psychiatrist|psychologist|betterhelp|talkspace|divorce|lawyer|attorney|legal)\b/i, /^(localhost|127\.0\.0\.1|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/i];function Af(t = [], e = 20) {  return t.filter((t => ! function(t) {    if (!t) return !1;    try {      const e = new URL(t),        i = e.hostname + e.pathname;      return Pf.some((t => t.test(i)))    } catch (e) {      return Pf.some((e => e.test(t)))    }  }(t.url))).slice(0, e).map((t => ({    url: t.url && t.url.length > 100 ? t.url.substring(0, 97) + "..." : t.url,    title: t.title  })))}if ("getTabContext" === t.action) {  return function(t = 20) {    const e = Date.now();    return Of > 0 && e - Of < 5e3 && Tf >= t ? Promise.resolve(Ef.slice(0, t)) : new Promise(((e, i) => {      chrome.tabs.query({}, (n => {        var r;        if (null === (r = chrome.runtime) || void 0 === r ? void 0 : r.lastError) return void i(new Error(chrome.runtime.lastError.message));        const s = Af(n, t);        Ef = s, Tf = t, Of = Date.now(), e(s)      }))    }))  }(t.limit || 20).then((t => {    i({      tabs: t    })  })).catch((t => {    i({      tabs: []    })  })), !0}
The code that does this

Content script attaches tab records to page context before typeahead

Readable version

Readable deobfuscated excerpt

deobfuscated/app.bundle.js
if (u) try {  const e = yield chrome.runtime.sendMessage({    action: "getTabContext",    limit: 20  });  e && e.tabs && (f = e.tabs)} catch (e) {}let p = Bd({  selectionState: u ? a : void 0,  activeElement: document.activeElement,  forceRefresh: c});f.length > 0 && (Array.isArray(p) ? p = [...p, {  otherTabs: f}] : "object" == typeof p ? p.otherTabs = f : p = {  content: p,  otherTabs: f});const h = "undefined" != typeof performance ? performance.now() : Date.now();ip("typeahead_request_start", {  textLength: (null == o ? void 0 : o.length) || 0,  priorTextLength: (null == r ? void 0 : r.length) || 0,  pageContextItems: Array.isArray(p) ? p.length : null,  hasMetadata: !!d,  typeaheadMetadata: t}), Ao({  typeaheadMetadata: t,  text: o,  pageContext: p,  priorText: r,  email: s,  metadata: d,  onFailure: () => {    i(!1)  }})
Destination used after tab context is attached
    • typeaheads.hyperwriteai.com

    HyperWrite typeahead service that receives page context for AI suggestions when the typeahead request is made.

Typed page text is sent to HyperWrite typeahead

When autocomplete is active, HyperWrite reads the text you're editing plus nearby page context and sends it to its typeahead service via a FormData POST with typed text, prior text, context, metadata, and snippets.

Severity
High unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You type into a page where the autocomplete feature is active.

The keypress handler waits for typing conditions that require a new typeahead suggestion.

The extension did this

The extension packages typed text and page context for a remote typeahead request.

The request includes the current text, prior text, page context, metadata, snippets, and app information.

Fields placed into the typeahead request
  • Text you are editing
    Please revise the vendor renewal paragraph before Friday. (illustrative)

    This is the content you typed or selected for an autocomplete suggestion.

  • Prior text
    Q3 contract renewal notes and approval comments. (illustrative)

    Nearby text gives extra context about what you were writing before the current cursor position.

  • Page context
    {"content":"Quarterly planning document","url":"https://docs.example.com/document/d/1abc234"} (illustrative)

    Surrounding page content and metadata can reveal where the text came from and what else was visible on the page.

  • Metadata and snippets
    {"host":"docs.google.com","app":"Google Docs"} (illustrative)

    Extra context can include app-specific metadata and locally indexed snippets used to improve suggestions.

Captured request
POSThttps://typeaheads.hyperwriteai.com/typeaheadnsorted
The code that does this

Content script opens the typeahead port and sends writing context

Readable version

Readable deobfuscated excerpt

deobfuscated/app.bundle.js
const So = e => chrome.runtime.connect({  name: e}),Co = () => {  if (vo) {    mo && vo.onMessage.removeListener(mo), go && vo.onDisconnect.removeListener(go);    try {      vo.disconnect()    } catch (e) {}  }  vo = So("getTypeAheads"), mo = function(e) {    "complete" === e.status && (e.key && delete xo[e.key], _o(Object.assign({}, e)))  }, vo.onMessage.addListener(mo), go = function(e) {    if (e !== vo) return;    Uo({      eventName: "typeahead_port_disconnect_handler",      metadata: {        documentHidden: null === document || void 0 === document ? void 0 : document.hidden,        timestamp: (new Date).toISOString()      }    }).catch((() => {})), vo.onMessage.removeListener(mo), vo.onDisconnect.removeListener(go), vo = void 0;    setTimeout((() => {      (null === document || void 0 === document ? void 0 : document.hidden) ? document.addEventListener("visibilitychange", (function e() {        document.hidden || (document.removeEventListener("visibilitychange", e), Co())      })): Co()    }), 0)  }, vo.onDisconnect.addListener(go)};Co();
The code that does this

Background script converts the port message into the POST body

Readable version

Port handler forwards fields into the typeahead call

deobfuscated/background.bundle.js
"getTypeAheads" === e.name && e.onMessage.addListener((function(t) {  e.postMessage({      status: "getting typeaheds"    }),    function({      port: t,      message: e    }) {      pf(this, void 0, void 0, (function*() {        let i = e.typeaheadMetadata;        Promise.resolve().then((() => pf(this, void 0, void 0, (function*() {          let t = [];          const n = yield Tr();          var r;          n || (t = yield(r = e.text, Qi(void 0, void 0, void 0, (function*() {            var t;            if (!r) return [];            let e = en.search(r, 5, {              suggest: !0            });            const i = nn.search({              parentid: e,              index: ["content"],              query: r.trim(),              expand: !0,              suggest: !0,              limit: 6            });            let n = [];            const s = null === (t = null == i ? void 0 : i[0]) || void 0 === t ? void 0 : t.result;            if (null == s ? void 0 : s.length)              for (let t = 0; t < s.length; t++) {                const e = yield Zi.sections.get(s[t]);                if (e) {                  const t = e.content;                  n.push({                    text: t                  })                }              }            return n          }))));          const s = yield wn();          return (n ? vr : fr)({            typeaheadMetadata: i,            text: e.text,            title: e.title,            description: e.description,            pageContext: e.pageContext,            priorText: e.priorText,            email: e.email,            app: e.app,            metadata: e.metadata,            similarSnippets: t,            variant: null == s ? void 0 : s.variant,            testCase: null == s ? void 0 : s.test_case,            experimentVersion: null == s ? void 0 : s.experiment_version          })        }))))      }))    }({      port: e,      message: t    })}))

FormData POST to the typeahead endpoint

deobfuscated/background.bundle.js
fr = ({  typeaheadMetadata: t,  text: e,  title: i = "Email",  description: n = "Email",  pageContext: r,  priorText: s,  email: o = "",  app: a = "",  metadata: u = {},  similarSnippets: l = [],  variant: c,  testCase: h,  experimentVersion: d}) => lr(void 0, void 0, void 0, (function*() {  let f = new FormData;  const v = yield Bn();  f.append("writing", e), f.append("title", i), f.append("description", n), f.append("snippets", JSON.stringify(l)), t && f.append("typeaheadMetadata", JSON.stringify(t)), f.append("website", "Superhuman"), r && f.append("pageContext", JSON.stringify(r)), s && f.append("priorText", s), a && f.append("app", a), u && f.append("metadata", JSON.stringify(u)), "string" == typeof c && c.length > 0 && f.append("variant", c), null != h && f.append("test_case", String(h)), null != d && f.append("experiment_version", String(d)), f.append("allowPartial", "true"), f.append("version", "0.0.3");  const p = new AbortController,    g = setTimeout((() => p.abort()), 5e3);  let y = {      method: "POST",      body: f,      redirect: "follow",      signal: p.signal,      headers: {        Authorization: `Bearer ${v}`      }    },    m = (new Date).getTime();  const _ = yield fetch(hr + "/typeaheadnsorted", y).then((t => t.json())).catch((t => "AbortError" === (null == t ? void 0 : t.name) ? {    choices: [],    success: !1,    error: "ClientTimeout"  } : ["Error generating typeahead"])).finally((() => {    clearTimeout(g)  }));  if (o.endsWith("@othersideai.com")) {    (new Date).getTime();    false  }  return _}))
Remote destination receiving the typeahead request
    • typeaheads.hyperwriteai.com

    HyperWrite typeahead service that receives typed text and page context for autocomplete suggestions.

Typeahead metrics sent to HyperWrite's API

HyperWrite's typeahead sends metrics to api.hyperwriteai.com on show/accept: acceptance method, site/app context, typeahead IDs, character counts, metadata.

A separate path sends timing data; testing missed these requests.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You view or accept a HyperWrite typeahead suggestion on a supported website.

The content script watches typeahead interactions such as Tab acceptance, right-arrow acceptance, and option toggles.

The extension did this

The extension prepares form-data metrics and posts them to HyperWrite's API.

The background script handles messages named logAcceptedTypeahead, logShownTypeahead, and saveMeasurements.

Fields prepared by the typeahead logging paths
  • Site or app context
    mail.google.com

    Shows which website or web app you were using when the typeahead event happened.

  • Acceptance method
    tab

    Shows how you interacted with the suggestion, such as accepting it with Tab or the right arrow.

  • Typeahead identifier
    gen_7f3a9c2b

    Can tie a shown suggestion to a later acceptance event for the same generated suggestion.

  • Character counts
    acceptedChars: 18

    Shows how much of the suggested text was accepted or displayed during the interaction.

  • Measurement counters
    messageTime: 1240, keyDownCount: 6, acceptedCount: 1

    Records timing and keyboard counters for typeahead use when the measurement path runs.

Captured request
POSThttps://api.hyperwriteai.com/logAcceptedTypeahead

The endpoint and method are defined in the shipped fetch call; the traffic analysis run did not record a request body for this endpoint.

Remote host receiving the typeahead metrics
    • api.hyperwriteai.com

    HyperWrite API host used for authenticated typeahead acceptance, display, measurement logging, and the logged-out /logTypeaheadNoAuth fallback path.

The code that does this

The content script records typeahead actions and the background script posts them

Readable version

API base URL used by the background script

deobfuscated/background.bundle.js
let cr, hr;cr = e.BASE_URL ? e.BASE_URL : "https://api.hyperwriteai.com/", hr = e.OVERRIDE_TYPEAHEAD_URL ? e.OVERRIDE_TYPEAHEAD_URL : "https://typeaheads.hyperwriteai.com/";

The page URL is reduced to a host/app value

deobfuscated/app.bundle.js
const z = e => {  const t = new URL(e).hostname,    n = N(e, t);  return n || {    hostname: t,    host: "General"  }};const Hp = window.location.href;const {  host: Xp,  hostname: Zp} = z(Hp);

The content script sends acceptance and display messages

deobfuscated/app.bundle.js
const Fo = ({    type: e,    metadata: t,    id: n,    app: r  }) => ho(void 0, void 0, void 0, (function*() {    const o = So("logAcceptedTypeahead");    yield new Promise(((i, a) => {      const l = setTimeout((() => a(new Error("timeout on log success"))), 3e3);      o.postMessage({        action: "logAcceptedTypeahead",        type: e,        metadata: t,        id: n,        app: r      }), clearTimeout(l)    }))  })),  Wo = ({    type: e,    app: t,    metadata: n  }) => ho(void 0, void 0, void 0, (function*() {    const r = So("logShownTypeahead");    yield new Promise(((o, i) => {      const a = setTimeout((() => i(new Error("timeout on log shown"))), 3e3);      r.postMessage({        action: "logShownTypeahead",        type: e,        app: t,        metadata: n      }), clearTimeout(a)    }))  }));

The background script posts the form data

deobfuscated/background.bundle.js
"saveMeasurements" === e.name && e.onMessage.addListener((function(t) {  var i;  e.postMessage({    status: "saving measurements"  }), i = t, lr(void 0, void 0, void 0, (function*() {    const t = cr + "/saveTypeAheadMeasurements";    let e = new FormData;    const n = yield Bn();    return (null == i ? void 0 : i.messageTime) && e.append("messageTime", i.messageTime), (null == i ? void 0 : i.keyDownCount) && e.append("keyDownCount", i.keyDownCount), (null == i ? void 0 : i.appName) && e.append("platform", i.appName), (null == i ? void 0 : i.acceptedCount) && e.append("acceptedCount", i.acceptedCount), (null == i ? void 0 : i.notAcceptedCount) && e.append("notAcceptedCount", i.notAcceptedCount), yield fetch(t, {      method: "POST",      body: e,      headers: {        Authorization: `Bearer ${n}`      }    }).then((t => {})).catch((t => ({})))  }))})), "logAcceptedTypeahead" === e.name && e.onMessage.addListener((function(t) {  var i;  return pf(this, void 0, void 0, (function*() {    e.postMessage({      status: "getting typeaheds"    });    const n = yield Tr();    ! function(t, e) {      var i, n, r, s, o;      pf(this, void 0, void 0, (function*() {        try {          const a = yield vf(), u = yield Bn().catch((() => null)), l = u ? Sf(u)["https://example.com/userid"] : null, c = chrome.runtime.getManifest();          cf.capture("typeahead_accepted", {            anonymous_extension_id: a,            app: t.app || null,            accept_type: t.type || null,            generation_id: (null === (i = t.metadata) || void 0 === i ? void 0 : i.generation_id) || t.id || null,            rank_id: null !== (r = null === (n = t.metadata) || void 0 === n ? void 0 : n.rank_id) && void 0 !== r ? r : null,            accepted_chars: null !== (o = null === (s = t.metadata) || void 0 === s ? void 0 : s.acceptedChars) && void 0 !== o ? o : null,            user_id: l ? String(l) : null,            logged_out_typeahead: Boolean(e),            extension_version: c.version,            manifest_version: c.manifest_version,            browser: "chromium",            source: "chrome_extension"          }, {            send_instantly: !0          })        } catch (t) {}      }))    }(t, n), n ? yield(({      event: t,      generation_id: e,      app: i,      metadata: n = {},      display_type: r,      accept_type: s    }) => lr(void 0, void 0, void 0, (function*() {      const o = new FormData;      return o.append("event", t), o.append("anonymous_extension_id", yield dr()), o.append("generation_id", e || ""), o.append("app", i || "unknown"), o.append("metadata", JSON.stringify(n || {})), r && o.append("display_type", r), s && o.append("accept_type", s), yield fetch(cr + "/logTypeaheadNoAuth", {        method: "POST",        body: o      }).then((t => t.json())).catch((t => ({})))    })))({      event: "accepted",      generation_id: (null === (i = t.metadata) || void 0 === i ? void 0 : i.generation_id) || t.id,      app: t.app,      metadata: t.metadata,      accept_type: t.type    }) : yield(({      type: t,      metadata: e,      app: i,      id: n    }) => lr(void 0, void 0, void 0, (function*() {      let r = new FormData;      const s = yield Bn();      r.append("type", t), void 0 !== e && r.append("metadata", JSON.stringify(e)), r.append("app", i), null != n && r.append("id", n);      let o = {        method: "POST",        body: r,        redirect: "follow",        headers: {          Authorization: `Bearer ${s}`        }      };      return yield fetch(cr + "/logAcceptedTypeahead", o).then((t => t.json())).catch((t => ["Error saving"]))    })))({      type: t.type,      metadata: t.metadata,      app: t.app,      id: t.id    })  }))})), "logShownTypeahead" === e.name && e.onMessage.addListener((function(t) {  return pf(this, void 0, void 0, (function*() {    e.postMessage({      status: "getting typeaheds"    }), (yield Tr()) || (yield(({      type: t,      app: e,      metadata: i    }) => lr(void 0, void 0, void 0, (function*() {      let n = new FormData;      const r = yield Bn();      n.append("type", t), n.append("app", e), n.append("metadata", JSON.stringify(i));      let s = {        method: "POST",        body: n,        redirect: "follow",        headers: {          Authorization: `Bearer ${r}`        }      };      return yield fetch(cr + "/logShownTypeahead", s).then((t => t.json())).catch((t => ["Error saving"]))    })))({      type: t.type,      app: t.app,      metadata: t.metadata    }))  }))}));
Updated 30 September 2026kljjoeapehcmaphfcjkmbhkinoaopdnd