Is Zapier Agents safe?
Zapier Agents is medium risk. Zapier Agents injects an active-tab script during the browser-context flow, returning page text, title, and URL to the Zapier Agents page. Testing did not observe this without login, but the code path is present in the shipped extension.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Active-tab page text is relayed to Zapier Agents
Zapier Agents injects an active-tab script during the browser-context flow, returning page text, title, and URL to the Zapier Agents page.
Testing did not observe this without login, but the code path is present in the shipped extension.
You use the Zapier Agents side panel while browsing in an active tab.
The flow depends on Zapier authentication and the side panel being open.
The extension reads the active tab's visible page text, title, and URL and returns them to the Zapier Agents page.
The service worker sends getPageContent to active-tab-script.js and forwards the response back through the content-script message path.
| Field | Value | Why it matters | |
|---|---|---|---|
Active tab URL | https://example.com/account/settings | Shows the exact page address open in your active browser tab. | |
Page title | Account Settings | Identifies the page or document visible in the browser tab. | |
Visible page text | Project notes and account details visible on the page | Can include readable content from the active page, including private document, workspace, or account text. | |
Google document text | Quarterly planning notes from a Google Doc | When the active tab is a Google Doc, the extension can request document content through the Google Docs API. | |
Google spreadsheet values | This text represents the values in the rows of a spreadsheet: [["Account","Owner"]] | When the active tab is a Google Sheet, the extension can request spreadsheet grid values through the Google Sheets API. |
The service worker injects the active-tab reader and requests page content
chrome.runtime.onMessage.addListener(((t, e, n) => {
if ("getBrowserContext" === t) return async function() {
const t = await vr();
return new Promise(((e, n) => {
t.id ? chrome.tabs.sendMessage(t.id, "getPageContent", (function(r) {
r ? e(r) : (t.url || e(void 0), je(`Unable to get page data from tab: ${t.url} (Last error: ${chrome.runtime.lastError?.message})`), n(new Error(`Unable to get page data from tab: ${t.url}`)))
})) : n(new Error("No current tab"))
}))
}().then((t => {
n(t)
})).catch((() => {
n("")
})), !0
}))chrome.tabs.onActivated.addListener((async ({
tabId: t
}) => {
if (t) {
try {
await chrome.scripting.executeScript({
target: {
tabId: t
},
files: ["active-tab-script.js"]
})
} catch {}
chrome.tabs.query({
active: !0,
currentWindow: !0
}, (async ([t]) => {
Sr(t.url?.includes("linkedin.com") ?? !1)
}))
}
})), chrome.tabs.onUpdated.addListener(((t, e) => {
(e.url || e.status) && chrome.tabs.query({
active: !0,
currentWindow: !0
}, (async ([e]) => {
try {
await chrome.scripting.executeScript({
target: {
tabId: t
},
files: ["active-tab-script.js"]
})
} catch {}
Sr(e.url?.includes("linkedin.com") ?? !1)
}))
}))The active-tab script returns page text, title, and URL
chrome.runtime.onMessage.addListener((function(t, e, n) {
return "getPageContent" === t && (A({
category: "chrome-extension",
message: `getPageContent from ${window.location.href}`,
level: "info"
}), window.location.href.includes("docs.google.com/document") ? (A({
category: "chrome-extension",
message: "getPageContent from Google Doc",
level: "info"
}), chrome.runtime.sendMessage("getAuthToken", (function(t) {
if (t) {
const e = new RegExp(/https:\/\/docs.google.com\/document\/d\/(.*)\//).exec(window.location.href),
s = e?.[1];
if (s) {
const e = {
method: "GET",
headers: {
Authorization: `Bearer ${t}`,
"Content-Type": "application/json"
}
};
try {
fetch(`https://docs.googleapis.com/v1/documents/${s}`, e).then((t => t.json())).then((function(t) {
const e = t.body?.content?.map((t => t.paragraph?.elements)).flat().filter((t => Boolean(t))).map((t => t?.textRun)),
s = e?.map((t => t?.content)).join("") ?? "";
n({
source: "Google Docs",
text: s,
title: t.title,
url: window.location.href
})
}))
} catch (t) {
j(`Fetch failed for for https://docs.googleapis.com/v1/documents/${s}`)
}
} else n({
source: "Google Docs",
text: "Unable to get the content of this document",
title: document.title,
url: window.location.href
}), j(`No documentId found for ${window.location.href}`)
} else n({
source: "Google Docs",
text: "Unable to get the content of this document",
title: document.title,
url: window.location.href
}), j(`No auth token found for ${window.location.href}`)
}))) : window.location.href.includes("docs.google.com/spreadsheets") ? (A({
category: "chrome-extension",
message: "getPageContent from Google Sheet",
level: "info"
}), chrome.runtime.sendMessage("getAuthToken", (function(t) {
if (t) {
const e = new RegExp(/https:\/\/docs.google.com\/spreadsheets\/d\/(.*)\/edit\?gid=(.*)#/).exec(window.location.href),
s = e?.[1],
o = e?.[2];
if (s) {
const e = {
method: "GET",
headers: {
Authorization: `Bearer ${t}`,
"Content-Type": "application/json"
}
};
try {
fetch(`https://sheets.googleapis.com/v4/spreadsheets/${s}?includeGridData=true`, e).then((t => t.json())).then((function(t) {
const e = t.sheets?.find((t => t.properties?.sheetId === Number(o))),
s = e?.data?.map((t => t.rowData?.map((t => t.values?.map((t => t.formattedValue)))))).flat(1);
n({
source: "Google Sheets",
text: `This text represents the values in the rows of a spreadsheet: ${JSON.stringify(s)}`,
title: t.properties?.title ?? document.title,
url: window.location.href
})
}))
} catch (t) {
j(`Fetch failed for for https://sheets.googleapis.com/v4/spreadsheets/${s}`)
}
} else n({
source: "Google Sheets",
text: "Unable to get the content of this spreadsheet",
title: document.title,
url: window.location.href
}), j(`No spreadsheetId found for ${window.location.href}`)
} else n({
source: "Google Sheets",
text: "Unable to get the content of this spreadsheet",
title: document.title,
url: window.location.href
}), j(`No auth token found for ${window.location.href}`)
}))) : window.location.href.includes("linkedin.com") ? (A({
category: "chrome-extension",
message: "getPageContent from LinkedIn",
level: "info"
}), n({})) : (A({
category: "chrome-extension",
message: "getPageContent from standard webpage",
level: "info"
}), n({
text: document.documentElement.innerText,
title: document.title,
url: window.location.href
}))), !0
}))The side panel embeds the remote Zapier Agents page
<html><body><head><style>html, body {
margin: 0;
padding: 0;
height: 100%;
}
iframe {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
border: 0;
}</style><script defer="defer" src="sidepanel.js"></script></head><iframe src="https://agents.zapier.com/chrome-extension" id="central" allow="clipboard-write"></iframe></body></html>- agents.zapier.com
Zapier Agents iframe and page context that receives browser-context responses.
- docs.googleapis.com
Google Docs API endpoint queried when the active tab is a Google Doc and an OAuth token is available.
- sheets.googleapis.com
Google Sheets API endpoint queried when the active tab is a Google Sheet and an OAuth token is available.
Visible tab screenshots relayed to Zapier Agents on request
The extension can capture the visible browser tab as a JPEG when its Zapier Agents page requests a screenshot.
The image is relayed to agents.zapier.com and can include sensitive content visible on the page.
A logged-in Zapier Agents side panel requests a browser screenshot.
The extension captures the visible tab as a JPEG and relays the screenshot response to Zapier Agents.
| Field | Value | Why it matters | |
|---|---|---|---|
Visible tab information | Screenshot of the active tab (illustrative) | The request can include what is currently visible in your active browser tab, including page content shown on screen. |
Dynamic analysis confirmed the screenshot capture path in the extension code. Earlier testing could not trigger the behavior without Zapier authentication and an open side panel.