The Excel add-in
Download the bundle from https://bridge.ram.sh/download/<your invite>/latest.zip (the invite is the code you got with your access), unzip it and run install.cmd. It puts the window add-in into %APPDATA%\Microsoft\Excel\XLSTART, so it loads in every Excel session; uninstall.cmd removes it again.
Bind this workbook points the bridge at the active workbook. It works whether or not a bridge is running — the next one to start picks it up.
Private marks or unmarks the current selection. One press always leaves the selection wholly private; a second press leaves it wholly public.
Show private selects every private range on the sheet. Shade private shades them, per workbook, off by default.
Private ranges are stored as workbook AI_PRIVATE_* Names, so they travel with
the file and survive with no bridge running. Each name is derived only from
the sheet and the marked range's own address, so inserting, moving, or
renaming a sheet, or marking and unmarking other ranges, never changes an
existing private range's name or what it points at. The names are hidden, so
they don't appear in the Name Box or the Paste Name dialog while you're
building a formula.
The guard follows direct references across sheets: with Sheet1!C1 private,
Sheet2!A1 = Sheet1!C1 and Sheet3!B2 = Sheet2!A1*2 are masked too, and a
SUM over a range that contains C1 is masked with them. Marking still lists
those cells and offers to mark them outright, so they stay covered if a formula
is later pasted as a value.
Known limit: the guard reads formula text, so it does not follow INDIRECT,
OFFSET arithmetic, references through a defined name, or values already
copied elsewhere as constants.
What hidden and locked mean to the AI
Excel's own hiding and protection count as privacy signals, not just layout:
- Hidden rows and columns (also collapsed outlines and filtered-out rows) are
private to the AI, exactly like marked cells, until you press Hidden private in the ribbon's Private group, which then reads Hidden readable (private by default, saved with the workbook). The AI may say a hidden row or column exists; it never unhides one and never explains how.
- Hidden and very-hidden sheets are never listed, read or counted. A hidden
sheet is as absent as a missing one, and a visible formula that reads it is masked.
- Protected sheets stay readable, and every reply says the sheet is protected.
Locked cells are never changed, sorted, filtered or formatted; unlocked cells can be written, without touching their formatting. A formula hidden by protection is never shown as text. The bridge never unprotects a sheet, never asks for a password and never uses one typed into the chat.
- Pivot tables built over private cells are private as a whole, like a formula
over them.
- Saving as CSV or another format without defined names drops the private marks
from that file; the ribbon warns once per workbook when that happens.
What the bridge reports
On start and on a heartbeat the bridge sends installation metadata to the
hosted broker so staff can see which invite is running: invite, bridge version,
channel, last-seen, OS, Excel version, daily tool-call counts, and per-action
outcome and timing counts (how many calls ended ok, refused, busy, timed out or
in error, and two duration bucket bounds). It never sends workbook content, cell
values, addresses, sheet names, or file names. privacy_report lists
exactly those fields. Uninstall reports uninstalled. Every host the bridge
talks to, and what each receives, is listed in PRIVACY.md;
what the bridge protects, what it does not, and how to report a security
problem is in SECURITY.md.
What every read tells you
Every read_range, analyze_sheet and get_active_selection reply carries:
masked— the addresses whose values were replaced by[redacted], and
masking_note — one sentence saying so. Addresses only, never contents.
suspicious_text— cells whose text looks like instructions, links or
commands (instruction-like, url-or-host, command-like). They are
returned as data and flagged; nothing is hidden or blocked.
Every tool call is appended to
%LOCALAPPDATA%\ExcelAIBridge\audit\<workbook-key>.jsonl with the caller's
identity (the Google account that signed in, token:<id> for the bearer
token, or local), the sheet and range, and how many cells were masked.
privacy_report() returns the current private addresses and the recent audit.
When something goes wrong the AI writes the bug report, not you: it works out what happened, says it back to you in plain words, asks whether that is right, and then files the whole technical ticket itself — you never see a number or a link. AI Bridge → My reports shows what you have reported from this computer and where each one stands; see docs/report-a-bug.md.
write_cells refuses to replace a non-empty cell unless overwrite=true, and
names what is there. Private cells are refused before that check and are
never quoted.
Understanding a model
trace(address, sheet, depth)— what feeds a cell (from formula text, across
sheets) and what it feeds (same sheet). Private cells appear by address with
[redacted] values.
analyze_workbook()— every sheet's map, which sheets reference which, and
how many private cells each holds.
find_errors()— every#REF!,#DIV/0!, … with its direct precedents.format_cells([...])— number format, bold, fill, font colour, italic,
underline, font size, borders (edges, weight, colour), horizontal and
vertical alignment, wrap text (no merge); journalled; undo_last_write
restores every attribute it changed. write_cells keeps a cell's own number
format unless you pass number_format.
get_instructions()— your standing instructions for this workbook, read
from %LOCALAPPDATA%\ExcelAIBridge\instructions\<workbook-key>.md. Edit the
file by hand; no tool writes it.
Native operations
sort_range, apply_filter / clear_filter, add_conditional_format,
add_validation_list, copy_range, move_range. Each refuses a range that
contains a private cell; a copy or move refuses when its source or its
destination touches one. Sorts, conditional formats, validations, copies and
moves are journalled and reversed by undo_last_write (a move is put back on
both sides); a filter changes no cell and is removed by clear_filter.
copy_range and move_range use Excel's own Copy / Cut, so relative references
adjust as they do by hand, and a non-empty destination is refused unless the
customer chose to replace it (overwrite=true).
rename_sheet, freeze_panes, autofit_columns and set_column_width change
the sheet's name or layout, never a cell; each is journalled and reversed by
undo_last_write, and each refuses a protected sheet or a private cell (a width
change refuses the whole column when any cell in it is private).
delete_sheet needs the customer's own Yes on the question page to a question
that names that sheet and says the word sheet (a "Yes, delete it" button), the
way replacing a non-empty cell does; a question giving the name alone deletes
nothing, because a name is no evidence of a kind; without it the call is refused
and nothing is deleted, and each Yes allows one delete of the sheet it named. It
refuses a sheet holding a private cell first, and cannot be undone.
add_hyperlink puts a clickable link on one cell and remove_hyperlink takes it
off, leaving the cell's text. The cell's own text is kept unless you pass text,
so a link to the source document can be dropped onto a figure; passing text for
a cell that already holds something is a replacement and needs the customer's yes
(overwrite=true), as write_cells does. A private cell is refused, and so is a
link whose url or display text carries a private value - the refusal says only
that, and repeats no value. Both are journalled with the cell's prior text and
prior link, and undo_last_write puts both back.
select_range moves Excel's selection to a range and scrolls it into view so
the customer sees what the assistant means; it reads and changes nothing.
find_cells answers "where is the 2024 budget line?" with addresses and masked
values instead of a whole sheet: look_in is values or formulas (the
parameter is look_in because in is a Python keyword), match is contains
or exact, both ignoring case, and without a sheet every visible sheet is
searched. It changes nothing, and a private cell -- or a cell holding a value
that matches a private one -- is dropped before the comparison, so a query whose
only matches are private answers with an empty list and no hint that anything
was withheld: what is found reveals what is hidden. find_replace replaces text
inside one bounded range; it refuses the whole batch when the range holds a
private cell or such a twin, and refuses a replacement that is itself a private
value (a twin written in the open). Cells holding a formula are left alone and
named in skipped. Each replaced cell is journalled before it changes, so
undo_last_write puts them back one at a time.
insert_rows / insert_columns, delete_rows / delete_columns and
clear_range (contents, formats or all) change the sheet's structure. A delete
refuses when a deleted cell is private or feeds a private cell, an insert when
it would split a private block, a clear when the range holds a private cell.
Each is journalled before the change: undo of a delete puts the rows or columns
back with their values, formulas and formats, undo of an insert removes them,
undo of a clear restores what was cleared.
add_comment puts a note on one cell and delete_comment takes it off; both
are journalled before the change, so undo_last_write puts the previous note
back or removes the one that was added, and both refuse a private cell plainly.
list_comments reads a sheet's notes: a note on a private cell keeps its
address and loses its text ({"address": "C3", "text": null, "private": true}),
the same shape a plain note has, so the reply never says more about a private
cell than where it is.
define_name, list_names and delete_name are the workbook's defined names,
so a formula can read =SUM(Sales) rather than =SUM(B2:B40). A name is
journalled before it is added or removed: undo of a define_name deletes the
name, undo of a delete_name defines it again with the reference it had.
define_name refuses a range holding a private cell — a private cell is never
named — and the AI_PRIVATE / CLAUDE_PRIVATE names are the guard's own, so
neither define_name nor delete_name will touch one. list_names never
returns the guard's names or the addresses they cover, and withholds every
customer name whose range holds a private cell, saying only how many were
withheld and why, never which cells.
add_chart draws one bar, column, line, pie or scatter chart over a bounded
range, anchored by default on the cell right of the data. The chart is read
back before it is called added (#2088): one whose series plot no number — a
column of text, or numbers only in hidden rows — is deleted again and the call
refused in plain words that name no cell value. That check is skipped over
private cells, since its answer would tell what they hold: such a chart is drawn
even when it plots nothing, and the customer can delete it. A range holding a
private cell is still drawn — Excel plots it on the customer's screen — but the
AI is never told its values, title or source: the reply says only that the chart
was added and its values are hidden, and the chart is untouchable to the AI (no
delete, no change of source). The chart is journalled by name, so
undo_last_write and the ribbon's Undo AI change still delete exactly that
chart. list_charts names every chart with its kind, sheet and anchor, withholds
the source range of a chart that reads a private cell, and withholds a title
Excel took from a cell (a series name, or a title linked to a cell) whenever the
chart reads a private cell or that cell is private or on a hidden sheet.
A chart of how many rows fall in each category needs no helper table (#2088):
add_pivot with that column as a row and a count of it as the value, then
add_chart over the address and on the sheet its reply gives, draws a pivot
chart; the tool text and the AI's write rules name that route.
delete_chart
removes one chart by name and cannot be undone, so it needs the customer's own
Yes on the question page to a question that names that chart and says the word
chart (a "Yes, delete it" button), the way delete_sheet does; without it the
call is refused and nothing is deleted, and each Yes allows one delete of the
chart it named. A chart that reads a private cell is refused before consent is
even looked at - "chart <name>
was not deleted: it reads a private cell, and private means untouchable;
while adding it is still on the undo list, undoing the AI's own changes back to it
removes it" - and no Yes can spend that away. That undo of the AI's own add is by design
(#2359): add_chart's note on such a chart, list_charts, restyle_chart and the
WRITES instructions say it in the same words.
A chart is styled as it is added and can be restyled later (#2058): add_chart
also draws doughnut, area, column_3d, bar_3d and pie_3d charts, and takes a size
in points (width, height), a style preset (style, Excel's chart style
number), a colour palette (palette, 10 to 26) or a colour per series - per
slice for a pie or doughnut - (colors), data labels on or off, the legend's
place or none, axis titles and gridlines on or off. restyle_chart changes the
kind, title and those same options on a chart already there, a chart over private
cells included: it changes the look only, never the source, and tells the AI
nothing of the chart's values. An option Excel will not apply to that kind of
chart (a pie has no axes) is named back in not_applied. A restyle reads the
chart back after it acts and answers per option: changed, unchanged (already
so) or not_applied with a plain reason, so an option Excel took without a word
and did not show is never said to be applied. A palette clears each series'
own colours first, so it recolours a chart drawn with colors too. A restyle is
journalled as evidence, with only the options that landed, and has no undo of
its own; undoing the chart's add removes it.
Every part of a chart can be designed, on add_chart and restyle_chart alike
(#2117): a font (name, size, bold, italic, colour) for the title, the data
labels, the legend and the axis titles (title_font, label_font,
legend_font, axis_title_font); what the labels show - any of value, percent
and category (label_content) - where they sit (label_position: inside,
outside, best fit or center on a pie) and their number format
(label_number_format); one slice of a pie or doughnut pulled out (explode:
by its position, its category or largest, with a distance in percent, or
none); and the chart and plot area's fill, a colour or none (chart_fill,
plot_fill). Each is read back like the rest. Over private cells every one of
them still applies and no reply tells a value: the bridge picks the largest
slice itself and does not say which or whether there was one (largest reads
back changed there whatever the cells hold), a slice is not picked there by its
category's name (whether one matched would tell that the category is there),
and labels that show values are drawn for the customer and never read back.
add_table turns a bounded range into an Excel table (a ListObject) for filters,
banded rows and structured references, remove_table turns one back into a plain
range, and list_tables names every table with its sheet, header flag and style.
Neither write touches a value. Both refuse a range holding a private cell — a
table's banding and filter dropdowns say where the private block is, and so does
its formatting on the way out — and list_tables withholds the address of such a
table while still naming it. Each is journalled before the change: undo of an
add_table converts the table back to a plain range, undo of a remove_table
re-creates it over the same range with its name, style and header flag.
Pivot tables
list_pivots names every pivot table: its sheet, the address of its report, its
source range, its row, column and value fields (each value with its aggregation),
its filter fields with their current selection, and when it was last refreshed —
names and addresses only, no value is read. read_pivot reads one report by name
through the same guard as every read. A pivot is an aggregation over its source,
so a report whose source overlaps a private cell reads as masked cells with the
standard wording, and list_pivots withholds that pivot's source and address
while still naming it. add_pivot builds one over a range or a table (row, column,
filter and value fields with their aggregation; a numeric column is summed and any
other counted when no aggregation is given) on an existing sheet or on a new sheet
named after the pivot; the source is refused when it holds a private cell, and the
report's ground is checked as a write is. delete_pivot takes the customer's Yes
on the question page to a question that names that pivot and says the words pivot
table, the way delete_chart does. Both are journalled: undo of an add_pivot
removes that pivot
(then its new sheet), undo of a delete_pivot rebuilds it from the layout the
record kept. change_pivot moves fields (add or remove row, column and value fields,
change an aggregation, set a filter) and journals the layout before the change so undo
puts it back; refresh_pivot re-reads the source into the report and cannot be undone,
which its reply says. Both refuse when the source holds a private cell.
Connecting claude.ai
Run venv\Scripts\python.exe src\server.py --broker and paste the connector URL it prints into claude.ai — the hosted-broker route, no tunnel and no DNS; see Route C.
The three tools
claude.ai sees three tools: excel_read (read_range, analyze_sheet, analyze_workbook,
get_active_selection, trace, find_errors, list_private_cells, privacy_report,
select_range, list_comments, list_names, find_cells, list_charts, list_tables),
excel_write (write_cells, create_sheet, format_cells, sort_range, apply_filter,
clear_filter, add_conditional_format, add_validation_list, copy_range, move_range,
undo_last_write, insert_rows, insert_columns, delete_rows, delete_columns, clear_range,
rename_sheet, delete_sheet, freeze_panes, autofit_columns, set_column_width,
add_comment, delete_comment, define_name, delete_name, find_replace, add_hyperlink,
remove_hyperlink, add_chart, delete_chart, restyle_chart, add_table, remove_table, label_rows) and excel_session
(bind_workbook, list_bound_workbooks, get_instructions, report_session, ask_customer,
report_bug, request_feature, collect_bug_evidence). Each call names an action and passes
that action's parameters in params; a wrong action or parameter answers with the exact
list. Reads run without a permission dialog; the write and session tools each ask once.
label_rows answers one closed question per row with a local model on the customer's computer,
writing one of the given labels into the output column and leaving unsure, private and blank rows
empty; until the model is on disk, each session asks the customer before its one-time download of
about 681 MB from models.ram.sh.
Only these three tools are listed. A connector added before 2026-09-08 must "Refresh tools
list" once in claude.ai, since it caches the tool list and the old per-action names are gone.
Using Skills with the bridge
claude.ai applies enabled Skills to any connector. Nothing to install here: enable the Skills you want in claude.ai and they apply to Excel AI Bridge conversations. Skills that suit a workbook: financial-model review, data cleaning, chart specification.
If a crash leaves a workbook encrypted
While a workbook is bound the bridge encrypts it on disk, so a copy of the file,
or a program that opens the .xlsx without the password, gets nothing readable.
It is not a guard against a program running as you, which can read the password
from the bridge's keystore first (see SECURITY.md). An Excel 97-2003 .xls is
the exception: it is left unencrypted, because a password on one makes Excel ask
on every save whether to convert it to the newer format. The password
comes off again when the bridge exits — but a hard kill, a crash or a power cut
never gets to run that step, and the file is left as an .xlsx Excel will not
open.
The next start puts it right by itself: before it binds anything, the bridge unlocks every workbook it holds a key for and finds still encrypted. Nothing is asked of you. That startup step is capped at two minutes in total, however many workbooks are waiting — anything it does not reach is left for the next start and named on screen, so recovery can never be the reason the bridge does not come up.
When the bridge will not start at all, run the same thing on its own, from the
bridge's folder (%LOCALAPPDATA%\ExcelAIBridge\releases\<version>):
bridgehost.exe --recover # fix
bridgehost.exe --recover --dry-run # report only
Excel AI Bridge recovery
C:\books\Model.xlsx encrypted, key on file -> unlocked
C:\books\Secret.xlsx recorded protected before the bind -> left alone
C:\books\Open.xlsx open in Excel -> left alone, in use
C:\books\Old.xlsx file missing -> stale key kept
--dry-run asks Excel which workbooks are open before it says what it
would do, so it takes a moment longer than you might expect — and it says
"would unlock" only about a workbook it really would.
It only ever touches a workbook it minted the password for. A file you
protected yourself and never bound has no key here, so it is not merely
skipped — recovery cannot see it, and it is left exactly as it is. The
Secret.xlsx row above is the other case: a workbook the bridge DID bind,
whose key blob records that the file was already protected when it did, so it
is listed and left alone rather than unlocked. Nor will it touch one another
bridge has bound, one that is open in Excel, or a read-only file — and if it
cannot tell (Excel will not answer, or the file that records which workbooks
are bound is unreadable) it refuses rather than guesses, and says so.
The limitation, stated plainly. The passwords live in
%LOCALAPPDATA%\ExcelAIBridge\keys\, encrypted with Windows DPAPI, which is
bound to this Windows account on this machine. Copying those files to
another computer, or restoring them after the profile is deleted or the OS is
reinstalled, will not decrypt them — and a workbook stranded encrypted at that
moment cannot be opened again. If you need a copy of a password somewhere else,
take it out deliberately:
bridgehost.exe --recover --show C:\books\Model.xlsx
That is the only command that prints a password.
bridgehost.exe --recover --forget <path> drops a key you no longer want; nothing deletes one on its own, because it is the only copy.