What a Google Sheets add-on can see

Written 6 September 2026. The permission list and the payload below were read out of Sortia's own appsscript.json and its assist code on that 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 withWhat it grants
spreadsheets.currentonlyThe one spreadsheet the add-on is open in. Not your other spreadsheets, and nothing else in the account.
spreadsheets.readonlyRead every spreadsheet in the account.
spreadsheetsRead and write every spreadsheet in the account.
drive.fileOnly the files this app creates, or that you specifically open with it.
drive.readonlyRead every file in the Drive, of any type.
driveRead, write and delete every file in the Drive.
script.container.uiDraw a sidebar or a dialog inside the document you are in.
script.external_requestThe add-on's own server code may call any outside address.
script.scriptappRun on a trigger, when you are not present.
userinfo.emailThe 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:

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. Five addresses, all of them under https://www.sortia.io/api: /gateEvent for anonymous usage counts, /latestKey to collect a renewed license key, /explain for a written reading, /aiPolicy to ask whether an organization has that feature switched on, and /claimDomain.

The fifth 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, and it sends the key together with the work address typed on the activation screen, 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. It is on the privacy policy under license checks.

One more thing crosses the network and it is not one of the five, so it belongs 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. That is a stylesheet and a few font files. Nothing of ours and nothing of your sheet's goes with it, and the network panel files it under CSS and Font rather than Fetch.

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.

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:

CellLabelValue or distribution
B2Average headcountTriangular, 84 low, 92 likely, 105 high
B3Average salaryNormal, mean 118,400, standard deviation 9,600
B4Benefits loadUniform, 0.22 to 0.31
B5Bonus pool150,000
B7Total people cost=B2*B3*(1+B4)+B5

Ten thousand trials, 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, butIn the browserAt the server
unalteredsentaccepted
with label: "Total people cost" addedrefused, fieldrefused, field
with the output's ref replaced by the row labelrefused, refrefused, ref
with the real median added as p50: 14135866refused, output-fieldrefused, output-field
with a driver carrying its labelrefused, driver-fieldrefused, driver-field
with sheet: "People" addedrefused, fieldrefused, 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.

Five questions to ask on any consent screen

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.

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.

Estimates in, odds out.

Home· Start here· Templates· Pricing· Teams· Tell someone· Privacy· Terms· Developers· Support· Changelog· Validation· © 2026 Sortia · Made for Google Sheets™. Google Sheets™ and Google Workspace™ are trademarks of Google LLC.
Sortia is not affiliated with or endorsed by Google.