Privacy: every place the bridge talks to, and what each one receives
The bridge runs on your computer, next to Excel. This page lists every host it connects to, what is sent there, and what never leaves the machine. It is written from the code, not from a policy template; each row names the module that makes the call so it can be checked.
What stays on this computer
- The workbook file, and the contents of any cell you marked Private (the
AI sees a typed placeholder such as [num:private] in its place: the kind of
the marked range, never a value; only the address travels). The mark
covers the contents, not only the address: a cell holding the same value as
a private cell, however it got there (a paste, a fill, a sort, a copy to
another sheet), is masked too, and the AI is told it matches a private
value. What is not recognised: a value changed into a different constant by
hand (a rounded figure typed again, a Flash Fill result).
- File paths outside the bridge's own folder. Reports say "bundled" or
"system" for the Python runtime, never a path.
- Your bridge token. The broker keeps a sha256 of it, never the token. Your
email address is a different matter: bridge.ram.sh holds it in clear, because your seat is what it identifies and because it is part of who your bridge is told to allow - see the bridge.ram.sh row below.
- The sign-in your AI client uses is moving. The Python bridge - what you run
today - runs that sign-in here, on this computer, and can do so on your own address as well as through bridge.ram.sh. The C# bridge that replaces it will use a sign-in that bridge.ram.sh runs instead, so bridge.ram.sh will hold the record of the permission you gave: see the bridge.ram.sh row for what that record will hold and how long it will live. Your Google password is typed at Google and is not sent to bridge.ram.sh either way. Your workbook file is not sent there either way. What does pass through bridge.ram.sh, today and afterwards, is each tool call and its result, forwarded between your AI client and your bridge - none of it is stored there.
- The write journal, the audit log and the local state files (see the last
section). They are read by the bridge and by the ribbon, and by nothing else.
Hosts
| Host | When | What it receives | What it keeps | Code |
|---|---|---|---|---|
| Your AI client's vendor (for example claude.ai) | every tool call while a chat is connected | the tool call and its result: workbook and sheet names, cell addresses, the values of cells you did not mark private, formulas, and the plain-language change line after a write | whatever the vendor's own terms say; the bridge does not talk to the vendor itself, the client does | src/bridge.py (tool results), src/guard.py (masking) |
| bridge.ram.sh (the hosted broker) | on start; while connected; on a heartbeat every few seconds; on a click of Update available; when you connect your AI client and sign in with Google; and, once the sign-in for AI clients moves here, each time that client renews its permission | register: your invite code, the bridge's token and the bridge version (a withdrawn version is refused and told to update). link: the same tool calls and results as above, forwarded as frames between the client and your bridge. installations: invite, bridge version, channel, last-seen, OS, Excel version, daily tool-call counts, per-action outcome and timing counts (how many calls ended ok, refused, busy, timed out or in error, and two duration bucket bounds - counts only, never what was read or written), and uninstalled when you uninstall. seat: when you open the bridge's Account page, and at most every thirty seconds, the bridge's token; what comes back is your seat's address (not while the seat is suspended or revoked), plan and state, and whether the last sign-in to this bridge was allowed or refused and when - never the address that was refused. download: the invite code in the URL, to fetch latest.json and latest.zip. auth, today: the sign-in hand-off for the connector, and more of the sign-in than that phrase suggests. Your bridge asks bridge.ram.sh for the fleet's Google client id and secret and for the addresses your seat allows; a bridge whose sign-in has moved to bridge.ram.sh is no longer given the secret. While you are at Google, bridge.ram.sh holds the one-time state value against your bridge id, and when Google sends your browser back it is bridge.ram.sh that receives Google's authorization code and passes it, and the rest of that query, to your bridge. Everything your bridge's own sign-in layer exchanges with your AI client - the client registering itself, the authorize step, the code, the renewal - travels through bridge.ram.sh to reach your bridge, because that is the road. And when a registration has to be confirmed, bridge.ram.sh completes a Google sign-in itself and receives your verified address directly. auth, once the sign-in moves here: bridge.ram.sh will run that sign-in rather than carry it, which means it will receive - your AI client's registration (the name it calls itself and the addresses it may be sent back to); the address of the bridge you are connecting, as resource; the one-way challenge value your client generates, so that only your client can finish the sign-in it started; the one-time values that tie one step to the next; your client's own secret, presented when it collects its pass; and, from Google, the claims for the scopes bridge.ram.sh asks for - the design asks for two, openid and email, which returns your verified address and the standard claims that carry it, and no name and no picture. Your Google password is typed at Google and is not part of any of it | the registration (id, sha256 of the token, invite), the installation record, the release pointer; the contact address of a firm, if your seat belongs to one, also in clear; and your seat: your email address and name in clear, your plan and its billing state, with the address also used as a lookup key so a seat can be found by it. That record does not expire, and revoking a seat does not remove it - it marks it revoked; removing it is something we do by hand, so ask us if you want it gone. It forwards link frames and does not store them. We have per-request logging switched on for bridge.ram.sh: every request leaves a record, kept three days, so that an outage can be diagnosed after the fact and abuse of an endpoint that must be open to the internet can be seen. Those records keep your IP address, your country, your browser or client's User-Agent and the referer - the site you came from, or one of bridge.ram.sh's own pages with the short-lived transaction id of a sign-in in progress - along with the status, the timing and the path of each request, that path kept with its ids masked, and the public parts of a sign-in request - the public half of its challenge value, which app is asking, where it is to be sent back and what it is asking for - none of which is a secret. Sign-in codes, the one-time values that go with them, cookies and the tokens in the authorization and bridge headers are masked there - Cloudflare's behaviour as measured on 2026-09-19, not a promise we can make for it. What our own code writes alongside them is the method, the path with codes and nonces stripped out, the status and the duration - never a body, a token or an Authorization header. Every change we make to your seat from the back office is written to an audit of its own and kept with no expiry: it names the seat it concerns, and the free text an operator or our payment provider writes alongside it can mention an address or a name - as can the notes we keep on your seat. Removing an address from those is not something the code does today; it is what issue #1263 is for. While a registration is waiting to be confirmed, and for thirty minutes at most, it also holds the address that confirmed it and the city and country Cloudflare reports for that request. Once the sign-in for AI clients moves here, bridge.ram.sh will also keep: a record of your AI client, with its secret stored as a one-way hash it cannot read back; a one-way hash of the renewal key in force, which is what will let a retired one be spotted and refused; the half-finished sign-in, discarded within fifteen minutes whether it completes or not, and the authorization code inside it for seconds; and one audit line per sign-in - which bridge, when, and allowed or refused, and on a refusal a short one-way fingerprint of the seat code it was checked against - holding your address as a keyed one-way value rather than in the clear, and beside it, per bridge, the verdict on the last sign-in alone - allowed or refused, and when, with no address at all - which is what your Account page shows; and one audit line per renewal - which bridge, when, allowed or refused, the same keyed one-way value of your address and a short one-way fingerprint of the renewal chain, never the renewal key - so that we can see which AI clients are still using a bridge and end one of them on its own. The plan is to keep the renewal-key hash for 30 days after its last use and the audit lines for 90 days, to sweep a client that registers and never finishes a sign-in after 24 hours and one that finished but went unused after 90 days, and to count registration attempts per address so a flood can be refused. None of that last group exists yet | src/broker_client.py, src/install_report.py, src/update.py, broker/src/index.ts |
| staff.ram.sh (the support relay) | when a report is filed from the ribbon or by the AI; when My reports is opened; when a report's status is checked | the report: title, the description the AI wrote back to you, the technical context (privacy_report lists every field: versions, runtime, channel, an 8-character invite prefix, client name, the self-reported model, locale, default browser, Excel bitness, add-in build), the recent audit tail (tool names, addresses, counts, never contents), and your email only if you typed one into the report | the report becomes an issue in the product's private repository, read by the support staff; your email is stored with the report to send you its status by mail and is never written into the issue | src/support.py, src/report_page.py |
| support@ram.sh (mail, sent by the relay) | after a report with an email: received, fix merged, fixed-in-release; and a confirmation when you reply STOP | your email address and the report's title and number | the mailbox's Sent Items; a STOP reply sets a per-address flag so no further status mail is sent | staff relay, tools/reporter-stop-poll.ps1 on the staff side |
| support@ram.sh (the password mail) | only when you press Email me the password on the password page and confirm the address - never on lock, never on a crash | the address you confirmed, the workbook's file name and its password, in clear text, in transit | nothing: the mail is not kept in Sent Items and the relay logs the address's domain and the status only; whoever can read that mailbox can open the file | src/bridge.py (mail_password), src/support.py, staff relay /v1/password-mail |
| accounts.google.com | when you connect the AI client to the bridge, and again whenever the sign-in is asked for afresh | the sign-in itself. Google is not told which workbook is open and is sent nothing from it. What comes back is your verified email address; the design for the broker-run sign-in asks Google for two scopes only, openid and email, so no name and no picture | your verified email address is what identifies you to this bridge. Today the Python bridge on this computer receives it. Once the sign-in moves to bridge.ram.sh, bridge.ram.sh will receive it and put it inside the pass it hands your AI client, a pass that lasts fifteen minutes, and your bridge will read it off that pass. On a C# bridge linked to bridge.ram.sh, bridge.ram.sh alone decides who may use it: it hands a pass only to your seat's own address, a valid pass lets its holder in, and no list on this computer is consulted for it - the lists below only name addresses in the bridge's startup line. On the Python bridge, who may use this bridge is decided here, on this computer, by one list rather than several added together. In the hosted path that list is your seat's own address, read from the small file naming your seat. If that file is there but cannot be read - locked for longer than half a second, damaged, or not the file it should be - or if it has gone from a bridge that registered with it, then that is the answer: nobody is let in until that file can be read again. The list bridge.ram.sh serves (your seat's address, and any address we allow on every bridge) is used in two cases only: the file reads cleanly but names no address, or there is no such file at all and this bridge registered from a code we gave you rather than from the file. A file that is there and cannot be read is nobody whichever way your bridge was set up. When none of those decides it, the decision falls to the lists on this computer: an EXCEL_BRIDGE_ALLOWED setting and an entry in our own password vault, both of them ours to set rather than yours, and the vault one reads as "nothing" on any machine that cannot reach it, which yours cannot; and then your own allowed.json. A bridge whose list comes out empty lets nobody in. Your bridge writes the address into the local audit as the caller's identity | src/connector.py, broker/src/index.ts (/auth/callback), broker/src/confirm.ts, excel/AIBridge.Mcp/Link/GoogleVerifier.cs |
| docs.excel.ram.sh (the docs site) | when you read the docs or ask for access | the request-access form: name, email, company, what you want to do; it becomes a lead on the relay | a lead issue for the sales staff | tools/build_docs_site.py, staff relay |
There is no other host. The bridge does not call GitHub, a telemetry service, or an analytics endpoint. (The project's own test laptop downloads releases from GitHub with a token; customer installs do not.)
A bridge can also be reached at an address of your own instead of through bridge.ram.sh: the no-broker mode. bridge.ram.sh is then not on the path of your tool calls. On the C# bridge the no-broker mode serves the static connector token only: no sign-in runs on it, so it is not a way to connect claude.ai. (On the Python bridge that mode still runs a Google sign-in of its own.)
The files the bridge keeps under %LOCALAPPDATA%\ExcelAIBridge
| File or folder | Holds | Read by |
|---|---|---|
| audit\<workbook-key>.jsonl | one line per tool call: caller identity, tool, sheet, range, how many cells were masked; never contents. A call with no one workbook to file it under - nothing connected, a connect the AI asked for that was refused with nothing connected, or several workbooks connected and the call naming none of them, or one that is not connected - gets its line in a file of its own in the same folder, which names the workbook none; for a refused connect the line carries the path the AI asked for, and a refusal's reason can name the workbooks that are connected | privacy_report, collect_bug_evidence (the last few lines go into a report); the no-workbook file is read by neither |
| tool-counts.json | today's counts: calls per tool, and per action how many ended in each outcome and in each duration bucket; names, outcome words and numbers only | the hourly installation report |
| journals\<workbook-key>.jsonl | the prior contents of cells the AI wrote, so undo_last_write can restore them; the newest 200 records per workbook | undo_last_write only; never sent anywhere |
| keys\ | the token, and in seat.json the license server's last answer about your license (its plan, state, email address and billing customer id, and the verdict on the last sign-in to this bridge: allowed or refused, and when - never the address that tried), DPAPI-protected to your Windows account | the bridge |
| seat-status.json | your license's email address, its plan and its state, and whether the plan includes the side panel and private marking, in plain text, so the Account button can show them and the ribbon can grey what your plan leaves out even while the bridge is not running | the ribbon's Account button and its Task pane and Mark private buttons; nothing sends it anywhere |
| bind.json, connected.json, health.json, activity.json, update.json, addin.json, self-check.json, recommended.json | the handshake between the ribbon and the bridge: which workbook is bound, whether a client is connected, the last action, whether an update exists, the add-in's build and Excel's bitness, the dials | the ribbon and the bridge |
| last-edit.json | the last change the AI made to the connected workbook, or the last request of yours it could not carry out: that workbook's full path, the sheet and the range of the change (none for a refused request), the time, whether it was an edit or a refusal, and for a refusal the plain sentence the card shows. No cell contents and no action names, and a request refused because of a private cell is not recorded at all. One file, overwritten by the next change or refusal; the bridge deletes it, with activity.json, when that workbook is connected again or disconnected | the ribbon's last-action button ("Edited HH:MM" / "Refused HH:MM") and its card; nothing sends it anywhere |
| health-history.jsonl | one line each time the bridge's public link went down or came back: the time, ok or offline, and the reason; the last 30 days, at most 64 KB | whoever diagnoses an outage on this computer; never sent anywhere |
| reports.json, reports-status.txt, reporter-email.txt | your reports from this computer and their status; the email you typed, so the next report can offer it | My reports, the report page |
| %LOCALAPPDATA%\fastmcp\oauth-proxy\ (a second folder, outside the one above) | the sign-in your AI client holds while this bridge runs the sign-in itself: its registration with this bridge, the renewal key it uses, and the record of what has been handed out. Encrypted on disk. No workbook content. Only the Python bridge writes it, so this row is removed when Python retires | the bridge's sign-in layer only |
Uninstalling removes the add-in and tells bridge.ram.sh uninstalled; delete
the folder above, and the fastmcp folder named in the table, to remove the
local records. Your seat record at bridge.ram.sh, including your email address,
is not removed by uninstalling, and is not removed by revoking the seat
either - ask us if you want it removed. Removing the connector inside your
AI client is not something bridge.ram.sh can see.
If bridge.ram.sh were broken into
Worth saying plainly. bridge.ram.sh already carries every message between your AI client and your bridge, so someone who took it over could already read what passes through and send your bridge instructions of their own, by reusing the passes it sees passing through it. That is true today.
It also already holds the email address of every customer, in clear, and can find a seat by it. A copy of that database is a list of our customers' addresses. It is not a list of passwords, and it opens no workbook by itself.
What the sign-in would add is a key. bridge.ram.sh would sign the passes your AI client uses, and whoever holds that signing key can make a pass for any customer - including one naming an address your bridge allows, because the address inside a pass is written by whoever signs it. So, without softening it: if bridge.ram.sh itself is taken over, whoever holds it can reach any workbook that is open and bound until the key is rotated. Rotating the key is not a cure while the intruder is still inside - they would read the new one, and they can reuse the passes they see. Once they are out, rotating the key cancels every pass they made.
What is reached, and what is not. bridge.ram.sh has never held your workbook file and still does not: the bridge reads the open workbook live, and the file on disk is not on its path. What a taken-over bridge.ram.sh could reach is a workbook that is open and bound at that moment, through the bridge running on your computer. A workbook that is closed is not reachable from there.
What you can do, any time:
- Sign out on the AI Bridge tab. Your bridge stops accepting the tokens it
issued for your address, and on today's Python bridge it also refuses to renew them, so your AI client is asked to sign in again. Both refusals are held in the running bridge's memory, and they do not last the same length of time. The refusal to renew ends at the next completed sign-in, or when the bridge stops - whichever comes first. The tokens already handed out stay refused until the bridge stops: signing in again does not revive them, it hands out new ones.
- Close the workbook, or stop the bridge. Nothing can be read while it is
not running.
- Ask us to revoke the seat. That disconnects every bridge on it today, and
under the new sign-in it will also make every pass for that seat fail at the next call.