How to Use IronXL with Claude Code
A general ledger export with a million journal lines opens in Excel without complaint and then stalls the import job that has to read it. On our test machine, WorkBook.Load needed about 26 seconds and 3 GB of memory to read a 60 MB workbook of that size, and the container in our test scenario caps the .NET heap at 512 MB. Anthropic ships Claude Code for the terminal, for VS Code and JetBrains IDEs, for the desktop, and for the web. In each of those places the agent can read a repository, change files, and execute commands.
IronXL has a second way into an XLSX file. That reader cannot handle several inputs, from formulas that were never calculated to .xls files, and an agent that has not been told about them can fall back on WorkBook.Load or on an overload that quietly buffers the whole file. This guide gives Claude Code four tested prompts. Sonnet handles the short tasks, Opus designs the benchmark in plan mode, and a read-only project subagent reviews the result.
Three of the four prompts start with a /model line, and the model it selects stays active until you pick another. Anthropic's model configuration page describes the sonnet alias as the choice for daily coding tasks and opus as the choice for complex reasoning.
Open a terminal in the repository and start claude. Send the /model line as its own message, then send the rest of the block as the request. Typing @ before a path attaches that file, so the column notes reach the agent before it writes any code. Mention the notes rather than the workbook, because the 60 MB of compressed XML is for the code to read.
/model sonnet
Add the IronXL.Excel NuGet package to LedgerScan.
Then read data/gl-2025.xlsx with it, without building a WorkBook.
The column notes are in @docs/ledger-export.md.
The license key is in the IRONXL_LICENSE_KEY environment variable. Don't print it.
Print every worksheet name first.
After that, make one forward-only pass over the Ledger sheet and print four figures:
- how many data rows the sheet holds
- the Amount total of the Posted rows
- how many seconds the pass took
- the peak working set of this process in megabytes
In our runs Claude Code also passed options that mark the first row as the header, so each cell could be read by column name. A peak in the gigabytes means the code loaded the workbook, and a peak above 600 MB means it handed IronXL a stream or a byte array.
StreamCell.Value holds every number as a boxed Double, so a cast of that value to decimal throws InvalidCastException, and Convert.ToDecimal with the invariant culture is the repair. Every run of ours used Convert from the start.
Minimal Workflow (5 steps)
- Download the C# Excel library from NuGet
- Open the repository in Claude Code and pick a model with
/model - Write the large-workbook rules into
CLAUDE.mdand add a read-only review subagent - Have Claude Code design the reader benchmark in plan mode before it writes a reader
- Run every reader in its own process and look up each member in the API reference
What does Claude Code do in a .NET Excel project?
In an Excel project, Claude Code writes the ledger readers and starts each one as a separate process. You describe the workbooks and review the figures that come back.
How do you set up Claude Code?
Install Claude Code with Anthropic's quickstart and sign in. When it starts and replies, the environment is ready.
How do you set up a project Claude Code can work in?
The tested repository is a .NET 9 console app with two inputs, the ledger workbook and a Markdown file that describes its columns and records the 512 MB heap cap of the production container.
Claude Code reads CLAUDE.md when each session opens. The rules name the streaming guide by URL. IronXL's llms.txt index lists the guide, but its skill.md is pinned to IronXL 2026.8.1, a release before StreamRows shipped in 2026.9.2. Neither that file nor llms-full.txt mentions StreamRows, so an agent that reads only those files learns WorkBook.Load alone. A project subagent is a Markdown file under .claude/agents/ whose frontmatter names it, says when to use it, lists the tools it may call, and picks its model. With IronXL already referenced by the Quickstart, a single prompt now writes both files.
This repo needs a CLAUDE.md at its root, with a section named Large workbooks.
Write it so every later session starts from these rules:
- IronXL.Excel stays on the version LedgerScan.csproj references now.
- Read workbooks under data/ with WorkBook.StreamRows and a file path, never a Stream or byte[].
- Use WorkBook.Load in a test or benchmark that compares it with StreamRows.
- Outside those, use WorkBook.Load only when the file is .xls, has a password or must be edited.
- Any StreamCell can be null on a sparse row.
- Convert StreamCell.Value with Convert. Don't cast it.
- Memory and time figures come from a separate child process, one per reader.
- A long command runs in the foreground until it exits, never as a background task.
- The license key comes from IRONXL_LICENSE_KEY and nowhere else. Don't print it.
- The reference is https://ironsoftware.com/excel/csharp/how-to/stream-large-excel-files/.
Then create .claude/agents/stream-auditor.md, a project subagent that runs on sonnet.
Give it Read, Grep and Glob and no other tools, and a description that says when to use it.
It checks C# changes against that section and against IronXL.xml in the local NuGet cache.
It replies with the file, the line and the rule broken, and it edits nothing.
Claude Code watches .claude/agents/ for new files, but only when that folder existed as the session started. In a repository where this prompt creates the folder, restart with claude --continue before the next prompt so the subagent is available.
Settle the license before the first read. With no key, the streaming and sheet-name calls throw LicensingException the moment they are called, before the file is opened. Check License.IsLicensed at startup. The IronXL License Keys page covers getting and storing a key.
Which IronXL settings belong in CLAUDE.md?
For a CLAUDE.md written by hand, the block below holds the same section.
## Large workbooks
- Keep IronXL.Excel on the version LedgerScan.csproj references now.
- Read workbooks under data/ with WorkBook.StreamRows and a file path, never a Stream or byte[].
- WorkBook.Load is fine in a test or benchmark that compares it with StreamRows.
- Anywhere else, use WorkBook.Load only when the file is .xls, has a password or must be edited.
- Any StreamCell can be null on a sparse row.
- Convert StreamCell.Value with Convert. Don't cast it.
- Take memory and time figures from a separate child process, one per reader.
- Run a long command in the foreground until it exits, never as a background task.
- Read the license key from IRONXL_LICENSE_KEY and nowhere else, and never print it.
- The reference is https://ironsoftware.com/excel/csharp/how-to/stream-large-excel-files/
Why do large Excel files need streaming in C#?
WorkBook.Load builds the complete workbook in memory, with every cell, style, and formula available for random access and editing. That model costs far more than the file. WorkBook.StreamRows reads the worksheet XML in order and keeps one row at a time, which is enough for imports, totals, and validation. The figures below come from our calibration on the fixture workbook, a 60 MB XLSX file with 1,000,000 rows, ten columns, and a distinct description on every row. Each reader ran in its own process, three times, and the table shows the median.
| Reader | Time | First row | Peak working set | 512 MB heap cap |
|---|---|---|---|---|
| StreamRows with a file path | 10.6 s | 1.0 s | 178 MB | Finished at 178 MB |
| StreamRows with a stream | 10.6 s | 1.6 s | 641 MB | Out of memory at 1.5 s |
| StreamRows with a byte array | 10.4 s | 1.5 s | 628 MB | Out of memory at 1.6 s |
| WorkBook.Load with a file path | 26.4 s | 23.7 s | 3,072 MB | Failed within 1.2 s |
All four readers produced the same 60 Posted totals by region and month.
The path reader's figure depends on the text in the file. An XLSX file keeps each distinct string once, in a shared string table, and a table with many entries makes the stream heavier and slower to start. At 500,000 rows, a workbook that repeats 258 strings streamed at a peak of 81 MB with its first row after 0.22 seconds, and a workbook whose descriptions were all distinct needed 130 MB and 0.58 seconds. The million-row fixture carries just over a million distinct strings.
Which StreamRows overload should you use?
Only the overloads that take a file path. StreamRows has nine overloads, one for each pairing of a path, stream, or byte array input with a sheet index, a sheet name, or StreamReadOptions. The path overloads read the package from disk one part at a time. The documentation that ships with the stream overloads explains that they copy the stream into a private buffer before parsing, so that IronXL never closes a stream the caller owns, and calls that copy "not the low-memory path". The streaming guide draws the same line in its overload table.
The XML documentation for the byte-array overloads still describes them as a "low-memory read", but with the 60 MB fixture the stream and byte-array overloads both peaked at more than ten times the size of the file on disk. An upload handler that receives the file as a stream should save it to a temporary file and stream the path.
How do you prompt for a streaming benchmark?
Ask for the whole comparison in one prompt and let Opus plan it. After the /model opus line, cycle the permission mode with Shift+Tab until the status bar shows plan mode on, then send the request. Plan mode lets Claude Code read files and run shell commands, and it edits no source file until you approve the plan.
/model opus
Plan a reader benchmark for data/gl-2025.xlsx under Ledger/, then build it.
I want three readers behind one interface.
Each one returns Posted Amount totals keyed by Region and the calendar month of PostedOn.
- PathStreamReader passes the file path to WorkBook.StreamRows.
- UploadStreamReader opens a FileStream and passes that stream, as an upload handler would.
- FullLoadReader calls WorkBook.Load and indexes cells with GetCellAt.
FullLoadReader reads WorkSheet.RowCount and ColumnCount once each, before its loops.
It never touches PhysicalColumnCount.
Add a bench command to Program.cs that does this:
1. Start this program again as a child process for each reader, with no memory limit.
2. Do the same with DOTNET_GCHeapHardLimit=0x20000000 in the child's environment.
3. Record exit code, wall-clock seconds, seconds until the first data row, and peak working set.
4. Write out/bench.csv with one line per reader and limit.
5. Write out/totals.csv from the path reader.
6. Fail the command if a reader that exited cleanly differs from the path reader on any total.
Run the bench, then ask @agent-stream-auditor to review Ledger/ and Program.cs.
Show me out/bench.csv and what the auditor found.
Each part of the prompt closes a gap that would let the numbers look better than they are.
- Three readers for one file: with the upload-style
FileStreamreader in the set, the benchmark tests the overload choice as well as streaming against loading. - The heap cap in the child's environment:
DOTNET_GCHeapHardLimitsets the most memory the garbage collector may commit, as a hexadecimal byte count, so 0x20000000 is 512 MB. A reader that exceeds it fails in its own process, and the bench records the exit code instead of crashing. - The totals must agree: a reader that is fast and wrong fails the command.
- The subagent closes the turn:
@agent-stream-auditornames the project subagent directly, so Claude Code delegates the review to that subagent.
The prompt spells out how the loading reader walks the sheet, because the full-load baseline has a trap of its own. WorkSheet.RowCount costs time on every read, and on the loaded million-row sheet one call took 33.5 milliseconds. A loop that reads RowCount on every pass pays that cost each time, which works out to about nine hours for this sheet. Reading the count once before the loop brought the whole pass over the loaded cells down to 1.8 seconds. WorkSheet.PhysicalColumnCount costs far more. On the loaded ledger one call took 45 to 50 seconds and lifted the process from 3 GB to 6.7 GB, while ColumnCount returned the same 10 columns in about a tenth of a second. Enumerating WorkSheet.Rows does not avoid the cost. It took 8 seconds for 50,000 loaded rows and 61 seconds for 100,000, and on the million-row sheet it had not reached row 100,000 after 15 minutes.
How do you measure memory use per reader?
Run each reader in its own process, because peak working set is a high-water mark for the whole process and never comes down. In one probe, a single process loaded the workbook and then ran the path reader, and Process.PeakWorkingSet64 reported 3,047 MB for the stream, which is the load's figure. Separate processes kept the two figures apart.
Microsoft's garbage collector settings page notes that .NET applies 75 percent of a container's memory limit by default, so a 512 MB cap matches a container limited to about 683 MB. Under that cap, the stream and byte-array readers stopped with "Out of memory." in all three calibration repeats. The full load failed twice with the same message and once with FileLoadException and "Could not load Excel from given Stream. The stream became unreadable during parsing, which may indicate insufficient memory for the file size."
What are the limits of StreamRows?
StreamRows reads values from an XLSX worksheet in order and does nothing else. The streaming guide lists the boundaries, and the table below records what IronXL 2026.10.1 did with each one in our probes.
| Input | StreamRows | WorkBook.Load |
|---|---|---|
| .xls file | OLE2NotOfficeXmlFileException | Loads |
| .xlsx with a password | OLE2NotOfficeXmlFileException | Loads with WorkBook.Load and a password, else ArgumentNullException |
| CSV file | ZipException "Cannot find central directory" | Loads |
| Formula with a cached result | Cached result, IsFormula true | Formula and value |
| Formula saved without a result | Type Blank, Value null, IsFormula true | Calculates the value |
| Missing path | ZipException "EOF in header" on the first row | FileNotFoundException |
| Missing sheet name | ArgumentException "Sheet 'Ledger' was not found." on the first row | Not applicable |
The exceptions in the first three rows come from libraries embedded in IronXL under namespace names that contain a generated identifier, so no catch clause can name them. Catch Exception around the first enumeration, or check the file before streaming it. An .xls file and an encrypted .xlsx file both start with the OLE2 signature, the bytes D0 CF 11 E0. Check File.Exists before the first call as well. In our probe, streaming a path that did not exist created a zero-byte file there, and a later WorkBook.Load of that path returned a workbook with one empty sheet instead of an error.
The formula row matters most for generated files. Excel stores the last calculated result next to every formula, and the stream returns that result. A workbook written by a script, or by IronXL itself without a call to EvaluateAll before saving, stores the formula alone, and the stream then sees an empty cell. WorkBook.Load calculates those formulas when the cells are read, so a total that matches under the loaded workbook and comes back as zero from the stream points to this case. The formula guide covers EvaluateAll.
Three smaller details affect streamed values.
- Rows are sparse: a blank cell is absent from the row's cells, and the indexer returns null for it.
TryGetCellreturns true for a known column even when the cell itself is null. - Row numbers stay at their sheet position:
RowNumberis zero-based, so sheet row 2 arrives as 1, and a blank sheet row is skipped without renumbering the rows after it. - Dates come typed: a date cell reports the Date type with a DateTime as its value, while its text holds the formatted string, which was 4-27-25 for the fixture's date format.
For a workbook small enough to load, reading XLSX files and selecting ranges cover cell-by-cell access.
How do you handle Excel error values when streaming?
A ledger saved from Excel can carry error values, such as the #N/A a failed lookup leaves behind. The stream reports such a cell with the Error type and the error text, "#N/A" or "#DIV/0!", as a string in its value, and its formula flag is true when the error is a formula's cached result. Convert.ToDecimal cannot parse that string and throws FormatException. In our probe, a copy of the ledger with #N/A in the Amount cell of its last row, sheet row 1,000,001, threw after 17.3 seconds of reading and printed no total.
WorkBook.Load reads error cells without throwing. A stored error comes back with the error code as a Byte value, 42 for #N/A and 7 for #DIV/0!, and the cell text shows the same number. DecimalValue returns 0 for every error cell, and IsError is true. On a 2,000-row ledger with three error amounts on Posted rows, a loading reader that summed DecimalValue came out 26,556.59 short without a warning. One that converted the raw value counted the two stored errors as 42 and 7, then threw FormatException on the formula error.
Test the type before the conversion. Convert an Amount only when its type is StreamCellType.Number, and record any other cell with its row, as the streaming guide's sample does with its StreamCellType.Error case. With that check, the million-row pass finished in 17.6 seconds and flagged one cell, I1000001. Its Posted total of 8,304,532,586.40 is 5,081.52 below the clean file, the amount that row held before the error went in. A loading reader tests IsError first. Print the flagged cells next to the total, so a short total can be traced to its rows.
How do you fall back to WorkBook.Load?
Give Claude Code the second file and have it compare the two readers on it. Before the last prompt, our test run added a 40,000-row workbook that a script generated. Its only sheet is named Adjustments, a title and a blank row sit above the header, and every Amount cell holds a formula with no stored result.
/model sonnet
Finance added data/gl-2025-q4-adjustments.xlsx.
A Python script of theirs writes it instead of Excel.
It has one sheet, with a title block above the header row.
1. Run PathStreamReader over it and compare its Posted totals with FullLoadReader's.
2. Where they disagree, count the stream's Amount cells per Type, IsFormula and Value combination.
3. If some cells give the stream no figure, fall back to WorkBook.Load for that workbook alone.
4. Print which reader produced each total. data/gl-2025.xlsx keeps streaming.
5. Add the adjustments file to the bench as a second workbook.
6. For that workbook, compare every reader with the fallback totals.
7. Don't fail the bench when a stream reader's totals differ there.
8. Run the bench again, wait for it to exit, then read out/bench.csv.
9. Ask the stream-auditor subagent to review the files this turn changed and nothing else.
Tell me the adjustments total and which reader it came from.
In our calibration, the file breaks a reader written for the ledger in three ways. Asking for the Ledger sheet throws ArgumentException. With the default HeaderRowIndex of 0, the title becomes the only header name and the Amount column indexer throws "Unknown header column: 'Amount'." With the header index set to 2, all 40,000 Amount cells come back as Blank formulas, and the streamed Posted total is 0.
What should you verify before trusting generated IronXL code?
Complete these two checks in order after each change.
- The build passes: look up each member in the API reference and rerun from a fresh Release build.
- The peak is the path reader's: read each peak against the thresholds under the Quickstart prompt.
What did the prompts produce in a real run?
The four prompts in this guide went to Claude Code in order, in one session, against the repository described above. A typed reply approved the plan for prompt 03. The Quickstart prompt names no version, so the run installed IronXL.Excel 2026.10.5. The calibration table used 2026.10.1, and the run's times are longer.
- The path reader peaked at 177.3 MB on the ledger after prompt 03 and at 253.4 MB after prompt 04. The fourth prompt's code finds the header row with a
StreamRowspass of its own before the read. - On the adjustments workbook, the stream flagged 36,045 Posted rows with an unusable Amount cell. The per-workbook fallback to a full load produced 15 totals that sum to 330,856,030.50, at a 284.3 MB peak, and it also finished under the 512 MB cap.
- Under the cap, only the path reader finished the ledger, at 253.1 MB. The FileStream reader ran out of memory at 556.1 MB, and the loading reader failed at 531.0 MB.
- Without the cap, the final bench peaked at 689.1 MB for the FileStream reader and 3,071.3 MB for the loading reader, which returned its first row after 43.24 seconds, against 1.35 seconds for the path reader. All 60 totals agreed.
- The Quickstart pass counted 1,000,000 rows with a Posted total of 8,304,537,667.92 in 19.32 seconds, at a 180.96 MB peak.
- The final bench ran 12 child processes, three readers on two workbooks with and without the cap, and exited with code 0. A fresh Release build of the committed code reran it, and its 60 ledger and 15 adjustment totals matched the values the workbook generator recorded.
- Some steps ran differently from the page. The prompt 03 plan closed with two questions, and after the approval Claude Code followed its own recommendations, a 300-second timeout for each child and no change to
CLAUDE.md. The subagent then reportedUploadStreamReaderin prompt 03 and theWorkBook.Loadfallback in prompt 04 as breaking theCLAUDE.mdrules, although the prompts asked for both. Claude Code passed each finding on and left the rules alone, so add an exemption for each if the review should come back clean. Every bench ran in the foreground and both reviews came back. - An earlier run used longer wording of the same prompts. There the subagent reported
WorkSheet.RowCountandColumnCountas missing, althoughIronXL.xmldocuments both under Range, the class WorkSheet inherits them from. Auto mode in that run also refused an edit to the subagent file in prompt 04 as self-modification, so plan to approve such edits yourself. Before that, Claude Code twice started a bench as a background task and ended its turn while the children were still running. In the second of those runs, a header loop that testedPhysicalColumnCounton every pass also kept the loading child busy past its five-minute timeout.
The run record lists the versions, models, timings and test machine behind this guide and the others in the set.
Troubleshooting
- Apply a License Key in IronXL if
StreamRowsthrows a licensing exception before the first row - IronXL File Size Limits if a workbook nears the row limit of its format
- IronXL Data-Loading Method Performance if the job also writes large workbooks
Questions?
If you have any questions, reach out to support@ironsoftware.com
Curtis Chau holds a Bachelor’s degree in Computer Science (Carleton University) and specializes in front-end development with expertise in Node.js, TypeScript, JavaScript, and React. Passionate about crafting intuitive and aesthetically pleasing user interfaces, Curtis enjoys working with modern frameworks and creating well-structured, visually appealing manuals.