Install
$ agentstack add skill-matrixfounder-universal-skills-docx ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
docx skill
Purpose: Give the agent a deterministic, script-first way to create, edit, and convert Microsoft Word .docx files so it does not have to re-derive low-level OOXML constructs (page sizes, tables, tracked changes, comment markers, image embeddings, template substitution) on every task. Writing .docx by hand through docx-js or python-docx calls is feasible but error-prone; the scripts here encode the practical knowledge and make the common operations a single command.
1. Red Flags (Anti-Rationalization)
STOP and READ THIS if you are thinking:
- "I'll just assemble the docx inline with
docx-jscalls, the scripts are overkill." → WRONG.md2docx.jsalready handles page size, numbering, dual-width tables, and image alt-text — regressions from hand-rolled code usually show as silently broken tables or images on Word for Windows. - "I can skip
docx_fill_template.py's run-merge pass and just regex-replace{{placeholder}}." → WRONG. Word and Google Docs fracture a single placeholder across multiple `` siblings after spell-check or autocorrect; a naïve regex won't match and the placeholder will ship unfilled. - "For tracked changes I'll just delete every `
block." → **WRONG**. That loses author attribution and breaks paragraph-level delete markers. Usedocxacceptchanges.py` which drives LibreOffice's own accept dispatcher. - "The smart-quote entity rewrite in
office/unpack.pyis ugly, I'll turn it off." → WRONG. Different Word builds round-trip UTF-8 quotes inconsistently. Keeping them as’etc. on unpack and reversing on pack makes edits deterministic. - "I'll just regex-replace
\|in OOXML to extract or modify text." → WRONG. Raw XML regex operates on serialised bytes and breaks namespace prefixes, entity encoding, and split-run anchors. Usedocx_replace.py(anchor-and-action surgical edit) which works at the lxml element level after run canonicalisation. - "I need to edit a
.docxand thedocx2md → md2docxround-trip loses styling." → Usedocx_replace.py --replaceinstead. It operates at run level, preserving bold/italic/colour/numbering. Round-trip through markdown is lossy: inline formatting, list numbering schemas, and table borders all flatten.
2. Capabilities
- Surgical anchor-and-action edit of a live
.docx(no template required) viadocx_replace.py— find a text anchor and--replace TEXT/--insert-after PATH/--delete-paragraphwithout a lossydocx2md → md2docxround-trip. Run-level formatting (bold, italic, colour, numbering) is preserved.--insert-afterautomatically relocates images, charts, OLE objects, SmartArt diagrams, and numbered/bulleted lists from the MD source into the base document (docx-6.5 + docx-6.6 v2 relocators, 2026-05-12). Honest scope:--replacerequires the anchor to fit within a single `after run-merge (cross-run anchors not supported, R10.a);--insert-afterand--delete-paragraphwork at paragraph granularity. Anchor not found → exit 2AnchorNotFound; last-paragraph delete refused → exit 2LastParagraphCannotBeDeleted`. - Convert Markdown to
.docxwith headings, lists, tables, images, and optional mermaid diagrams viamd2docx.js. - Convert HTML / web archives to
.docxviahtml2docx.js— accepts.html/.htm, Chrome.mhtml/.mht, and Safari.webarchive(sub-resources extracted to a temp dir). Confluence, GitLab wiki, and CMS chrome (ARIA-role-tagged headers/sidebars/footers, namespaced `macros,tablesorterwrappers) is stripped automatically before walking. Inline SVG diagrams (drawio/mermaid/PlantUML) are rastered to PNG via a two-tier renderer: **Tier 1** = headless Chrome / Chromium / Edge / Brave (auto-detected from conventional install paths orHTML2DOCX_BROWSER) gives pixel-perfect CSS layout including foreignObject labels and word-wrap; **Tier 2** =@resvg/resvg-jsfallback for hosts without a browser, with foreignObject → SVGconversion that preserves drawio's centring math, synthesises a viewBox on canvas-clipped diagrams (5% expansion absorbs drawio's edge-overshoot artifact), and word-wraps long Cyrillic/Latin labels at the wrapper'swidth:Npxso text fits inside its box.--reader-modeopt-in: stripped CMS chrome via a curated candidate list (#main-content,.entry,.post-content,article) with per-candidate min-text filter — useful for browser-saved news / blog pages where Confluence-priority selectors fall through to`. - Extract
.docxcontent back to Markdown preserving tables, lists, and embedded images viadocx2md.js. Comments and tracked changes (which mammoth strips) are pulled into a JSON sidecar (.docx2md.json) — useful for contract audits where the audit trail must accompany the converted markdown. Footnotes/endnotes are converted to pandoc-style[^fn-N]/[^en-N]markers with definitions appended at the end. Schema versioned (v: 1); the sidecar'sunsupportedfield reports counts of revision types not yet captured (rPrChange,pPrChange,moveFrom,moveTo,cellIns,cellDel) so callers know what was lost. Opt-out via--no-metadata(skip sidecar) and--no-footnotes(skip pandoc conversion). Sidecar is not written when the source has no comments, no revisions, and zero unsupported counts — clean docs stay clean. - Fill
.docxtemplates containing{{placeholder}}or{{nested.key}}markers from a JSON payload with safe run-merging. - Accept all tracked changes in a
.docxvia headless LibreOffice without leaving artefacts in the user's profile. - Unpack and repack
.docxarchives for raw OOXML editing, with smart-quote entity round-tripping and run canonicalisation. - Structurally validate a
.docx: relationships, content types, tracked-change/`integrity, comment marker pairing, **package-layout allow-list** (every ZIP entry must live under[ContentTypes].xml,rels/,word/,docProps/, orcustomXml/per ECMA-376 §11.3.10 — catches scratch-file leaks that Word refuses to open while LibreOffice silently tolerates), and optional XSD binding. With--strict`, the package-layout warning is promoted to exit 1. - Reject password-protected and legacy
.doc(CFB-container) inputs early with a clear remediation message (exit 3) instead of aBadZipFiletraceback. - Detect macro-enabled inputs (
.docm, withvbaProject.bin) and warn when the chosen output extension would silently drop the macros (docm→docx). - Render any
.docx/.docm/.pdf(or peer-skill.xlsx/.pptx) into a single PNG-grid preview viapreview.py(LibreOffice + Poppler). - Emit failures as machine-readable JSON to stderr with
--json-errors(uniform across all four office skills). - Set or remove a password on a
.docx/.xlsx/.pptx(MS-OFB Agile, Office 2010+) viaoffice_passwd.py— three modes:--encrypt PASSWORD,--decrypt PASSWORD,--check(exit 0 encrypted / 10 clean / 11 missing). - Insert a Word review comment anchored on a text substring via
docx_add_comment.py— wires `//markers, appends toword/comments.xml, and patches[Content_Types].xml+ relationships. Supports threaded **replies** (--parent N) viaincommentsExtended.xml; reply-to-reply chains are flattened to the conversation root to match Word's review-pane render. Multi-paragraph bodies via\nin--commentare split into separateper ECMA-376 §17.13.4.2. Opt-in **library mode** (--unpacked-dir DIR) operates in-place on an already-unpacked tree (skips unpack/pack/encryption-check) — **not reentrant**: do not run two processes against the same tree concurrently, no file locking. Malformed OOXML side-parts surface asMalformedOOXMLenvelope (not a traceback). See [references/add-comment-howto.md`](references/add-comment-howto.md) for verification steps and §6 troubleshooting for failure modes. - Merge N
.docxfiles into one viadocx_merge.py(VDD iter-2 real-world hardened). Appends body content + styles + numbering + media + relationships into the first input (base), with full reference relocation: image rIds renumbered,r:embed/r:link/r:idin body remapped, `bumped past base's max,/shifted withbody refs rewritten, missingDefault Extensionentries pulled into[Content_Types].xml. Strips paragraph-levelfrom extras (their header/footer references would dangle). Inserts newBEFORE first` per ECMA-376 §17.9.20 schema-order. Honest scope (still not merged, warned when extras have content): footnotes / endnotes / headers / footers / comments.
3. Execution Mode
- Mode:
script-first. - Why this mode: Each operation is a small, deterministic CLI wrapping a specific OOXML-aware library. Inline assembly in the agent's text is slower and regresses on details (page size, DXA units, namespace ordering). Scripts also make the skill reusable from any shell environment.
4. Script Contract
- Commands:
node scripts/md2docx.js INPUT.md OUTPUT.docx [--header "TEXT"] [--footer "TEXT"] [--page-size A4|Letter] [--landscape] [--margins T,R,B,L]node scripts/docx2md.js INPUT.docx OUTPUT.md [--metadata-json PATH] [--no-metadata] [--no-footnotes] [--json-errors]node scripts/html2docx.js INPUT OUTPUT.docx [--header "TEXT"] [--footer "TEXT"] [--reader-mode] [--json-errors]— INPUT may be.html/.htm,.mhtml/.mht, or.webarchive; sub-resources in archives are extracted to a temp dir automatically (cleaned up on exit incl. SIGINT/SIGTERM).--reader-modeswaps the article-root candidate list for a CMS/blog-specific one (#main-content/.entry/.post-content/articlewith per-candidate min-text filter; bare `deliberately omitted as it wraps whole-site chrome on news sites). Override SVG renderer withHTML2DOCXBROWSER=/path/to/chromeor set it to a non-existent path to force the resvg-js fallback for CI determinism. SetHTML2DOCXALLOWNOSANDBOX=1` only inside a trusted CI container — default leaves Chrome's sandbox enabled.python3 scripts/docx_fill_template.py TEMPLATE.docx DATA.json OUTPUT.docx [--strict]python3 scripts/docx_accept_changes.py INPUT.docx OUTPUT.docx [--timeout 120]python3 scripts/office/unpack.py INPUT.docx OUTDIR/ [--no-pretty] [--no-escape-quotes] [--no-merge-runs]python3 scripts/office/pack.py INDIR/ OUTPUT.docx [--no-unescape-quotes] [--no-condense]python3 scripts/office/validate.py INPUT.docx [--strict] [--json] [--schemas-dir PATH] [--compare-to ORIGINAL.docx]python3 scripts/preview.py INPUT OUTPUT.jpg [--cols 3] [--dpi 110] [--gap 12] [--padding 24] [--label-font-size 14] [--soffice-timeout 240] [--pdftoppm-timeout 60]python3 scripts/office_passwd.py INPUT [OUTPUT] (--encrypt PASSWORD | --decrypt PASSWORD | --check)— pass-as PASSWORD to read it from stdin.python3 scripts/docx_add_comment.py INPUT.docx OUTPUT.docx --anchor-text TEXT --comment BODY [--author NAME] [--initials AB] [--date ISO] [--all]python3 scripts/docx_add_comment.py INPUT.docx OUTPUT.docx --parent N --comment BODY [--author NAME]— reply to comment N (inherits its anchor range; threads viacommentsExtended.xml).python3 scripts/docx_add_comment.py --unpacked-dir DIR --anchor-text TEXT --comment BODY [...]— library mode: edit an already-unpacked tree in-place (combine with--parentto add a reply over the same tree).python3 scripts/docx_replace.py INPUT.docx OUTPUT.docx --anchor TEXT (--replace TEXT | --insert-after PATH_OR_DASH | --delete-paragraph) [--all] [--unpacked-dir DIR] [--scope=LIST] [--json-errors]— Tier B (script-first); positional INPUT OUTPUT;--anchor TEXTis always required; exactly one of--replace/--insert-after/--delete-paragraphis required (mutex).--allreplaces/acts on every match instead of first.--unpacked-dir DIRoperates on an already-unpacked tree in-place (library mode — skips unpack/pack/encryption-check).--insert-after -reads the markdown body from stdin.--scope=LIST(docx-6.7) restricts anchor-search to a subset of OOXML parts: comma-separatedbody,headers,footers,footnotes,endnotes,all(default:all). Example:--scope=bodylimits edits toword/document.xml, leaving header/footer boilerplate untouched. Order within the requested set is deterministic (document → headers → footers → footnotes → endnotes). Exit codes: 0 success, 1 I/O or OOXML error, 2 anchor-not-found / last-paragraph-delete refused / invalid--scopevalue, 3 encrypted/password-protected input, 6 same-path self-overwrite refused, 7 post-validate failure.--json-errorsemits failures as{v:1, error, code, type, details}JSON on stderr (cross-5 envelope parity). Honest scope:--replaceanchor must fit within a single `after run-merge;--insert-afterconverts markdown to OOXML viamd2docx.js` (requires Node.js in PATH).python3 scripts/docx_merge.py OUTPUT.docx INPUT1.docx INPUT2.docx [...] [--page-break-between] [--no-merge-styles]- All scripts above accept
--json-errorsto emit failures as a single line of JSON on stderr ({v, error, code, type?, details?}). The schema versionvis currently1; argparse usage errors are routed through the same envelope (type:"UsageError"). - Inputs: positional paths only; optional flags per command.
- Outputs: a single file at the named output path;
office/unpack.pyproduces a directory tree;office/validate.pyprints a report (or JSON with--json).docx2md.jsadditionally creates_images/next to the Markdown output when the document has embedded images. - Failure semantics: non-zero exit on missing input, invalid JSON, unresolved placeholders (with
--strict), unreadable ZIP, or soffice errors. Error detail goes to stderr. - Idempotency: repeated runs with the same inputs produce equivalent output files (byte-exact for the XML parts after pretty-print and entity normalisation).
docx_accept_changes.pyis idempotent on an already-accepted document. Exception:office_passwd.py --encryptis intentionally non-deterministic — Office encryption uses a fresh random salt per run, so the encrypted bytes differ each time even with the same password. The decrypted output, however, is byte-equal to the pre-encryption input (lossless round-trip). - Dry-run support: not applicable; operations are file-to-file and reversible by re-running.
5. Safety Boundaries
- Allowed scope: only the input/output paths named on the command line and, for Python scripts, modules under
scripts/. Never modify files the user did not name. - Default exclusions: never overwrite the input in place unless the same path is also passed as output; never fetch remote images silently; never install npm/pip packages globally.
- Destructive actions:
docx_accept_changes.pyrewrites the destination; always use a distinct output path unless the user explicitly asks for in-place replacement.office/pack.pyoverwrites the destination archive. - Optional artifacts: the
office/schemas/directory is optional;office/validate.pyruns structural checks without it and only warns about missing XSDs unless--strict.
6. Validation Evidence
- Local verification:
cd skills/docx/scripts && npm install— installs docx, marked, mammoth, turndown, image-size, turndown-plugin-gfm intoscripts/node_modules/.python3 -m venv .venv && source .venv/bin/activate && pip install -r scripts/requirements.txt— installs python-docx, lxml, defusedxml.node scripts/md2docx.js examples/fixture-simple.md /tmp/out.docx && unzip -l /tmp/out.docx | grep word/document.xml— exits 0 and lists the document part.node scripts/docx2md.js /tmp/out.docx /tmp/back.md && test -s /tmp/back.md— round-trips non-empty Markdown. On a docx with comments / tracked changes, also writes/tmp/back.docx2md.json(schemav:1); on a docx with footnotes/endnotes, the markdown gets pandoc-style[^fn-N]markers
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: MatrixFounder
- Source: MatrixFounder/Universal-skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.