Back to Blog
Technical

Excel Shared Strings Table: The Hidden Vocabulary Inside xl/sharedStrings.xml That Preserves Deleted Cell Contents and Reveals Editing History

Every workbook with text values in it carries a single deduplicated vocabulary at xl/sharedStrings.xml — the shared string table, or SST. Worksheet cells do not store their own text; they store integer indexes into this table. The mechanism is an elegant compression scheme that also turns out to be a near-perfect forensic record: deleting a cell removes its index from the worksheet but almost never removes its string from the table, the order of entries preserves the chronological sequence in which values were first typed, rich-text runs preserve the formatting history of every text edit, and Document Inspector does not touch the layer at all. This post walks through how the SST is laid out, why orphan entries survive every normal save, how to read the table without Excel, what an investigator reconstructs from it, and how to actually rewrite the table so it carries only the values currently in use.

Technical Team
May 13, 2026
22 min read

Why Excel Has a Shared String Table at All

A workbook with ten thousand rows of customer data typically reuses the same country names, product codes, status labels, and category strings over and over. If every worksheet cell stored its own copy of "United Kingdom", the XLSX would balloon. The Office Open XML specification solves this with a single shared string table inside the package: every distinct text value appears once at xl/sharedStrings.xml, and worksheet cells reference it by zero-based index.

// xl/worksheets/sheet1.xml — cell referring to the SST

<c r="B7" t="s">

<v>42</v> // index into the shared string table

</c>

The t="s" attribute marks the cell as a shared-string reference. The value 42 is not a number to display; it is the zero-based offset into the table. The cell’s actual text lives at /sst/si[43] inside sharedStrings.xml. This indirection is the heart of the format and the reason the table behaves the way it does.

// xl/sharedStrings.xml — header

<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"

count="17284" uniqueCount="612">

<si><t>United Kingdom</t></si>

<si><t>Germany</t></si>

<si><t>France</t></si>

...

</sst>

The count attribute is the total number of shared-string references made by the worksheets. uniqueCount is the number of <si> entries in the table. The two numbers tell a reader at a glance how heavily the workbook reuses text: a workbook with count=17284 and uniqueCount=612 is dominated by a small vocabulary repeated many times. A workbook with count=900 and uniqueCount=900 is one where every text value is unique — a strong signal that the workbook contains free-text data such as comments, descriptions, or names rather than categorical data.

The Index Order Is the Chronological Insertion Order

Excel does not sort the SST. New entries are appended at the end as they appear during workbook construction. This is the first forensic property of the table: the index sequence preserves the order in which distinct text values were first typed into the workbook.

A workbook whose first hundred SST entries are sheet headers (“Date”, “Account”, “Amount”, “Memo”), followed by entries that look like draft column titles (“TEMP_lookup”, “FIXME_check_rates”, “Old_GL_code”), followed by the production data, tells a reader the author scaffolded headers first, prototyped lookups next, and only then pasted real values. A workbook whose first entries are city names, then a long block of personal names, then a sparse tail of one-off comments, suggests the workbook started as a directory and accumulated annotations over time.

Why The Order Is Not Just Worksheet Order

Excel writes the SST in the order strings were first encountered during whichever editing session produced the workbook’s current state — not in worksheet or row order. Copying a block of cells from another workbook injects those strings into the table at the point of the paste, not at the cell position. The result is a layered insertion-order record that often shows imported data clustered together, with later free-text edits and renames trailing at the end of the table.

Deleted Cells Leave Their Strings Behind: The Orphan Problem

When a user deletes a cell or clears its content, Excel removes the cell’s reference from sheet1.xml. It does not remove the corresponding entry from sharedStrings.xml. The string remains in the table, no longer referenced by any worksheet, and survives every subsequent save until the workbook is rewritten with a different tool or saved through Excel’s “Save As” in some configurations.

These leftover entries are called orphan strings. They are trivially detected: enumerate every shared-string reference in every worksheet, build the set of used indexes, and subtract from the full range [0, uniqueCount). Whatever remains was once on a worksheet and is no longer.

# Python: enumerate orphan strings in a workbook

import zipfile, re

from xml.etree import ElementTree as ET

NS = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"

with zipfile.ZipFile("workbook.xlsx") as z:

sst = ET.fromstring(z.read("xl/sharedStrings.xml"))

strings = [si.find(NS + "t").text or ""

for si in sst.iter(NS + "si")]

used = set()

for name in z.namelist():

if re.match(r"xl/worksheets/sheet\d+\.xml", name):

sheet = ET.fromstring(z.read(name))

for c in sheet.iter(NS + "c"):

if c.get("t") == "s":

v = c.find(NS + "v")

if v is not None: used.add(int(v.text))

orphans = [(i, strings[i]) for i in range(len(strings)) if i not in used]

