Skip to main content
•9 min read

When an AI Agent Edits a Spreadsheet, What Happens to the Workbook?

Simon Willison noticed something odd about the ChatGPT desktop app: tucked inside a hidden runtime cache (~/.cache/codex-runtimes) sits a full copy of LibreOffice.

I started tracing how agent harnesses handle spreadsheets. The investigation led from Python libraries to recalculation scripts and sandbox workarounds, then to a simpler question about the file itself.

When an agent opens a workbook, saves it, and changes nothing, what actually happens inside the file?

I ran a simple test across five workbooks. On a save with no requested edits, the bundled LibreOffice rewrote 8 to 151 internal package parts. WolfXL changed zero.

Simon Willison's tweet: Just noticed the ChatGPT desktop app bundles a full copy of the LibreOffice open source office suite, tucked away in a hidden folder in the ~/.cache directory
Simon Willison on September 1, 2026 noting LibreOffice bundled in ChatGPT Desktop's ~/.cache directory. Read post →

TL;DR: measured results

8–151

unrelated OOXML parts rewritten by the bundled LibreOffice on a no-op save, across five fixtures

0

unrelated parts changed by WolfXL on the same five no-op round trips

Every number below is backed by a public, SHA-256-pinned receipt. See the receipts

Silent package drift

An .xlsx or .xlsm file is not a flat document. It is an Open Packaging Conventions (OPC) ZIP archive containing dozens of interdependent XML parts: worksheets, shared string tables, styles, themes, drawing relationships, chart definitions, and calculation chains.

A no-op save should be invisible. Nothing was edited, so no part should change.

The test: five workbooks, round-tripped through both engines. Open the file, save it, touch no cell, then diff every package part against the original.1

Three fixtures are Excel-authored files (external links, a pivot chart with slicers, and a PowerPivot model); two come from the openpyxl and excelize test corpora (an aggregation model and a validation/protection/custom-XML workbook).

Across the five no-op round trips, the bundled LibreOffice rewrote 8 to 151 unrelated OOXML parts. WolfXL changed zero:

Unrelated OOXML package parts modified per workbook fixture on a no-op round trip, WolfXL vs bundled LibreOffice
Workbook fixtureWolfXL driftLibreOffice drift
Average by region and quarter08 parts
Validation, protection, and custom XML019 parts
External links014 parts
Pivot chart and slicers031 parts
PowerPivot model0151 parts

Horizontal bar chart: package parts rewritten on a no-op round trip. LibreOffice rewrites 8 to 151 parts across the five fixtures; WolfXL changes zero.

For an agent workflow, every rewritten part is another piece of state to verify. A targeted package diff isolates where the file changed; a broader rewrite leaves more work to establish that the surrounding workbook still behaves as intended.1

Why harnesses reach for LibreOffice

The route exists because each standard Python tool gives up something an agent workflow needs:

  • openpyxl has no calculation engine, so formulas are stored but never evaluated, and the documentation warns that shapes in existing files are lost on a load/save cycle. VBA survives only when the caller passes keep_vba=True.2
  • pandas is built around tabular data. Reading a sheet into a DataFrame does not preserve the workbook's formulas, styles, or package structure for round-trip editing.
  • xlsxwriter is write-only by design; it cannot inspect or edit existing files.

To get recalculation and export anyway, harnesses route spreadsheet work through a generalist engine. Three public examples:

How major AI agent harnesses handle spreadsheet editing today
HarnessSpreadsheet routeWeight
OpenAI CodexBundles a full 423 MB headless LibreOffice build inside ChatGPT Desktop.423 MB
Anthropic CoworkRuns openpyxl for edits, then a headless LibreOffice StarBasic macro (Standard.Module1.RecalculateAndSave) to recalculate.Full LibreOffice
Nous Research / XiaomiCloned the StarBasic script, ran persistent 400 MB daemons, or built custom XML unpack-and-repack scripts (explode.py/assemble.py).~400 MB

The Cowork implementation goes further than most. When sandbox security blocks the UNIX domain sockets LibreOffice needs, it compiles a custom C socket shim (lo_socket_shim.so via gcc) on the fly and injects it with LD_PRELOAD. And Anthropic's own skills/xlsx/SKILL.md warns the model about a silent truncation trap:

"Never use XLOOKUP, SORT, FILTER, UNIQUE... they are spilling array functions and openpyxl has no spill metadata, so only the top-left cell gets a value; recalc.py reports total_errors: 0 on the truncated result."

A silent wrong answer that reports zero errors is the worst failure mode an agent workflow can have.

An in-process alternative

I built WolfXL, a Rust-backed spreadsheet engine for Python, around a strict preservation contract: in-process execution with surgical byte preservation.

The two architectures differ at the process boundary:

Harness subprocess route (today)

agent
→ spawn headless LibreOffice (423 MB)
→ full package re-serialization
→ workbook (8–151 parts rewritten)

