What a Google Sheets add-on can see
Written 6 September 2026, and the network section rewritten on 28 September 2026 after published result links and the Guide added traffic the first version did not know about. The permission list and the payload below were read out of Sortia's own appsscript.json and its sidebar code on the later date, and the simulation was run at seed 20260906. You can reproduce all of it, and the last section says how.
The consent screen is the only moment you get to decide. It looks like a formality, which is why almost everybody clicks through it, and it is not one: those two or three lines are the entire access you are handing over, and the difference between two of them is the difference between one file and every file you own.
This page is about how to read that screen, for any add-on. It uses one add-on as the specimen, because a specimen you can check beats a principle you cannot, and the one we can open the source of is our own. Nothing here is a claim about anybody else's add-on.
The screen is asking two questions, not one
Strip the wording away and almost every permission screen for a spreadsheet add-on comes down to two decisions.
How far does it reach? One file, or the account. "The spreadsheet you use this with" and "your Google Drive files" are two completely different requests, and on screen they are one line apart in a list that looks uniform. If a tool operates on the sheet you have open, there is no reason for it to be able to open the sheet you have not.
When can it act? While you are sitting there, or on its own. Permission to run when you are not present is a separate grant, and it is the one that turns an add-on from something you use into something that runs. Some tools genuinely need it, for scheduled refreshes. A tool that does not schedule anything should not be asking.
Everything else on that screen is detail. Those two answers are the shape of what you are agreeing to.
Read the scope, not the sentence
The friendly sentence on the consent screen is a translation. The thing being granted is an OAuth scope, which is a URL, and the last part of that URL is the part worth reading. A scope with a suffix on it is a narrowed one; a scope with no suffix is the wide version of the same thing.
| The scope ends with | What it grants |
|---|---|
spreadsheets.currentonly | The one spreadsheet the add-on is open in. Not your other spreadsheets, and nothing else in the account. |
spreadsheets.readonly | Read every spreadsheet in the account. |
spreadsheets | Read and write every spreadsheet in the account. |
drive.file | Only the files this app creates, or that you specifically open with it. |
drive.readonly | Read every file in the Drive, of any type. |
drive | Read, write and delete every file in the Drive. |
script.container.ui | Draw a sidebar or a dialog inside the document you are in. |
script.external_request | The add-on's own server code may call any outside address. |
script.scriptapp | Run on a trigger, when you are not present. |
userinfo.email | The email address of the account it is running under. |
Two of those pairs are worth staring at, because they look almost identical in a list and are not close at all. drive.file is bounded by your own choices: the app sees a file when you hand it one. drive is bounded by nothing, and includes files you had forgotten you owned. Likewise spreadsheets.currentonly against spreadsheets: one is the file in front of you, the other is all of them.
Google publishes the authoritative list of every scope and what it covers at developers.google.com/identity/protocols/oauth2/scopes. If a scope on a consent screen is not obvious to you, that page is where to settle it, and it takes about a minute.
You can also read this after the fact. Every app you have ever approved is listed under third-party access in your Google Account, at myaccount.google.com/permissions, with what it was granted and a button to take it back. That page is worth ten minutes once a year regardless of what you install next.
The specimen: one add-on's entire permission list
An Apps Script add-on declares its scopes in a manifest file that ships inside it. Here is all of Sortia's, copied from appsscript.json:
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets.currentonly",
"https://www.googleapis.com/auth/script.container.ui"
]
Two lines. The first is read and write access to the one spreadsheet you opened the add-on in, so it can read the ranges you point a tool at and write the report tabs back. The second draws the sidebar.
Those two lines are not the whole consent screen, and this is the part most write-ups skip. Google attaches two of its own to every Workspace Marketplace install, your primary Google account email address and your basic profile information, so the Permissions tab on the listing shows four where the manifest shows two. Count four and you are counting correctly. The manifest is still the thing that decides what the code can do: Apps Script hands a function only what the script itself declared, so Sortia asks Google for the running account and gets an empty string back. That is checkable from the outside, because it is why activating an organization key has to ask the holder to type an address at the covered domain. If the add-on could read the address, it would not have to ask.
What matters more than what any of that allows is the list of things no line in the manifest allows, because that list is not a promise anyone made. It is arithmetic on the manifest:
- It cannot open another spreadsheet in the account, including the one in your next browser tab.
- It cannot list, read, write or delete anything else in Drive. There is no Drive scope of any kind.
- It cannot see the email address of the account it is running under, whatever the Permissions tab lists, because the manifest is what the runtime honors and the manifest has no email scope. That is not the same as nobody knowing: if you type an address onto an activation screen, or paste a license key, you handed that over yourself. Both are named further down this page, and both are on the policy.
- It cannot run when you are not there. No trigger scope, so there is nothing to fire.
- Its server-side code cannot make a network call at all, because
script.external_requestis not in the list.
That last one has a consequence that the next section is entirely about, and it is the reason this page exists rather than stopping here with a short list and a compliment.
What a short scope list still cannot tell you
A scope list bounds what an add-on's Apps Script code can ask Google for. It does not bound what the sidebar's own JavaScript can send over the network, because a request from the sidebar is an ordinary browser request and not a Google API call at all. No OAuth scope governs it, and none is asked for.
That is not a loophole somebody found. It is how the platform works, and Sortia relies on it: every request that carries anything is a browser fetch from the sidebar, which is exactly why the manifest above does not need script.external_request.
When this page was written in September there were five such addresses. There are now twelve, because published result links, the Guide and connecting to a sortia.io account all shipped since, and a count that was right once is worth less than nothing if nobody goes back and fixes it. All twelve are under https://www.sortia.io/api, and this is the whole list:
| Address | When it fires, and what it carries |
|---|---|
/gateEvent | Anonymous usage counts. No account identifier, no license key, nothing from your sheet. |
/latestKey | Collects a renewed license key in the two weeks before yours expires. Carries the key. |
/aiPolicy | On an organization-wide key only: asks whether the administrator has switched the AI reading on. Carries the key and nothing else, and it goes out before the answer is known, so it is sent on a seat where the answer turns out to be no. |
/aiTurn | The AI box in the sidebar. It is switched off today, and when it is on it sends nothing until you tick yes. Then it carries your question, the cells you selected, the shown text of the used cells on the tab you are on (up to 2,000 cells, so the answer can use your own labels and read a cell you name, such as E5; the server reads them with code and does not send them to an AI model), the conversation so far, and a short signed ticket that lets the server count spending. It never carries the license key. |
/explain | A written reading of a result, for a Pro user who asks for one, and the Guide's replies. The payload is printed byte for byte further down this page. |
/claimDomain | Activating an organization-wide site or school license. Carries the key and the work address typed on the activation screen. |
/sharePolicy | On an organization-wide key only: asks whether your organization allows publishing at all. Carries the domain from the key. |
/sharePublish | Publishing one result as a read-only page, after you have seen a preview of exactly what the page will hold and pressed Publish. |
/shareList | Listing the links you have published, when you ask Settings to show them. |
/shareRevoke | Taking one of those pages down. |
/connect | In the new Sortia only, when you press Connect, Check connection or Disconnect under Settings, sortia.io account. Carries the fixed hashed id your links are filed under and, on a paid plan if you tick Include my license (it starts unticked), your license key, once. It answers with a one-time code, or with whether the add-on is connected and that account’s address with most of it hidden; never a plan or a license. |
/founding | Only when the upgrade screen opens: a plain read of how many founding places are left, the same count the pricing page shows. It sends nothing from your sheet, your account or your key. |
/runLargeSim | Pro only, and only for an export of a simulation larger than a browser tab can build: past 250,000 runs the model itself is sent to be run on our server and the finished file comes back. This is the one request that carries anything derived from the cells of your model, and it is described in its own section of the privacy policy. |
/claimDomain is worth stating plainly, because it is the one request that carries an address you typed, in a field of its own next to the key. It fires when somebody activates an organization-wide site license, once per activation, so a single key claimed far past the organization that bought it is visible before the renewal rather than after it. The server turns that address into a keyed hash and keeps the hash and a count; the address itself is not stored. A personal license never reaches it.
Two more things cross the network and neither is one of the twelve, so they belong on a page like this rather than left for you to find. When the panel opens it pulls its typefaces from Google's font service, the way an ordinary web page does: a stylesheet and a few font files, nothing of ours and nothing of your sheet's, and the network panel files it under CSS and Font rather than Fetch. And when /runLargeSim has finished, the sidebar downloads the finished file from the one-time storage.googleapis.com link that request handed back, which is a download and carries nothing outward.
So the honest reading of a two-line permission list is this. It is a ceiling on how far an add-on can reach into your account, and a low ceiling is genuinely worth something: it is the difference between a bad day and a very bad one. It is not a description of what the code does with the file it can already see. Anyone who tells you a short scope list makes an add-on safe has told you half of it.
Which leaves a fair question: if the permission screen cannot answer that, what can? Three things, in increasing order of how much they are worth.
- A privacy policy that names every request. Not "we may collect usage data". A list of the addresses called, the fields sent and the reason. If a policy is written in a way that could cover anything, it is covering something.
- The product printing the payload it sent. Rare, and worth looking for. A claim you can open is a different kind of claim.
- Watching it yourself. This is the one nobody mentions and it takes two minutes. Open your browser's developer tools, go to the network panel, filter to Fetch or XHR, then use the add-on. The panel lists the requests every frame on the page makes, the sidebar included, and you can click any of them and read the body that was sent. You do not need to trust a policy about traffic you can watch.
The worked example: what actually leaves, byte for byte
Here is the whole of it on one real result. Sortia writes a plain-language reading under every run; for everyone that sentence is composed in your browser from the figures the engine already computed, and nothing is sent. A Pro user can additionally have a short reading written by a language model, which is the one path where anything derived from a result leaves the tab. This is that path, on a sheet whose figures nobody would want anywhere.
The spreadsheet is five cells:
| Cell | Label | Value or distribution |
|---|---|---|
B2 | Average headcount | Triangular, 84 low, 92 likely, 105 high |
B3 | Average salary | Normal, mean 118,400, standard deviation 9,600 |
B4 | Benefits load | Uniform, 0.22 to 0.31 |
B5 | Bonus pool | 150,000 |
B7 | Total people cost | =B2*B3*(1+B4)+B5 |
Ten thousand runs, Latin hypercube sampling, seed 20260906. The engine's answer: the median total is 14,135,866, eight runs in ten land between 12,479,671 and 15,939,186, and the mean is 14,179,066. Average salary moves the result most, at a rank correlation of +0.84, ahead of headcount at +0.48 and the benefits load at +0.21.
Now the two artifacts. On the left is the payload: the whole of what this result contributes to the request that goes over the wire when it is sent for a written reading. It is printed here with line breaks so it can be read; on the wire it is one line, and one line is how the product prints it too. On the right is what the browser keeps back to fill the sentence in with afterwards.
Leaves the tab
{
"kind": "simulation",
"trials": 10000,
"outputs": [
{
"ref": "B7",
"p10r": 0.883,
"p90r": 1.128,
"meanr": 1.003
}
],
"drivers": [
{ "ref": "B3", "rho": 0.84 },
{ "ref": "B2", "rho": 0.48 },
{ "ref": "B4", "rho": 0.21 }
]
}
Stays in the browser
{
"output": "Total people cost (B7)",
"p10": "12.5M",
"p50": "14.1M",
"p90": "15.9M",
"mean": "14.2M",
"driver1": "Average salary (B3)",
"rho1": "+0.84",
"driver2": "Average headcount (B2)",
"rho2": "+0.48",
"driver3": "Benefits load (B4)",
"rho3": "+0.21"
}
On one line the left block is 185 bytes. It carries no label, no money and no formula. The three numbers marked in it are ratios to the median, which is the part that does the work: p10r of 0.883 says the tenth percentile sits 11.7% below the median, and p90r of 1.128 says the ninetieth sits 12.8% above it. Neither says the median is 14,135,866. The shape of the answer goes; the size of it does not, and the size is not recoverable from the shape.
The right block is display text rather than raw figures. Sortia abbreviates a readout the same way on every surface, so where the engine computed 14,135,866 the browser holds 14.1M, and a cell formatted as currency would hold $14.1M. Those strings never leave the tab: they are what the browser puts into the finished sentence wherever the model left a placeholder.
The cell references go, because a sentence that says which input to tighten has to be able to point at one. B7 and B3 mean something inside your file and nothing at all outside it.
The request wraps that payload in exactly two more fields: which surface asked, and the license key, so the server can confirm the plan before doing anything. Say plainly what that key is, because it is the one part of this request that is about you rather than about the result. A key is base64 of the address the license was bought under and its expiry date, then a signature. The address is therefore readable by anyone holding the key, you included: decode the first part and it is there. That is how the plan is confirmed without an account system, and on an organization-wide license the encoded address is the domain rather than a person. The key is not derived from anything in your sheet, and it is not forwarded to the model. Free users never reach this path, and organization-wide licenses have it off until an administrator turns it on. Finding that out is itself a request, and it is worth saying so: on an organization-wide seat the key alone goes to /aiPolicy once a session, before the answer is known, which means it is sent on a seat where the answer comes back off as well as on one where it comes back on. Nothing about your result goes with it. The privacy policy inventories every field on this page and every other request the add-on makes.
The field list is closed, and it is checked twice
A payload that only carries safe fields today is a promise about today. What makes it a property of the code instead is that the contract is a list of permitted fields rather than a list of forbidden ones: anything the list does not name is refused, so a new field cannot start leaving by accident when somebody adds one upstream. For a simulation reading the permitted top-level fields are four, kind, trials, outputs and drivers, and the numeric fields a single output may carry are six: p10r, p90r, meanr, p80r, cv and skew. Every one of them is a ratio or a coefficient.
The check runs in the browser before the request is made, and again on the server before the request is acted on. Here is the same payload doctored six ways, put through both:
| The payload above, but | In the browser | At the server |
|---|---|---|
| unaltered | sent | accepted |
with label: "Total people cost" added | refused, field | refused, field |
with the output's ref replaced by the row label | refused, ref | refused, ref |
with the real median added as p50: 14135866 | refused, output-field | refused, output-field |
| with a driver carrying its label | refused, driver-field | refused, driver-field |
with sheet: "People" added | refused, field | refused, field |
The same contract written twice, same verdict and same reason on every case. The server copy is deliberately a mirror of the browser's rather than a second derivation, so the two agreeing is not evidence that the contract is the right one; it is what writing it twice is for. The browser check is the one that stops the request leaving, and the server check is the one that still holds if a browser is ever persuaded to send something the browser check would have stopped.
What this page does not claim
Being clear about this is most of what makes the rest worth reading.
- No certification, no audit, no badge. Sortia has not been through an independent third-party security assessment, and nothing on this page should be read as one. Sortia has been through Google's OAuth verification, which is a review of whether the access an app asks for matches what it says it does. That is a useful check and it is not a security audit, and this page does not dress it up as one.
- A narrow scope list is not a guarantee of behavior. It is a ceiling on what is possible, which is the weaker-sounding claim and the stronger real one, for the reason set out three sections up.
- Nothing here is a claim about any other add-on. We have not read anybody else's code and are not going to describe it. The method on this page is the useful part, and it works on any consent screen, including one we are not on.
- Your spreadsheet data stays in your sheet unless you send it is the scoped claim and the only one we make. Usage counts, license checks and the payload above do leave, they are listed on the privacy policy, and pretending otherwise would be the exact kind of thing this page is telling you to distrust.
Five questions to ask on any consent screen
- One file, or the account? Look for
currentonlyordrive.file. If the answer is the account, the tool needs a reason and you should be able to name it. - Can it act while I am away? A trigger scope on a tool that does not schedule anything is worth a question.
- Does it need to know who I am? Plenty of tools do not, and ask anyway.
- Where does the data go, and does anyone say so in specifics? Addresses and field names, not adjectives.
- Can I watch it? Network panel, two minutes, before you put anything real in the file.
If you are the person a colleague forwarded this to for approval, those five questions and the account permissions page are most of a review. The teams page covers the rest of what a finance or IT reviewer usually asks, and validation covers how the numbers themselves are checked.
How to reproduce what is on this page
Every figure above is checkable, and most of it without taking our word for anything.
- The permission list. It appears on the consent screen when you install, and afterwards at myaccount.google.com/permissions. Two entries, and no Drive.
- The simulation. Build the five cells in the table, set the three distributions, leave the run count at its default of 10,000 with Latin hypercube sampling on, and put 20260906 in the seed box under Advanced. The engines are deterministic under a seed, so the percentiles come back identical on any machine running the same engine build, which the receipt under the result names. Start here walks the first run if you have not done one.
- The payload. Under any reading a model wrote, open "See what was sent". The panel prints that same JSON on one line, for whatever result is on your screen.
- The traffic. Network panel, filtered to Fetch or XHR. You should see calls only to the thirteen addresses in the table above, and no others. Take the filter off and there is more, but none of it is ours: the typefaces described further up, and the Google resources that serve the sidebar frame itself, which every Apps Script add-on loads and none of which we choose or control. Which of the eleven show up depends on what you do, and most sessions show one. /gateEvent in any session; /latestKey only inside the last two weeks of a key, or after it has already expired; /aiPolicy only on an organization-wide license, once per sidebar session, at the first moment a reading would be requested, which is before anyone knows whether the answer is yes; /explain only when a reading is actually sent, or when you type to the Guide, so on an organization seat with the feature switched off you will see the policy request and never the reading; /claimDomain only at the moment a site license is activated; the four /share addresses only when you preview, publish, list or revoke a published page, and /sharePolicy only on an organization key; /connect only when you press Connect, Check connection or Disconnect; and /runLargeSim only if you are on Pro and export a simulation of more than 250,000 runs.
If you find something on this page that does not match what the product does, that is a defect and we want it: privacy@sortia.io.
Sortia is a Google Sheets add-on for statistics, risk analysis (estimates in, odds out), optimization and decision analysis. It asks for the two permissions above, it computes inside your file, and everything that leaves is on the privacy policy, field by field.