for idx, s in orphans:

print(f"[{idx}] {s!r}")

The output is a list of strings that were once on a worksheet and were subsequently deleted. The index also tells you roughly when in the workbook’s history the string was first introduced relative to its still-living neighbours.

What Orphan Strings Typically Reveal

In real-world workbooks, orphan strings routinely include items the author never intended to ship:

  • Personal names of employees who were once on a roster but were removed before the file was shared.
  • Old project codenames replaced by sanitised versions in the visible cells.
  • Internal account numbers, GL codes, or test customer records left over from a template.
  • Comment-like strings (“ask Bob about this”, “double-check”, “TODO”) deleted before sharing.
  • Earlier wording of cell labels that was edited but not removed from the SST.
  • Strings from a hidden sheet that was deleted entirely.

Rich Text Runs: The Formatting History of a Cell

Cells that combine multiple formatting runs — bold for one word, italic for another, a different colour or font for a third — are stored as a sequence of <r> elements inside an <si> entry, each with its own <rPr> (run properties) block. The runs preserve the exact formatting state of every fragment of text the author ever applied.

// xl/sharedStrings.xml — a rich-text shared string

<si>

<r>

<rPr>

<b/>

<sz val="11"/>

<color rgb="FFC00000"/>

<rFont val="Calibri"/>

</rPr>

<t xml:space="preserve">URGENT — </t>

</r>

<r>

<rPr><sz val="11"/><rFont val="Calibri"/></rPr>

<t>Submit by EOD Friday</t>

</r>

</si>

A reader sees not only the text but its visual emphasis: the word URGENT in red, the rest in plain Calibri. Strip the styling out of the cell and the runs still describe the original formatting verbatim. For investigators working with redacted workbooks, the rich-text runs frequently reveal what was once highlighted, underlined, or coloured for attention — a forensic signal independent of the visible content.

Strikethrough Survives Even When the Cell Is “Cleaned Up”

A common pattern in collaborative workbooks is to mark removed items with strikethrough rather than actually deleting them. When an author later “cleans up” the cell by removing the strikethrough, the rich-text run that recorded the formatting may persist in the SST as an orphan entry. The same is true for cells that once carried highlighted segments of confidential information that were later visually de-emphasised but not actually removed.

Phonetic Guides and Other Hidden Annotations

The <si> element can carry far more than text and runs. Three less-obvious children are worth knowing.

ElementPurposeWhat it can leak
<rPh>Phonetic guide (Furigana) for East Asian textWhen a Japanese personal name is entered with a phonetic guide, the guide ships separately and persists even if the visible spelling is later anonymised. The guide is often a more reliable identifier than the kanji itself.
<phoneticPr>Phonetic properties (alignment, font)Names the input method used when the cell was typed; some IMEs leave a distinctive fingerprint.
xml:spaceWhitespace preservation flagStrings with leading or trailing whitespace appear with xml:space="preserve". The presence of unusual padding (tabs, trailing spaces) often reveals copy-paste from a specific source system.
_x????_ escapesEncoded control charactersStrings containing tab, newline, or other control characters appear as _x0009_, _x000a_, and so on. The presence of these escapes is a giveaway that data was imported from another tool.

The Alternative: Inline Strings

Not every workbook uses the SST. The OOXML spec defines a second mode in which a cell stores its text directly inline:

// xl/worksheets/sheet1.xml — inline string

<c r="B7" t="inlineStr">

<is><t>United Kingdom</t></is>

</c>

Excel itself almost never writes inline strings. They are common in workbooks generated by third-party libraries (Apache POI, EPPlus, openpyxl in some configurations, every JavaScript XLSX writer in streaming mode) because writing inline strings does not require a second pass to build the table. The forensic implication is the opposite of the SST: inline strings cannot leak orphans, but they reveal the writing tool by their mere presence. A workbook that mixes t="s" and t="inlineStr" cells was almost certainly produced by a pipeline of at least two different tools — for example, an export from a Python service that wrote inline strings, opened and saved once by Excel which migrated some cells into the SST and left the rest inline.

The Mixed-Mode Fingerprint

A workbook with a small SST and a large block of inline strings on a single sheet is almost always the result of an automated tool writing the sheet’s contents and Excel touching only the rest. The boundary between the two regions identifies the cells that were never opened by Excel, which is itself a useful forensic signal: those cells, and any formulas referring to them, were not recalculated by Excel and may carry stale cached values.

Detecting When count and uniqueCount Lie

Both attributes on the <sst> root are optional and not authoritatively validated by Excel. Some tools write them. Some omit them. Some write them stale, with the result that the declared counts no longer match the actual contents of the table.

