| name | headless-word |
| description | Creating & editing Word documents (.docx) via CLI. Use when the user needs to create, read, edit, or manipulate Word documents. |
headless-word CLI
Setup
which headless-word 2>/dev/null || uv tool install headless-word
Requires LibreOffice installed:
- macOS:
brew install --cask libreoffice
- Linux:
sudo apt install libreoffice libreoffice-writer python3-uno
- Windows: download from libreoffice.org
For screenshots, also need poppler-utils (brew install poppler / apt install poppler-utils).
headless-word check
Daemon Lifecycle
headless-word start
headless-word stop
headless-word status
The daemon auto-exits after 5 minutes of inactivity.
Session Management
Every document operation requires a session. Open returns a session ID used by all subsequent commands:
headless-word open /path/to/doc.docx
headless-word new /path/to/new.docx
headless-word save <sid> [--path out.docx]
headless-word close <sid>
headless-word list
headless-word export-pdf <sid> out.pdf [--no-comments] [--no-changes] [--pages 1-3]
Reading Tools
All output is JSON on stdout.
get-document-text
Returns paragraphs with text, style, alignment, list info:
headless-word get-document-text <sid>
headless-word get-document-text <sid> --start 5 --end 10
headless-word get-document-text <sid> --no-formatting
get-document-structure
Returns paragraph count, headings, tables, sections, page count, content controls:
headless-word get-document-structure <sid>
get-ooxml
Extracts OOXML and writes it to a temp file. Returns the file path plus a summary with body-child indices, types, line numbers, and text previews. Includes referenced styles and numbering definitions. Never load the full XML into context — use grep or read with offset/limit.
headless-word get-ooxml <sid>
headless-word get-ooxml <sid> --start-child 0 --end-child 5
grep -n 'rFonts\|w:sz \|w:color\|w:b/' /path/to/body-0-5.xml | head -20
grep -n 'w:numPr\|w:pStyle' /path/to/body-0-5.xml | head -20
read /path/to/body-0-5.xml --offset 710 --limit 40
Body children are direct elements under <w:body>: paragraphs (<w:p>), tables (<w:tbl>), content controls (<w:sdt>), section properties (<w:sectPr>).
screenshot
Renders a page to PNG via LO PDF export + pdftoppm:
headless-word screenshot <sid> --page 1 --out page1.png
headless-word screenshot <sid> --page 2 --dpi 300 --no-comments --no-changes
Writing Tools
execute (primary editing tool)
Execute Python code inside LibreOffice with full UNO API access.
Available in scope: doc, text, cursor, desktop, uno, smgr, ctx, prop. Set result to return a value.
cat <<'EOF' | headless-word execute <sid>
from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK
text.insertString(cursor, "Hello World", False)
text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False)
result = "done"
EOF
headless-word execute <sid> --code 'text.insertString(cursor, "Hello", False)'
headless-word execute <sid> --file script.py
headless-word execute <sid> --raw --code 'result = doc.getText().getString()'
insert-ooxml
Insert raw OOXML elements (<w:p>, <w:tbl>, etc.) into the document body via direct ZIP/XML manipulation:
headless-word insert-ooxml <sid> --xml '<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:r><w:rPr><w:b/></w:rPr><w:t>Bold text</w:t></w:r></w:p>'
headless-word insert-ooxml <sid> --file snippet.xml --position start
headless-word insert-ooxml <sid> --file snippet.xml --position "after:5"
cat snippet.xml | headless-word insert-ooxml <sid> --position end
Positions: end (default, before sectPr), start, after:<body_child_index>
Live Preview
headless-word watch /path/to/doc.docx --open
headless-word watch /path/to/doc.docx --port 8090 --dpi 150
Auto-refreshes when the .docx changes on disk. The watcher opens/closes sessions for each render, so it won't block saves from other sessions.
⚠️ Preserving Formatting When Editing Existing Content
setString() and cell.setString() DESTROY direct run-level formatting (<w:rPr> — fonts, sizes, colors, bold/italic), reverting to bare style defaults. Most real-world documents rely on direct formatting.
Editing decision tree:
-
Simple text replacement → Use search/replace via execute (auto-preserves formatting):
search = doc.createSearchDescriptor()
search.SearchString = "old text"
search.ReplaceString = "new text"
doc.replaceAll(search)
-
New content in new/empty document → Safe to use execute with insertString()
-
Editing formatted content (templates, styled docs) → Inspect first, then use OOXML:
headless-word get-ooxml <sid> --start-child 2 --end-child 2
grep -n 'rFonts\|w:sz \|w:color\|w:b/' /path/to/body.xml | head -20
-
List paragraphs with <w:numPr> — bullets/numbers render automatically. Do NOT prefix text with - or 1. .
Adding new content to existing documents
Match the document's formatting — don't guess. Grep a nearby body child for font/size/color and apply the same properties via execute or matching <w:rPr> blocks in insert-ooxml.
OOXML Reference
Run properties (<w:rPr>) — direct character formatting:
<w:r>
<w:rPr>
<w:rFonts w:ascii="Open Sans" w:hAnsi="Open Sans" w:cs="Open Sans"/>
<w:sz w:val="18"/>
<w:szCs w:val="18"/>
<w:color w:val="002060"/>
<w:b/>
<w:i/>
</w:rPr>
<w:t>Formatted text</w:t>
</w:r>
Paragraph properties (<w:pPr>) — element order matters:
<w:pPr>
<w:pStyle w:val="Normal"/>
<w:numPr>
<w:ilvl w:val="0"/>
<w:numId w:val="1"/>
</w:numPr>
<w:spacing w:after="200"/>
<w:jc w:val="left"/>
<w:rPr>...</w:rPr>
</w:pPr>
Whitespace preservation — required when text has leading/trailing spaces:
<w:t xml:space="preserve"> text with spaces </w:t>
Execute Cookbook
Text & Paragraphs
from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK
text.insertString(cursor, "Hello World", False)
text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False)
cursor.setPropertyValue("ParaStyleName", "Heading 1")
text.insertString(cursor, "Section Title", False)
Formatting
from com.sun.star.awt.FontWeight import BOLD
cursor.setPropertyValue("CharHeight", 12)
cursor.setPropertyValue("CharWeight", BOLD)
cursor.setPropertyValue("CharColor", 0x2E75B6)
cursor.setPropertyValue("ParaAdjust", 3)
cursor.setPropertyValue("ParaTopMargin", 200)
cursor.setPropertyValue("ParaBottomMargin", 100)
cursor.setPropertyValue("ParaLeftMargin", 500)
Tables
table = doc.createInstance("com.sun.star.text.TextTable")
table.initialize(3, 4)
text.insertTextContent(cursor, table, False)
table.getCellByName("A1").setString("Header")
table.getCellByName("B1").setValue(42)
Comments
import datetime
def now_dt():
n = datetime.datetime.now()
dt = uno.createUnoStruct("com.sun.star.util.DateTime")
dt.Year, dt.Month, dt.Day = n.year, n.month, n.day
dt.Hours, dt.Minutes, dt.Seconds = n.hour, n.minute, n.second
return dt
comment = doc.createInstance("com.sun.star.text.textfield.Annotation")
comment.setPropertyValue("Content", "Review this section")
comment.setPropertyValue("Author", "Reviewer")
comment.setPropertyValue("DateTimeValue", now_dt())
text.insertTextContent(cursor, comment, False)
comment.setPropertyValue("Resolved", True)
Threaded replies: Set ParaIdParent on the reply to the parent's ParaId. headless-word save auto-injects proper commentsExtended.xml threading (LO's OOXML export doesn't serialize this correctly).
parent = doc.createInstance("com.sun.star.text.textfield.Annotation")
parent.setPropertyValue("Content", "Please review")
parent.setPropertyValue("Author", "Reviewer")
parent.setPropertyValue("DateTimeValue", now_dt())
text.insertTextContent(cursor, parent, False)
reply = doc.createInstance("com.sun.star.text.textfield.Annotation")
reply.setPropertyValue("Content", "Addressed in v2")
reply.setPropertyValue("Author", "Author")
reply.setPropertyValue("DateTimeValue", now_dt())
reply.setPropertyValue("ParaIdParent", parent.getPropertyValue("ParaId"))
text.insertTextContent(cursor, reply, False)
Tracked Changes
doc.setPropertyValue("RecordChanges", True)
doc.setPropertyValue("RecordChanges", False)
doc.setPropertyValue("RedlineMode", 0)
Images
img = doc.createInstance("com.sun.star.text.TextGraphicObject")
img.setPropertyValue("GraphicURL", uno.systemPathToFileUrl("/path/to/image.png"))
size = uno.createUnoStruct("com.sun.star.awt.Size")
size.Width = 5000
size.Height = 3000
img.setPropertyValue("Size", size)
text.insertTextContent(cursor, img, False)
Headers & Footers
styles = doc.getStyleFamilies().getByName("PageStyles")
ps = styles.getByName("Standard")
ps.setPropertyValue("HeaderIsOn", True)
header = ps.getPropertyValue("HeaderText")
hcursor = header.createTextCursor()
header.insertString(hcursor, "Company Name", False)
Page Setup
styles = doc.getStyleFamilies().getByName("PageStyles")
ps = styles.getByName("Standard")
ps.setPropertyValue("TopMargin", 1000)
ps.setPropertyValue("BottomMargin", 1000)
ps.setPropertyValue("LeftMargin", 1200)
ps.setPropertyValue("RightMargin", 1200)
Read Document Content
result = doc.getText().getString()
enum = text.createEnumeration()
paragraphs = []
while enum.hasMoreElements():
p = enum.nextElement()
if p.supportsService("com.sun.star.text.Paragraph"):
paragraphs.append(p.getString())
result = paragraphs
tables = doc.getTextTables()
result = tables.getByIndex(0).getCellByName("A1").getString()
Best Practices
- Read before writing — use
get-document-structure for layout, get-document-text for content, get-ooxml + grep for formatting. Never assume fonts/sizes/styles.
- Build incrementally — add one section at a time,
save → screenshot → verify. Use watch --open for live preview.
- Use heredocs —
cat <<'EOF' | headless-word execute <sid> avoids shell expansion issues.
Unit reference
- Spacing: 1/100mm in UNO (1000 = 10mm)
- Font size: points in UNO (
CharHeight), half-points in OOXML (<w:sz w:val="18"/> = 9pt)
- Colors: RGB int in UNO (
0xFF0000), hex string without # in OOXML (w:val="002060")
- Borders: use
uno.createUnoStruct("com.sun.star.table.BorderLine2") (don't import)
UNO API Reference
IDL type definitions in ./uno-idl/ (relative to this skill directory). Grep to discover properties, methods, or type names:
grep -r "Annotation\|ParentName" references/uno-idl/com/sun/star/text/
grep -r "CellProperties\|BackColor" references/uno-idl/com/sun/star/table/
grep -r "CharHeight\|ParaAdjust" references/uno-idl/com/sun/star/awt/ references/uno-idl/com/sun/star/style/
Key namespaces: text/ (paragraphs, tables, fields, annotations), sheet/ (cells, ranges, formulas), table/ (borders, cell properties), drawing/ + presentation/ (shapes, slides), awt/ (font weights, colors, sizes), style/ (paragraph/character/page styles), document/ (doc-level properties), frame/ (desktop, dispatch), util/ (date, URL, search).
Note: Some runtime properties (e.g. ParentName, Resolved, ParaId on Annotation) exist at runtime but aren't declared in the IDL — they're undocumented C++ extensions.