In-process route (WolfXL)

agent
→ import wolfxl (in-process)
→ surgical part patch
→ workbook (0 unrelated parts)

For scripts on the common load/edit/save path, the API keeps familiar openpyxl semantics:

-from openpyxl import load_workbook, Workbook
+from wolfxl import load_workbook, Workbook

wb = load_workbook("model.xlsx", modify=True)
ws = wb["Inputs"]
ws["B7"] = 0.045
wb.save("model.updated.xlsx")

For agent pipelines, WolfXL provides a guarded transaction CLI:

wolfxl-ops inspect 'input.xlsx' --request-id 'agent-mutation-001' > 'inspect-receipt.json'
wolfxl-ops plan 'mutation-request.json' > 'plan-receipt.json'
wolfxl-ops apply 'mutation-request.json' > 'mutation-receipt.json'

The apply step writes an isolated temporary workbook, verifies the requested edit, asserts zero unrelated package drift, emits an attestation, and atomically publishes the destination file. If an unsupported operation or verification failure occurs, the process halts before the destination file is touched.

Speed and footprint

I measured WolfXL against the bundled Codex LibreOffice binary on macOS arm64. These selected cold-task medians use three recorded observations per row; the full receipt includes the other scenarios and the warm-path comparison.3

Selected paired median runtimes, WolfXL vs bundled LibreOffice on macOS arm64
TaskInputWolfXLLibreOfficeSpeedup
No-op round trip, cold20,000x8 XLSX0.371 s1.446 s3.90x
CSV extraction, cold20,000x8 XLSX0.421 s1.503 s3.57x
Recalculate and extract, cold10x4 formula XLSX0.293 s0.721 s2.46x

Installation and memory

Installed runtime footprint, WolfXL benchmark environment vs bundled LibreOffice
RuntimeInstalled bytes
WolfXL benchmark environment37,524,645
Bundled LibreOffice423,048,828

Workload peak process-tree RSS favored WolfXL (56-61 MiB vs. 104-124 MiB for LibreOffice). An isolated idle footprint probe favored LibreOffice (34.5 MiB vs. 53.1 MiB).

Intelligent routing, not replacement

Whatever engine produces an edit, verify two things: the intended change landed, and the protected state around it survived. Byte-preserved parts need no separate content diff. Rewritten parts need additional checks, including any dependent formulas or features affected by the edit.

For work outside a preservation engine's scope, LibreOffice remains the capable fallback for general document conversions, presentations, and legacy file formats.4 The sensible architecture is routing:

Recommended routing of spreadsheet work between WolfXL and LibreOffice
NeedPreferred route
Supported cell, style, formula, or structural edit where preservation mattersGuarded WolfXL transaction
Broad office conversion or unsupported legacy formatLibreOffice fallback
Unsupported operation with no approved fallbackFail visibly

(OpenAI also maintains a private engine called Walnut for its own spreadsheet work; it was outside this comparison.)

Reproduce it yourself

Both producers are short scripts with pinned SHA-256 hashes (below). With a Python environment that has wolfxl installed and any LibreOffice binary on disk:

# Preservation drift on your own workbooks
python run_libreoffice_ooxml_preservation_compare.py model.xlsx --output-dir receipts/

# Paired runtime benchmark against your LibreOffice build
python benchmark_libreoffice_vs_wolfxl.py run \
  --soffice "$(command -v soffice)" \
  --output-dir receipts/ --run-id my-comparison

The producers emit the same JSON receipt format this post cites, so every number can be regenerated from source.

Evidence receipts

All benchmark runs, fixtures, and diff tools (5 pinned artifacts)

The measured commit was clean before and after the wheel build, and the wheel was the one used for the published local benchmark.

Try WolfXL

pip install wolfxl

If an agent edit has to preserve charts, pivots, links, macros, or custom XML, run the workflow fit check. The source is public.


Footnotes

  1. Package drift reflects internal ZIP re-serialization rather than corruption. Office export filters can legitimately normalize namespaces, relationship identifiers, calculation metadata, and drawing geometry without altering user-visible data. Observations are bounded to the five test fixtures and one operation, a no-op round trip; the receipt does not measure cell edits. ↩ ↩2
  2. These are documented tool limitations, not measurements in this study. The openpyxl tutorial describes preservation flags and unsupported shapes; its formula documentation explains that formulas are not evaluated. ↩
  3. Observed ratios describe the recorded adapters, builds, fixtures, and modes on macOS arm64 rather than a universal performance ranking. ↩
  4. Scope boundaries: LibreOffice is a complete office suite handling documents, presentations, and general conversions; WolfXL is a Python-facing spreadsheet engine without full openpyxl API parity, and unsupported operations halt visibly in the guarded transaction path. LibreOffice parity does not imply Microsoft Excel correctness. ↩
XLinkedIn

Building something where correctness matters?

Get the next post