A mismatched uniqueCount is a near-certain indicator that the workbook was edited by something other than Excel after the last clean save. The most common cause is a third-party library that appended a string to the table without updating the header. The same workbook opened in Excel and re-saved would have a corrected count. Spotting the mismatch is one of the cheapest authenticity checks available:

# Quick count-mismatch check

import zipfile

from xml.etree import ElementTree as ET

with zipfile.ZipFile("workbook.xlsx") as z:

sst = ET.fromstring(z.read("xl/sharedStrings.xml"))

declared = int(sst.get("uniqueCount", "-1"))

actual = len(sst.findall(".//{*}si"))

print(f"declared={declared}, actual={actual}, mismatch={declared != actual}")

What an Investigator Reconstructs from the SST

An analyst with five minutes and a ZIP utility can extract a remarkably complete picture of the workbook’s editing history from sharedStrings.xml alone.

  • The full text vocabulary. Every distinct text value the workbook has ever held, in the order it was first introduced. Search the file for any name, address, account, or codename without ever opening a worksheet.
  • The orphan history. Strings that once appeared in a cell and have been deleted. Frequently personal names, draft labels, internal codenames, or comment-like working notes the author thought were removed.
  • The categorical-versus-free-text ratio. The ratio of count to uniqueCount reveals the workbook’s nature without reading any worksheet.
  • The writing-tool fingerprint. Inline-string usage, escape patterns, run-property defaults, and count-attribute hygiene all narrow down which application wrote the file.
  • The formatting history. Rich-text runs preserve every bold-italic-underlined fragment that has ever existed in the file, even if the visible cell was later normalised.
  • Imported data tells. Strings containing _x000a_ newlines, tab escapes, or unusual whitespace usually came from a database export or another file format rather than direct typing.
  • Phonetic-guide identifiers. For workbooks with East Asian text, the <rPh> entries often persist after the visible spelling is anonymised.

Document Inspector Does Not Touch the SST

File > Info > Check for Issues > Inspect Document has no item that addresses shared strings. The Document Inspector’s “Hidden Worksheets” option deletes hidden sheets but leaves their strings in the SST as orphans. The “Custom XML Data” option does not consider sharedStrings.xml at all. The “Document Properties and Personal Information” option clears docProps/core.xml but never reaches into the SST. Running every inspector option and seeing “No items found” tells you nothing about the orphan strings still present in the table.

Saving the workbook through “Save As > Excel Workbook (*.xlsx)” from a freshly opened Excel session occasionally rewrites the SST and drops orphans, but the behaviour is not documented and not reliable across Office builds. Treat it as a side effect, not a defence.

How to Actually Rewrite the SST

Cleaning the shared string table is a structural operation: every worksheet cell that references the table by index must be rewritten with the new index, and the table must be rebuilt to contain only currently-referenced strings. Four approaches, in increasing order of robustness.

1. Copy every used worksheet into a fresh workbook

Open the workbook in Excel, create a new empty workbook, and copy each sheet into it via right-click on the tab > “Move or Copy” > “To book: (new book)”. Excel writes a fresh SST in the new workbook with only the strings currently in use. The orphans never make the trip. This is the simplest mitigation when there are no formulas with external references that need to survive the move.

2. Round-trip through CSV and back

For data-only sheets, exporting to CSV and re-importing into a new XLSX guarantees a fresh SST. The trade-off is the loss of formulas, formatting, comments, and every metadata layer the workbook carries. See Excel Metadata vs CSV Metadata for the full list of what is dropped.

3. Rewrite the SST programmatically

A short script reads every worksheet, collects every shared-string reference, rebuilds the SST with only the used entries, and rewrites the worksheets with new indexes. The result is a workbook with the same visible content and a clean string table.

import zipfile, re

from xml.etree import ElementTree as ET

from pathlib import Path

NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"

ET.register_namespace("", NS)

NSB = "{" + NS + "}"

src, dst = Path("in.xlsx"), Path("out.xlsx")

with zipfile.ZipFile(src) as z:

sst = ET.fromstring(z.read("xl/sharedStrings.xml"))

items = list(sst.findall(NSB + "si"))

sheets = [n for n in z.namelist() if re.match(r"xl/worksheets/sheet\d+\.xml", n)]

sheet_trees = {n: ET.fromstring(z.read(n)) for n in sheets}

used = set()

for t in sheet_trees.values():

for c in t.iter(NSB + "c"):

if c.get("t") == "s":

v = c.find(NSB + "v")

if v is not None: used.add(int(v.text))

remap = {old: new for new, old in enumerate(sorted(used))}

new_sst = ET.Element(NSB + "sst", {"count": str(len(used)),

"uniqueCount": str(len(used))})

for old in sorted(used): new_sst.append(items[old])

for t in sheet_trees.values():

for c in t.iter(NSB + "c"):

