Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Edit existing DOCX files — tracked-change redlines, placeholder fills, range deletes, comments, and accept/reject finalization. Plain-text-only tools; no raw OOXML. For structural edits beyond text replacement, invoke `Skill('docx-xml')`.
.claude/skills/anylegal-ai-docx-editing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | -56% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 259% | 0% |
| case-21 | ✗→✓ | ▲ Improved | 51% | 0% |
| case-20 | ✓→✗ | ▼ Worse | 167% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 106% | 0% |
Author name for tracked changes: Use the user's name from anylegal.md (the "About Me" section). If unknown, use "Anylegal.ai".
edit_document is the default tool for editing existing DOCX files. Pass plain-text old_text / new_text; the server generates <w:ins> / <w:del> markup and preserves run properties.
| Task | Tool | Notes | |------|------|-------| | Change clause text (tracked) | edit_document | One change per call. | | Fill a placeholder | edit_document | Same tool — tracked change; finalize later if user wants a clean output. | | Delete a section | edit_document with start_text / end_text | Range deletion — everything between two anchors, inclusive. | | Disambiguate duplicate text | edit_document + near_text | Picks the occurrence closest to the anchor. | | Create a doc from a template | instantiate_template | Fill placeholders, save as a new file. NO tracked changes in output — produces a clean final document. The template is untouched. Name output by content (e.g. "Acme Board Resolution 2026-04-25.docx"), not "_v2". | | Add a margin comment | add_comment | Handles 4-file OOXML coordination. | | Accept all tracked changes | accept_all_changes | LibreOffice-backed finalization. Pass output_path to save as a new file. | | Reject all tracked changes | reject_all_changes | LibreOffice-backed restoration. | | Accept SPECIFIC changes by ID | accept_changes | Per-revision accept (lawyer-style: "accept the indemnity edits, leave the IP open"). Pair with get_revision_stats(with_snippets=True) to pick IDs. | | Reject SPECIFIC changes by ID | reject_changes | Per-revision reject. Same workflow — get stats with snippets, pass the IDs you want to reject. | | Revert specific edits by ID | revert_edit | Surgical revert of edits we made (from the edit_document response). Functionally similar to reject_changes; use whichever framing fits the user's ask ("undo my edit" vs. "reject the counterparty's change"). | | Read tracked-change stats | get_revision_stats | Counts, authors, IDs. Pass with_snippets=True to also get per-revision text + context — required input for accept_changes / reject_changes. | | Compare two documents (agent-internal diff) | compare | Returns structured text diff with addition/deletion counts and similarity %. Use for "what changed between v1 and v2" reasoning. For a Word-openable redlined DOCX deliverable for the user, use produce_redline instead. | | Produce a redlined comparison DOCX (user-facing) | produce_redline | Word-openable DOCX with file2's changes shown as tracked changes against file1. LibreOffice-backed. For "show me what changed" deliverables. | | Clone before a big edit session | clone_document | Optional — edit_document auto-clones to _v2.docx on first edit. Use only to pick a non-default name. |
Rules:
edit_document CALL. Multiple changes → multiple calls.run_code for ordinary text edits — even placeholder fills like [●] are edit_document cases.Skill('docx-xml') for the run_code + lxml + zipfile reference.Skill('draft') instead — that skill uses docx-js for new-doc creation.read_document(path="Contract.docx") → get current text
edit_document(path="Contract.docx",
old_text="the laws of the State of Delaware",
new_text="the laws of England and Wales",
explanation="Change governing law")Auto-clones the original to Contract_v2.docx on first edit. Response includes:
revision_ids: list of w:id values for the tracked changes createdmatched_text: the text that was actually matchedcontext_around_edit: surrounding textedit_document(path="Contract_v2.docx",
start_text="8. NON-COMPETE", end_text="9. GOVERNING LAW",
explanation="Remove non-compete clause")edit_document(path="Contract_v2.docx", old_text="written notice", new_text="prior written notice",
near_text="termination",
explanation="Disambiguate to the termination notice clause only")revert_edit(path="Contract_v2.docx", revision_ids=[1001, 1002])get_revision_stats(path="Contract_v2.docx")Returns insertion/deletion counts, authors, revision IDs.
Two distinct shapes, distinct semantics:
| Call shape | Behavior | When to use | |---|---|---| | accept_all_changes(path="Contract_v2.docx", output_path="Contract_Clean.docx") | Produces clean deliverable at output_path; Contract_v2.docx stays editable with its tracked changes intact. | "Send me a clean version" — you may keep iterating on v2 after. | | accept_all_changes(path="Contract_v2.docx") (no output_path) | Mutates v2 in place, marks v2 finalized. Next edit on Contract.docx auto-clones to Contract_v3.docx (round-bump). | "We're done with this round — accept everything and move on." Subsequent edits start a fresh round. | | Same shapes apply to reject_all_changes. | | |
Both routed through LibreOffice — handles every OOXML edge case (nested changes, paragraph marks, table cells, content controls, comment anchors). Returns {success, path, remaining_insertions, remaining_deletions, round_finalized?} — both counts should be 0 on success. When round_finalized: true appears in the response, surface this to the user (e.g. "Round 1 finalized — next edit will start v3"). The selective accept_changes / reject_changes tools are intra-round micro-edits and do not trigger the round-bump.
instantiate_template(
template_path="Templates/Board_Resolution.docx",
output_path="Acme Board Resolution 2026-04-25.docx",
replacements={
"[Company Name]": "Acme Corporation",
"[Date]": "25 April 2026",
"[Resolution Number]": "2026-001",
},
)The template is untouched. The output is a clean final document with the template's formatting preserved (run properties, paragraph styles, headers, footers all intact).
Disambiguating repeated placeholders. If the same token appears multiple times in the template (e.g. a generic [●] or [___] marker showing up at many fill points), each replacement key must be a unique-in-document string — include surrounding context so the matcher targets the right span. Pattern:
replacements={
"Term of [●] months": "Term of 24 months",
"Cap of $[●]": "Cap of $5,000,000",
"Discount of [●] percent": "Discount of 20 percent",
}The longer key is unambiguous; identical short keys like [●] would only fill the first occurrence (or fail with ambiguity). For reviewable per-placeholder fills (rare — when each fill needs separate sign-off), use clone_document + edit_document per placeholder instead, then finalize with accept_all_changes.
get_revision_stats(path="Contract_v2.docx", with_snippets=True)
→ {revisions: [{id: 1003, type: "insertion", author: "Counterparty",
text_snippet: "...sole and absolute discretion...",
context_around: "...the Buyer in its sole and absolute..."},
...]}
accept_changes(path="Contract_v2.docx", revision_ids=[1003, 1005])
reject_changes(path="Contract_v2.docx", revision_ids=[1004])Use accept_changes / reject_changes when the lawyer wants per-revision control ("accept the indemnity edits, leave the IP edits open"). Use the _all_changes variants for batch finalization. Both auto-clone the original to _v2.docx on first call so the pristine source isn't mutated.
Every edit operation must include verification. Check success: true and inspect context_around_edit from the response. If the matched span looks wrong, call read_document(path=...) to confirm the current state before continuing.
Don't reach for run_code + python-docx for placeholder fills on existing DOCX. Use edit_document, one call per placeholder. python-docx's run-level .text.replace() subtly corrupts complex legal templates: it strips non-target glyphs (other placeholders, special characters), collapses <w:rPr> run properties across runs (loses bold/font/size), and has no reliable API for footnotes, bookmarks, TableOfContents, or internal hyperlinks.
Don't create a new DOCX in this skill. Creation belongs in the draft skill — invoke Skill('draft').
Don't write lxml-based accept/reject recipes in run_code. The accept_all_changes / reject_all_changes tools route through LibreOffice and handle every OOXML edge case correctly. Hand-rolled lxml accept/reject misses paragraph-mark + content-control edge cases and produces "unreadable content" dialogs in Word.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-06 | pass→pass | 19,312 | 25,107 | +30% | 1 | 1 | 0% | 3,608 | 7,437 | +106% | 0 | 0 | — |
case-01 | fail→fail | 16,522 | 5,087 | -69% | 1 | 1 | 0% | 3,456 | 2,906 | -16% | 0 | 0 | — |
case-02 | fail→fail | 19,716 | 6,622 | -66% | 1 | 1 | 0% | 2,917 | 2,972 | +2% | 0 | 0 | — |
case-03 | fail→fail | 10,840 | 7,294 | -33% | 1 | 1 | 0% | 1,065 | 2,820 | +165% | 0 | 0 | — |
case-04 | fail→pass | 48,988 | 3,993 | -92% | 1 | 1 | 0% | 6,173 | 2,691 | -56% | 0 | 0 | — |
case-05 | pass→pass | 14,170 | 4,576 | -68% | 1 | 1 | 0% | 2,981 | 2,726 | -9% | 0 | 0 | — |
case-07 | fail→fail | 8,343 | 7,573 | -9% | 1 | 1 | 0% | 1,578 | 2,791 | +77% | 0 | 0 | — |
case-08 | fail→fail | 12,549 | 6,798 | -46% | 1 | 1 | 0% | 594 | 2,794 | +370% | 0 | 0 | — |
case-09 | fail→fail | 4,399 | 5,268 | +20% | 1 | 1 | 0% | 815 | 2,740 | +236% | 0 | 0 | — |
case-10 | fail→fail | 13,849 | 6,721 | -51% | 1 | 1 | 0% | 2,782 | 2,896 | +4% | 0 | 0 | — |
case-11 | fail→fail | 6,827 | 6,562 | -4% | 1 | 1 | 0% | 434 | 2,805 | +546% | 0 | 0 | — |
case-17 | fail→fail | 9,468 | 4,849 | -49% | 1 | 1 | 0% | 1,924 | 2,706 | +41% | 0 | 0 | — |
case-12 | fail→fail | 6,700 | 5,392 | -20% | 1 | 1 | 0% | 1,152 | 2,709 | +135% | 0 | 0 | — |
case-13 | fail→fail | 8,725 | 5,708 | -35% | 1 | 1 | 0% | 542 | 2,841 | +424% | 0 | 0 | — |
case-14 | pass→pass | 9,854 | 2,881 | -71% | 1 | 1 | 0% | 1,682 | 2,970 | +77% | 0 | 0 | — |
case-15 | fail→fail | 6,111 | 5,114 | -16% | 1 | 1 | 0% | 1,095 | 2,775 | +153% | 0 | 0 | — |
case-16 | fail→fail | 11,064 | 8,768 | -21% | 1 | 1 | 0% | 1,971 | 3,111 | +58% | 0 | 0 | — |
case-18 | fail→fail | 10,563 | 5,951 | -44% | 1 | 1 | 0% | 1,438 | 2,869 | +100% | 0 | 0 | — |
case-19 | fail→pass | 5,668 | 2,509 | -56% | 1 | 1 | 0% | 791 | 2,843 | +259% | 0 | 0 | — |
case-20 | pass→fail | 5,440 | 8,261 | +52% | 1 | 1 | 0% | 1,053 | 2,816 | +167% | 0 | 0 | — |
case-21 | fail→pass | 17,220 | 6,209 | -64% | 1 | 1 | 0% | 2,095 | 3,160 | +51% | 0 | 0 | — |
case-22 | fail→fail | 11,321 | 4,839 | -57% | 1 | 1 | 0% | 2,169 | 2,694 | +24% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 6 counted toward the lift figure. The other 16 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +9 percentage points is the difference between those two pass rates over the 6 comparable cases. 4 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.