How the Docx Skill Processes Tracked Changes at the XML Level
The docx skill validates tracked changes by parsing word/document.xml to detect author-specific <w:ins> and <w:del> elements, stripping those changes from both the original and modified XML trees, and comparing the resulting plain text for equivalence.
The anthropics/skills repository provides a specialized docx skill that ensures all modifications to Word documents are properly tracked at the XML level. When Microsoft Word's Track Changes feature is enabled, insertions and deletions are stored as specific XML elements within document.xml. The skill's validation logic, implemented in skills/docx/scripts/office/validators/redlining.py, processes these elements to verify that changes attributed to a specific author (defaulting to Claude) are correctly recorded and do not corrupt the document structure.
Understanding Tracked Changes in Word XML
Microsoft Word stores revision markup using the WordprocessingML namespace (w:). The docx skill targets two primary element types when processing tracked changes at the XML level.
Insertions and Deletions Structure
- Insertions are wrapped in
<w:ins>elements with an author attribute:<w:ins w:author="Claude">...</w:ins> - Deletions are wrapped in
<w:del>elements and contain<w:delText>nodes instead of standard<w:t>text nodes:<w:del w:author="Claude"><w:delText>deleted text</w:delText></w:del>
The RedliningValidator class specifically searches for these patterns using XPath queries with the w namespace to isolate changes made by the configured author.
The Redlining Validation Pipeline
The validation process follows a strict pipeline defined in the validate() method of RedliningValidator. This method orchestrates the comparison between an original baseline document and a modified version.
Parsing the Modified Document
The validator first locates the modified document.xml within the unpacked .docx directory structure:
modified_file = self.unpacked_dir / "word" / "document.xml"
tree = ET.parse(modified_file)
root = tree.getroot()
It then identifies all tracked change elements and filters for the target author:
del_elements = root.findall(".//w:del", self.namespaces)
ins_elements = root.findall(".//w:ins", self.namespaces)
author_del_elements = [e for e in del_elements
if e.get(f"{{{self.namespaces['w']}}}author") == self.author]
author_ins_elements = [e for e in ins_elements
if e.get(f"{{{self.namespaces['w']}}}author") == self.author]
If no elements match the author, validation passes immediately.
Loading the Original Baseline
To establish a comparison baseline, the validator unpacks the original .docx file into a temporary directory:
with tempfile.TemporaryDirectory() as temp_dir:
with zipfile.ZipFile(self.original_docx, "r") as zip_ref:
zip_ref.extractall(temp_path)
original_file = temp_path / "word" / "document.xml"
original_tree = ET.parse(original_file)
original_root = original_tree.getroot()
This provides the pre-modification XML tree for comparison.
Removing Author-Specific Markup
The core validation logic removes the author's tracked changes from both the original and modified trees. This ensures that if the author made identical changes to both documents (or if the changes are properly isolated), the resulting text will match.
self._remove_author_tracked_changes(original_root)
self._remove_author_tracked_changes(modified_root)
XML Transformation Logic
The _remove_author_tracked_changes() method implements specific rules for handling insertions versus deletions at the XML level.
Handling Insertions
Insertions by the target author are removed entirely from the tree. The method identifies <w:ins> elements with matching author attributes and removes them from their parent nodes:
for parent in root.iter():
to_remove = []
for child in parent:
if child.tag == ins_tag and child.get(author_attr) == self.author:
to_remove.append(child)
for elem in to_remove:
parent.remove(elem)
This effectively strips all inserted content by the specified author from the document.
Normalizing Deletions
Deletions require more complex handling because the deleted text must be preserved as normal content for comparison purposes. The method performs deletion normalization:
- Converts
<w:delText>elements to standard<w:t>elements - Lifts the children of the
<w:del>element into the parent paragraph - Removes the empty
<w:del>wrapper
for del_elem, del_index in reversed(to_process):
for elem in del_elem.iter():
if elem.tag == deltext_tag:
elem.tag = t_tag # <w:delText> → <w:t>
for child in reversed(list(del_elem)):
parent.insert(del_index, child) # lift children up
parent.remove(del_elem)
This ensures that deleted text appears as regular content in the extracted plain text, allowing the validator to verify that deletions were properly tracked rather than permanently removed.
Text Extraction and Comparison
After XML transformation, the validator extracts plain text from both trees using _extract_text_content().
def _extract_text_content(self, root):
p_tag = f"{{{self.namespaces['w']}}}p"
t_tag = f"{{{self.namespaces['w']}}}t"
paragraphs = []
for p_elem in root.findall(f".//{p_tag}"):
text_parts = [t_elem.text for t_elem in p_elem.findall(f".//{t_tag}") if t_elem.text]
paragraph_text = "".join(text_parts)
if paragraph_text:
paragraphs.append(paragraph_text)
return "\n".join(paragraphs)
This method concatenates all <w:t> text runs within each <w:p> paragraph element, joining paragraphs with newline characters to create a normalized text representation.
The final comparison occurs in validate():
modified_text = self._extract_text_content(modified_root)
original_text = self._extract_text_content(original_root)
if modified_text != original_text:
error_message = self._generate_detailed_diff(original_text, modified_text)
print(error_message)
return False
When texts differ, _generate_detailed_diff() invokes _get_git_word_diff() to produce a word-level diff using Git's --word-diff=plain option, highlighting exactly where the untracked modifications occurred.
Command-Line Usage Examples
The RedliningValidator integrates into the docx skill's validation CLI via skills/docx/scripts/office/validate.py.
Validating Against an Original Document
python -m skills.docx.scripts.office.validate \
./modified_document.docx \
--original ./original_document.docx \
--author Claude \
--verbose
This command unpacks both documents, runs the redlining validator to ensure all changes by "Claude" are properly tracked, and outputs a detailed diff if validation fails.
Programmatic Validation
from pathlib import Path
from skills.docx.scripts.office.validators.redlining import RedliningValidator
validator = RedliningValidator(
unpacked_dir=Path("./unpacked_modified"),
original_docx=Path("./original.docx"),
author="Alice",
verbose=True
)
if validator.validate():
print("All tracked changes validated successfully")
else:
print("Validation failed - see diff output above")
Summary
- The docx skill processes tracked changes by parsing
word/document.xmlto identify<w:ins>and<w:del>elements attributed to a specific author. - Insertion handling removes
<w:ins>elements entirely when they match the target author, effectively stripping that content from the comparison text. - Deletion normalization converts
<w:delText>nodes to standard<w:t>elements and lifts them out of<w:del>wrappers, preserving deleted text as regular content for comparison. - The validator compares plain-text extractions from both the original and modified documents after XML transformation, using
git diff --word-diffto highlight any discrepancies indicating untracked changes. - All logic resides in
skills/docx/scripts/office/validators/redlining.pyand integrates with the CLI viaskills/docx/scripts/office/validate.py.
Frequently Asked Questions
How does the docx skill identify which tracked changes belong to a specific author?
The RedliningValidator filters XML elements by checking the w:author attribute against the configured author name (defaulting to Claude). It constructs the full attribute name using the WordprocessingML namespace and compares it to self.author for every <w:ins> and <w:del> element found in word/document.xml.
What happens to deletions when the validator removes tracked changes from the XML?
Rather than removing the content entirely, the validator normalizes deletions by converting <w:delText> elements to standard <w:t> text nodes and lifting them out of the <w:del> wrapper into the parent paragraph. This preserves the deleted text as regular content, allowing the validator to verify that the deletion was properly tracked rather than permanently removed from the document.
Can I use the redlining validator to check changes made by authors other than Claude?
Yes. The validator accepts an author parameter that can be set to any string matching the w:author attribute in the Word XML. When using the command-line interface, pass --author "Alice" or any other name to validate tracked changes attributed to that specific user instead of the default Claude value.
Why does the validator compare plain text instead of comparing the XML directly?
The validator extracts plain text from both the original and modified documents after stripping the target author's tracked changes because Word documents can contain irrelevant XML differences (such as different relationship IDs, timestamps, or formatting variations) that do not affect the actual document content. By normalizing both trees to plain text paragraphs, the validator ensures it is comparing semantic content rather than XML serialization artifacts, using git diff --word-diff to pinpoint exactly where untracked modifications occurred.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →