Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. When Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 72% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 51% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 31% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 72% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 246% | 0% |
A .docx file is a ZIP archive containing XML files.
| Task | Approach | |------|----------| | Read/analyze content | pandoc or unpack for raw XML | | Create new document | Use docx-js - see Creating New Documents below | | Edit existing document | Unpack → edit XML → repack - see Editing Existing Documents below |
bash# Text extraction with tracked changes pandoc --track-changes=all document.docx -o output.md # Raw XML access python3 scripts/unpack.py document.docx unpacked/
bashsoffice --headless --convert-to pdf document.docx pdftoppm -jpeg -r 150 document.pdf page
Generate .docx files with JavaScript. Install: npm install -g docx
javascriptconst { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink, TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType, VerticalAlign, PageNumber, PageBreak } = require('docx'); const doc = new Document({ sections: [{ children: [/* content */] }] }); Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer));
javascript// CRITICAL: docx-js defaults to A4, not US Letter // Always set page size explicitly for consistent results sections: [{ properties: { page: { size: { width: 12240, // 8.5 inches in DXA height: 15840 // 11 inches in DXA }, margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } // 1 inch margins } }, children: [/* content */] }]
Common page sizes (DXA units, 1440 DXA = 1 inch):
| Paper | Width | Height | Content Width (1" margins) | |-------|-------|--------|---------------------------| | US Letter | 12,240 | 15,840 | 9,360 | | A4 (default) | 11,906 | 16,838 | 9,026 |
Use Arial as the default font (universally supported). Keep titles black for readability.
javascriptconst doc = new Document({ styles: { default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt default paragraphStyles: [ // IMPORTANT: Use exact IDs to override built-in styles { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: 32, bold: true, font: "Arial" }, paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // outlineLevel required for TOC { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: 28, bold: true, font: "Arial" }, paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } }, ] }, sections: [{ children: [ new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] }), ] }] });
javascript// ❌ WRONG - never manually insert bullet characters new Paragraph({ children: [new TextRun("• Item")] }) // BAD new Paragraph({ children: [new TextRun("\u2022 Item")] }) // BAD // ✅ CORRECT - use numbering config with LevelFormat.BULLET const doc = new Document({ numbering: { config: [ { reference: "bullets", levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }, { reference: "numbers", levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }, ] }, sections: [{ children: [ new Paragraph({ numbering: { reference: "bullets", level: 0 }, children: [new TextRun("Bullet item")] }), new Paragraph({ numbering: { reference: "numbers", level: 0 }, children: [new TextRun("Numbered item")] }), ] }] }); // ⚠️ Each reference creates INDEPENDENT numbering // Same reference = continues (1,2,3 then 4,5,6) // Different reference = restarts (1,2,3 then 1,2,3)
CRITICAL: Tables need dual widths - set both columnWidths on the table AND width on each cell. Without both, tables render incorrectly on some platforms.
javascript// CRITICAL: Always set table width for consistent rendering // CRITICAL: Use ShadingType.CLEAR (not SOLID) to prevent black backgrounds const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }; const borders = { top: border, bottom: border, left: border, right: border }; new Table({ width: { size: 100, type: WidthType.PERCENTAGE }, // Always set table width columnWidths: [4680, 4680], // Set at table level (DXA: 1440 = 1 inch) rows: [ new TableRow({ children: [ new TableCell({ borders, width: { size: 4680, type: WidthType.DXA }, // Also set on each cell shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, // CLEAR not SOLID margins: { top: 80, bottom: 80, left: 120, right: 120 }, // Cell padding (internal, not added to width) children: [new Paragraph({ children: [new TextRun("Cell")] })] }) ] }) ] })
Table width calculation:
Use WidthType.PERCENTAGE for simplicity, or WidthType.DXA for precise control:
javascript// Option 1: Percentage (recommended - automatically fits content area) width: { size: 100, type: WidthType.PERCENTAGE } // Option 2: DXA (precise control) // Table width = sum of columnWidths = content width // US Letter with 1" margins: 12240 - 2880 = 9360 DXA width: { size: 9360, type: WidthType.DXA }, columnWidths: [7000, 2360] // Must sum to table width
Width rules:
columnWidthswidth must match corresponding columnWidthmargins are internal padding - they reduce content area, not add to cell widthjavascript// CRITICAL: type parameter is REQUIRED new Paragraph({ children: [new ImageRun({ type: "png", // Required: png, jpg, jpeg, gif, bmp, svg data: fs.readFileSync("image.png"), transformation: { width: 200, height: 150 }, altText: { title: "Title", description: "Desc", name: "Name" } // All three required })] })
javascript// CRITICAL: PageBreak must be inside a Paragraph new Paragraph({ children: [new PageBreak()] }) // Or use pageBreakBefore new Paragraph({ pageBreakBefore: true, children: [new TextRun("New page")] })
javascript// CRITICAL: Headings must use HeadingLevel ONLY - no custom styles new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" })
javascriptsections: [{ properties: { page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } // 1440 = 1 inch }, headers: { default: new Header({ children: [new Paragraph({ children: [new TextRun("Header")] })] }) }, footers: { default: new Footer({ children: [new Paragraph({ children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] })] })] }) }, children: [/* content */] }]
\n - use separate Paragraph elementsLevelFormat.BULLET with numbering configtype - always specify png/jpg/etcwidth - use { size: 100, type: WidthType.PERCENTAGE } for full widthcolumnWidths array AND cell width, both must matchmargins: { top: 80, bottom: 80, left: 120, right: 120 } for readable paddingShadingType.CLEAR - never SOLID for table shadingoutlineLevel - required for TOC (0 for H1, 1 for H2, etc.)Follow all 3 steps in order.
bashpython3 scripts/unpack.py document.docx unpacked/
Extracts XML, pretty-prints, merges adjacent runs, and converts smart quotes to XML entities (“ etc.) so they survive editing. Use --merge-runs false to skip run merging.
Edit files in unpacked/word/. See XML Reference below for patterns.
Always use "Claude" as the author for tracked changes and comments.
Use the Edit tool directly for string replacement. Do not write Python scripts. Scripts introduce unnecessary complexity. The Edit tool shows exactly what is being replaced.
CRITICAL: Use smart quotes for new content. When adding text with apostrophes or quotes, use XML entities to produce smart quotes:
xml<!-- Use these entities for professional typography --> <w:t>Here’s a quote: “Hello”</w:t>
| Entity | Character | |--------|-----------| | ‘ | ‘ (left single) | | ’ | ’ (right single / apostrophe) | | “ | “ (left double) | | ” | ” (right double) |
Adding comments: Use comment.py to handle boilerplate across multiple XML files (text must be pre-escaped XML):
bashpython3 scripts/comment.py unpacked/ 0 "Comment text with & and ’" python3 scripts/comment.py unpacked/ 1 "Reply text" --parent 0 # reply to comment 0
Then add markers to document.xml (see Comments in XML Reference).
bashpython3 scripts/pack.py unpacked/ output.docx --original document.docx
Validates with auto-repair, condenses XML, and creates DOCX. Use --validate false to skip.
Auto-repair will fix:
durableId >= 0x7FFFFFFF (regenerates valid ID)xml:space="preserve" on <w:t> with whitespaceAuto-repair won't fix:
<w:r> elements: When adding tracked changes, replace the whole <w:r>...</w:r> block with <w:del>...<w:ins>... as siblings. Don't inject tracked change tags inside a run.<w:rPr> formatting: Copy the original run's <w:rPr> block into your tracked change runs to maintain bold, font size, etc.<w:pPr>: <w:pStyle>, <w:numPr>, <w:spacing>, <w:ind>, <w:jc>, <w:rPr> lastxml:space="preserve" to <w:t> with leading/trailing spaces00AB1234)Insertion:
xml<w:ins w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z"> <w:r><w:t>inserted text</w:t></w:r> </w:ins>
Deletion:
xml<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z"> <w:r><w:delText>deleted text</w:delText></w:r> </w:del>
Minimal edits - only mark what changes:
xml<!-- Change "30 days" to "60 days" --> <w:r><w:t>The term is </w:t></w:r> <w:del w:id="1" w:author="Claude" w:date="..."> <w:r><w:delText>30</w:delText></w:r> </w:del> <w:ins w:id="2" w:author="Claude" w:date="..."> <w:r><w:t>60</w:t></w:r> </w:ins> <w:r><w:t> days.</w:t></w:r>
Rejecting another author's insertion - nest deletion inside their insertion:
xml<w:ins w:author="Jane" w:id="5"> <w:del w:author="Claude" w:id="10"> <w:r><w:delText>their inserted text</w:delText></w:r> </w:del> </w:ins>
Restoring another author's deletion - add insertion after (don't modify their deletion):
xml<w:del w:author="Jane" w:id="5"> <w:r><w:delText>deleted text</w:delText></w:r> </w:del> <w:ins w:author="Claude" w:id="10"> <w:r><w:t>deleted text</w:t></w:r> </w:ins>
After running comment.py (see Step 2), add markers to document.xml. For replies, use --parent flag and nest markers inside the parent's:
xml<!-- Comment 0 with reply 1 nested inside --> <w:commentRangeStart w:id="0"/> <w:commentRangeStart w:id="1"/> <w:r><w:t>text</w:t></w:r> <w:commentRangeEnd w:id="1"/> <w:commentRangeEnd w:id="0"/> <w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r> <w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="1"/></w:r>
word/media/word/_rels/document.xml.rels:xml<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>
[Content_Types].xml:xml<Default Extension="png" ContentType="image/png"/>
xml<w:drawing> <wp:inline> <wp:extent cx="914400" cy="914400"/> <!-- EMUs: 914400 = 1 inch --> <a:graphic> <a:graphicData uri=".../picture"> <pic:pic> <pic:blipFill><a:blip r:embed="rId5"/></pic:blipFill> </pic:pic> </a:graphicData> </a:graphic> </wp:inline> </w:drawing>
npm install -g docx (new documents)pdftoppm for imagesOther measured skills in the registry, with their headline benchmark lift.