if c.get("t") == "s":

v = c.find(NSB + "v")

if v is not None: v.text = str(remap[int(v.text)])

with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:

for item in z.infolist():

if item.filename == "xl/sharedStrings.xml":

zout.writestr(item, ET.tostring(new_sst, xml_declaration=True, encoding="UTF-8"))

elif item.filename in sheet_trees:

zout.writestr(item, ET.tostring(sheet_trees[item.filename], xml_declaration=True, encoding="UTF-8"))

else:

zout.writestr(item, z.read(item.filename))

Excel reopens the rewritten workbook cleanly. The script preserves <si> children verbatim, so rich-text runs and phonetic guides for surviving entries are kept exactly as they were. If you also want to drop rich-text runs (because they may carry deleted formatting history of their own), iterate each <si> after the remap and replace the children with a single plain <t>.

4. Combine with a full metadata-stripping pipeline

Rewriting the SST in isolation closes one leak but leaves the rest of the workbook’s metadata layers intact. For workbooks crossing the perimeter, the SST cleanup should run alongside parallel sanitisation of defined names, the calculation chain, external links, threaded comments and persona files, printer settings, and custom XML parts and sensitivity labels. A pre-share pipeline that touches all of these in one pass is a far more reliable defence than any single inspector run.

Things That Break If You Rewrite the SST Carelessly

The SST is referenced by index from more places than just worksheet cells. A naive rewrite that updates only sheet1.xml..sheetN.xml can corrupt:

  • Tables. xl/tables/tableN.xml header captions reference the SST when stored as shared strings.
  • Pivot caches. xl/pivotCache/pivotCacheRecordsN.xml uses indexes into a pivot-local cache, but pivot field captions can reference the SST.
  • Comments (legacy). Legacy comment authors are listed separately, but the comment text itself in xl/comments1.xml uses inline strings, not the SST.
  • Data validation lists. Validation prompts and error messages are inline; the dropdown source range references cells, not the SST directly.

Before deploying a remap, search the entire package for the literal sequence t="s" and confirm worksheet cells are the only consumer in your workbooks. If they are not, extend the remap to every consumer.

Pre-Share Checklist

Run this checklist against any workbook leaving your organisation, especially after a long editing history of deletions, renames, and copy-pastes.

  • Have I enumerated every <si> entry in xl/sharedStrings.xml and reviewed the contents of the table directly, not through Excel?
  • Have I computed the set of indexes referenced by worksheets and identified every orphan string by subtraction?
  • Have I read every orphan and confirmed none of them contain personal names, internal codenames, account numbers, or working notes I do not want to disclose?
  • Have I checked the rich-text runs of every surviving <si> for embedded fragments with strikethrough, hidden colour, or other formatting that may carry deleted information?
  • Have I verified the count and uniqueCount attributes match the actual table size, and investigated any mismatch as a possible third-party-tool footprint?
  • Have I scanned for _x????_ escapes that reveal database-export provenance, and for unusual whitespace padding that may give away the source system?
  • Have I checked for <rPh> phonetic-guide entries that may persist after anonymisation of East Asian names?
  • For workbooks shipped externally as final deliverables, have I rewritten the SST to contain only currently-referenced strings, remapped every worksheet cell index, and confirmed the workbook reopens cleanly?
  • Have I coordinated the SST cleanup with sanitisation of the other metadata layers the workbook may carry — defined names, calc chain, external links, threaded comments, printer settings, and custom XML?

Conclusion

The shared string table at xl/sharedStrings.xml is one of the oldest pieces of the OOXML format and one of the most underappreciated metadata layers in any workbook. Its design as a deduplicated, append-only vocabulary makes it efficient for storage and convenient for analysis, and it makes it a persistent record of every distinct text value the workbook has ever held. Deleting a cell never deletes a string. Orphan entries accumulate over the life of the file. Rich-text runs preserve formatting history. Phonetic guides and whitespace escapes leak provenance. Document Inspector does nothing about any of it.

For workbooks staying inside an organisation, the SST is exactly what it is supposed to be: a compact lookup table that keeps file sizes small and editing fast. For workbooks crossing the perimeter, it is the closest thing an XLSX has to a typing-history log — a record of values that were once on a sheet, in roughly the order they were first introduced, with the formatting they were once given. The only durable defence is to rewrite the table as part of a coordinated pre-share pipeline that touches every metadata layer in one pass, leaving recipients with the values the author intended to disclose and none of the vocabulary they thought they had deleted.

Audit Shared Strings in Your Workbooks

Use MetaData Analyzer to enumerate every shared-string entry, identify orphan strings left behind by deleted cells, decode rich-text run formatting, surface count-attribute mismatches that reveal third-party tooling, and confirm the table is clean before your workbooks leave the organisation.