Back to Blog
Technical

Automating Excel Metadata Removal in Your Workflow

Manual metadata cleanup is error-prone and unsustainable. Every time someone forgets to run Document Inspector before sharing a spreadsheet, your organization risks leaking author names, internal file paths, revision history, and hidden content. The solution is automation—building metadata removal into your existing workflows so that files are cleaned consistently, reliably, and without human intervention.

By Technical TeamFebruary 28, 202622 min read

Why Manual Metadata Removal Fails

Excel's built-in Document Inspector works well for one-off cleanup, but it has fundamental limitations in a team environment. It requires every person to remember to run it, it cannot be enforced across a department, and it offers no audit trail to prove that cleanup actually happened. When you send hundreds of spreadsheets per month—to clients, regulators, partners, or the public—manual cleanup simply cannot scale.

Automation solves this by intercepting files at key points in your workflow: when they are saved to a shared drive, committed to version control, uploaded to a portal, or attached to an email. The goal is to make metadata removal invisible and automatic, so that clean files become the default rather than the exception.

The Cost of Forgetting

What Leaks Without Automation

  • • Author names and email addresses from core.xml
  • • Company name and department from app.xml
  • • Internal file paths from printer settings
  • • Revision history and total editing time

Real-World Consequences

  • • Competitor discovers your internal org structure
  • • Client sees the original author was a junior analyst
  • • Hidden sheets with draft pricing are exposed
  • • GDPR violation from personal data in metadata

Understanding What Needs to Be Removed

Before building automation, you need to know exactly which metadata fields to target. XLSX files store metadata across multiple XML files inside the ZIP archive. A thorough cleanup script must address all of these locations, not just the obvious document properties.

docProps/core.xml — Identity and Timestamps

Fields to Clear

  • dc:creator — original author name
  • cp:lastModifiedBy — last editor name
  • cp:lastPrinted — last print timestamp
  • cp:revision — revision count

Fields to Consider

  • dcterms:created — creation date (may need to keep)
  • dcterms:modified — modification date
  • dc:description — document comments
  • cp:keywords — document tags

docProps/app.xml — Application Fingerprint

  • Company — organization name embedded by IT policy
  • Manager — manager name field
  • TotalTime — total editing minutes (reveals effort level)
  • Application and AppVersion — software fingerprint (usually safe to keep)

Additional Metadata Locations

  • docProps/custom.xml — DLP labels, SharePoint IDs, classification tags
  • xl/printerSettings/ — printer names, network paths, hardware fingerprints
  • xl/comments*.xml — cell comments with author attribution
  • Hidden/veryHidden sheets — sheets not visible in the UI but still present in the archive
  • xl/sharedStrings.xml — orphaned text from deleted cells

Approach 1: Python with openpyxl

Python's openpyxl library provides the most flexible approach to metadata removal. It can read and write XLSX files natively, giving you fine-grained control over every document property. This is the best choice for cross-platform workflows, CI/CD pipelines, and server-side processing.

clean_metadata.py

# pip install openpyxl

# Usage: python clean_metadata.py report.xlsx

 

import sys

import os

from pathlib import Path

from openpyxl import load_workbook

 

def clean_metadata(filepath: str) -> None:

    """Remove identifying metadata from an XLSX file."""

    wb = load_workbook(filepath)

    props = wb.properties

 

    # Clear identity fields

    props.creator = ""

    props.lastModifiedBy = ""

    props.last_printed = None

 

    # Clear descriptive fields

    props.title = ""

    props.subject = ""

    props.description = ""

    props.keywords = ""

    props.category = ""

 

    # Clear revision tracking

    props.revision = None

 

    # Remove comments from all cells

    for sheet in wb.worksheets:

        for row in sheet.iter_rows():

            for cell in row:

                cell.comment = None

 

    wb.save(filepath)

    print(f"Cleaned: {filepath}")

 

if __name__ == "__main__":

    for arg in sys.argv[1:]:

        for path in Path(".").glob(arg):

            if path.suffix.lower() == ".xlsx":

                clean_metadata(str(path))

Limitations of openpyxl

While openpyxl handles core and extended properties well, it has blind spots that you need to address separately:

  • Printer settings: openpyxl does not expose or remove xl/printerSettings/ binary files. You need to remove these at the ZIP level.
  • Custom XML properties: DLP labels and SharePoint metadata in docProps/custom.xml require separate handling.
  • Shared strings: Orphaned entries in sharedStrings.xml are not automatically cleaned.
  • Macros: openpyxl does not handle .xlsm files. Use xlsxwriter or the ZIP-level approach for macro-enabled workbooks.

Approach 2: Deep Clean with ZIP-Level Manipulation

For a thorough cleanup that reaches every metadata location—including printer settings, custom properties, and binary artifacts—you need to manipulate the XLSX file at the ZIP archive level. This approach treats the file as what it truly is: a ZIP container of XML files that can be individually modified or removed.

deep_clean.py

import zipfile

import io

import re

import sys

from lxml import etree

 

# Fields to blank in core.xml

CORE_CLEAR = {

    "{http://purl.org/dc/elements/1.1/}creator",

    "{http://schemas.openxmlformats.org/package/2006/metadata/core-properties}lastModifiedBy",

    "{http://schemas.openxmlformats.org/package/2006/metadata/core-properties}lastPrinted",

    "{http://schemas.openxmlformats.org/package/2006/metadata/core-properties}revision",

}

 

# Files to remove entirely

REMOVE_PATTERNS = [

    r"xl/printerSettings/.*",

    r"docProps/custom\.xml",

]

 

def should_remove(name: str) -> bool:

    return any(re.match(p, name) for p in REMOVE_PATTERNS)

 

def clean_core_xml(xml_bytes: bytes) -> bytes:

    tree = etree.parse(io.BytesIO(xml_bytes))

    root = tree.getroot()

    for el in root:

        if el.tag in CORE_CLEAR:

            el.text = ""

    return etree.tostring(tree, xml_declaration=True,

                          encoding="UTF-8", standalone=True)

 

def clean_app_xml(xml_bytes: bytes) -> bytes:

    tree = etree.parse(io.BytesIO(xml_bytes))

    root = tree.getroot()

    ns = {"ep": "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"}

    for tag in ["Company", "Manager", "TotalTime"]:

        el = root.find(f"ep:{tag}", ns)

        if el is not None:

            el.text = ""

    return etree.tostring(tree, xml_declaration=True,

                          encoding="UTF-8", standalone=True)

 

def deep_clean(src: str, dst: str) -> None:

    with zipfile.ZipFile(src, "r") as zin, \

         zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:

        for item in zin.infolist():

            if should_remove(item.filename):

                continue

            data = zin.read(item.filename)

            if item.filename == "docProps/core.xml":

                data = clean_core_xml(data)

            elif item.filename == "docProps/app.xml":

                data = clean_app_xml(data)

            zout.writestr(item, data)

 

if __name__ == "__main__":

    src = sys.argv[1]

    dst = src.replace(".xlsx", "_clean.xlsx")

    deep_clean(src, dst)

    print(f"Cleaned: {src} → {dst}")

Why ZIP-Level Manipulation Is More Thorough

  • Removes binary files: Printer settings and embedded objects that openpyxl cannot touch
  • Removes custom XML: DLP labels, SharePoint metadata, and third-party tool data
  • Controls ZIP metadata: You can normalize ZIP entry timestamps to prevent leaking modification patterns
  • Works with any OOXML file: The same technique applies to DOCX and PPTX files with minor adjustments

Approach 3: PowerShell for Windows Environments

In Windows-centric organizations, PowerShell provides a natural integration point. You can use the COM automation interface to drive Excel directly, which gives you access to the native Document Inspector functionality—the same engine that powers the manual cleanup in the Excel UI.

Clean-ExcelMetadata.ps1

# Requires Excel installed on the machine

param(

    [Parameter(Mandatory)]

    [string]$Path

)

 

$excel = New-Object -ComObject Excel.Application

$excel.Visible = $false

$excel.DisplayAlerts = $false

 

$fullPath = (Resolve-Path $Path).Path

$wb = $excel.Workbooks.Open($fullPath)

 

# Clear built-in properties

$props = $wb.BuiltinDocumentProperties

$clearFields = @("Author", "Last Author",

    "Manager", "Company", "Subject",

    "Title", "Keywords", "Comments",

    "Category")

 

foreach ($field in $clearFields) {

    try {

        $props.Item($field).Value = ""

    } catch { # property may not exist }

}

 

# Remove all custom properties

$custom = $wb.CustomDocumentProperties

for ($i = $custom.Count; $i -ge 1; $i--) {

    $custom.Item($i).Delete()

}

 

# Remove all comments

foreach ($ws in $wb.Worksheets) {

    $ws.Cells.ClearComments()

}

 

$wb.Save()

$wb.Close()

$excel.Quit()

[System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel) | Out-Null

 

Write-Host "Cleaned: $fullPath"

Advantages

  • • Uses Excel's own engine for maximum compatibility
  • • Handles all file formats (.xlsx, .xlsm, .xlsb)
  • • Can invoke Document Inspector programmatically
  • • Integrates with Windows Task Scheduler and Group Policy

Limitations

  • • Requires Excel to be installed (no headless servers)
  • • COM automation is slow for large batches
  • • Process can hang if Excel prompts for user input
  • • Windows-only (no macOS/Linux support)

Batch Processing: Cleaning Entire Directories

Real-world workflows rarely involve a single file. You need to process entire folders of spreadsheets—shared drives, export directories, or outgoing file queues. Here's how to wrap the Python deep-clean approach into a batch processor with logging and error handling.

batch_clean.py

import logging

from pathlib import Path

from datetime import datetime

from deep_clean import deep_clean

 

logging.basicConfig(

    filename=f"metadata_cleanup_{datetime.now():%Y%m%d}.log",

    level=logging.INFO,

    format="%(asctime)s %(levelname)s %(message)s"

)

 

def batch_clean(directory: str, in_place: bool = False) -> dict:

    stats = {"cleaned": 0, "skipped": 0, "errors": 0}

 

    for xlsx in Path(directory).rglob("*.xlsx"):

        if xlsx.name.startswith("~$"): # skip temp files

            stats["skipped"] += 1

            continue

        try:

            dst = str(xlsx) if in_place else \

                str(xlsx).replace(".xlsx", "_clean.xlsx")

            deep_clean(str(xlsx), dst)

            logging.info(f"OK {xlsx}")

            stats["cleaned"] += 1

        except Exception as e:

            logging.error(f"FAIL {xlsx}: {e}")

            stats["errors"] += 1

 

    logging.info(f"Summary: {stats}")

    return stats

Batch Processing Best Practices

Safety

  • • Always keep originals until cleanup is verified
  • • Skip temp files (~$ prefix) and lock files
  • • Log every operation for audit compliance
  • • Set file permissions to read-only after cleaning

Performance

  • • Use concurrent.futures for parallel processing
  • • Process files in batches to manage memory
  • • Skip files that haven't changed since last clean
  • • Monitor disk space for the output directory

Integration: Git Pre-Commit Hook

If your team stores Excel files in Git repositories—common for configuration files, test fixtures, data templates, and documentation—a pre-commit hook ensures that no file with sensitive metadata ever reaches the repository. This is one of the most effective integration points because it catches metadata at the last possible moment before it becomes part of the permanent history.

.git/hooks/pre-commit (or use pre-commit framework)

#!/usr/bin/env bash

# Pre-commit hook: reject XLSX files with metadata

 

STAGED=$(git diff --cached --name-only --diff-filter=ACM \

    | grep -iE '\.xlsx$')

 

if [ -z "$STAGED" ]; then

    exit 0 # No Excel files staged

fi

 

FAIL=0

for file in $STAGED; do

    # Extract and check core.xml for author data

    CREATOR=$(unzip -p "$file" docProps/core.xml 2>/dev/null \

        | grep -oP '<dc:creator>\K[^<]+')

 

    if [ -n "$CREATOR" ]; then

        echo "BLOCKED: $file contains author: $CREATOR"

        FAIL=1

    fi

 

    # Check for company name in app.xml

    COMPANY=$(unzip -p "$file" docProps/app.xml 2>/dev/null \

        | grep -oP '<Company>\K[^<]+')

 

    if [ -n "$COMPANY" ]; then

        echo "BLOCKED: $file contains company: $COMPANY"

        FAIL=1

    fi

done

 

if [ $FAIL -ne 0 ]; then

    echo ""

    echo "Run: python clean_metadata.py <file>"

    echo "Then re-stage and commit."

    exit 1

fi

Using the pre-commit Framework

For teams already using the pre-commit framework, you can define a reusable hook configuration:

# .pre-commit-config.yaml

repos:

  - repo: local

    hooks:

      - id: clean-excel-metadata

        name: Clean Excel Metadata

        entry: python clean_metadata.py

        language: python

        types: [file]

        files: \\.xlsx$

        additional_dependencies: [openpyxl]

Integration: CI/CD Pipeline

CI/CD pipelines provide the strongest enforcement point because they run on a centralized server that team members cannot bypass. Whether you use GitHub Actions, GitLab CI, Jenkins, or Azure DevOps, the pattern is the same: add a step that scans for metadata and fails the build if sensitive data is found.

.github/workflows/metadata-check.yml

name: Excel Metadata Check

on: [push, pull_request]

 

jobs:

  check-metadata:

    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5

        with:

          python-version: '3.12'

 

      - name: Install dependencies

        run: pip install openpyxl lxml

 

      - name: Scan for metadata

        run: |

          python -c "

          import glob, sys

          from openpyxl import load_workbook

          failed = False

          for f in glob.glob('**/*.xlsx', recursive=True):

            wb = load_workbook(f)

            p = wb.properties

            if p.creator:

              print(f'FAIL {f}: creator={p.creator}')

              failed = True

            if p.lastModifiedBy:

              print(f'FAIL {f}: lastModifiedBy={p.lastModifiedBy}')

              failed = True

          sys.exit(1 if failed else 0)

          "

Block on Detection

Fail the pipeline if any XLSX file contains author names, company information, or other sensitive properties. Force developers to clean files before merging.

Auto-Clean on Build

Automatically strip metadata during the build step, so cleaned files are always what gets deployed or packaged, regardless of what was committed.

Report and Warn

Generate a metadata audit report as a build artifact. Useful for compliance teams who need to review what metadata was found and verify it was removed.

Integration: File System Watcher

For organizations that share Excel files through network drives, SharePoint, or cloud-synced folders, a file system watcher provides real-time cleanup. Whenever a new XLSX file appears in a monitored directory, the watcher automatically strips its metadata before anyone can access it.

watch_and_clean.py

# pip install watchdog openpyxl

 

import time

from watchdog.observers import Observer

from watchdog.events import FileSystemEventHandler

from clean_metadata import clean_metadata

 

class ExcelHandler(FileSystemEventHandler):

    def on_created(self, event):

        if event.src_path.endswith(".xlsx") \

            and not event.is_directory:

            time.sleep(1) # wait for write to finish

            try:

                clean_metadata(event.src_path)

            except Exception as e:

                print(f"Error: {e}")

 

observer = Observer()

observer.schedule(ExcelHandler(), "/shared/outgoing",

                 recursive=True)

observer.start()

print("Watching /shared/outgoing for new Excel files...")

 

try:

    while True:

        time.sleep(1)

except KeyboardInterrupt:

    observer.stop()

observer.join()

Production Considerations

  • File locking: Wait for the file to be fully written before processing. Check that the file size has stabilized or use retry logic with exponential backoff.
  • Concurrent access: If multiple users drop files simultaneously, ensure your cleaner handles concurrent modifications safely.
  • Service management: Run the watcher as a systemd service (Linux) or Windows Service for automatic restart and logging.
  • Monitoring: Send alerts when cleanup fails so that uncleaned files don't slip through unnoticed.

Verifying That Cleanup Actually Worked

Automation is only valuable if you can prove it works. Every cleanup pipeline needs a verification step that confirms metadata was actually removed. This is especially important for compliance requirements where you need auditable proof of data sanitization.

verify_clean.py

from openpyxl import load_workbook

import zipfile

import sys

 

def verify(filepath: str) -> list[str]:

    """Return a list of metadata issues found."""

    issues = []

 

    # Check document properties

    wb = load_workbook(filepath)

    p = wb.properties

    if p.creator:

        issues.append(f"creator: {p.creator}")

    if p.lastModifiedBy:

        issues.append(f"lastModifiedBy: {p.lastModifiedBy}")

    if p.title:

        issues.append(f"title: {p.title}")

 

    # Check for printer settings and custom XML

    with zipfile.ZipFile(filepath) as zf:

        for name in zf.namelist():

            if "printerSettings" in name:

                issues.append(f"printer settings: {name}")

            if name == "docProps/custom.xml":

                issues.append("custom properties present")

 

    return issues

 

if __name__ == "__main__":

    issues = verify(sys.argv[1])

    if issues:

        print("FAIL - metadata found:")

        for i in issues:

            print(f" - {i}")

        sys.exit(1)

    else:

        print("PASS - no metadata found")

Choosing the Right Integration Point

The best integration point depends on your workflow, team size, and compliance requirements. Most organizations benefit from combining multiple approaches for defense in depth.

Git Pre-Commit Hook

Best for: Development teams that store Excel files in repositories. Catches metadata at the point of commit, before it enters version history.

Limitation: Only covers files committed to Git. Doesn't protect files shared via email, Slack, or file servers.

CI/CD Pipeline

Best for: Automated deployments where Excel files are packaged into releases, portals, or customer-facing downloads. Provides centralized enforcement that cannot be bypassed.

Limitation: Only runs during build/deploy cycles. Files shared outside the pipeline are not covered.

File System Watcher

Best for: Shared network drives and outgoing file directories where non-technical users drop files for external distribution.

Limitation: Requires a persistent process running on the file server. Race conditions can occur with rapidly created files.

Scheduled Batch Job

Best for: Periodic sweeps of shared drives to catch files that slipped through other controls. Acts as a safety net.

Limitation: Not real-time. Files may be accessed by others between creation and the next scheduled run.

Recommended: Defense in Depth

Combine multiple integration points for comprehensive coverage:

  1. Pre-commit hook catches files entering version control
  2. CI/CD check enforces policy on all builds and releases
  3. File watcher protects shared drives in real-time
  4. Scheduled batch job sweeps for anything that slipped through

Common Pitfalls and How to Avoid Them

Pitfall 1: Only Cleaning core.xml

Many scripts only target dc:creator and cp:lastModifiedBy, leaving company names in app.xml, network printer paths in binary files, and DLP labels in custom.xml. Your script must address all metadata locations to be effective.

Pitfall 2: Breaking File Integrity

When removing files from the ZIP archive (like printer settings), you must also update the [Content_Types].xml and relationship files that reference them. Leaving broken references can cause Excel to display repair warnings or silently discard data when opening the file.

Pitfall 3: Ignoring Cell Comments

Cell comments carry author names that are separate from the document-level properties. A file can have a blank dc:creator but still contain dozens of comments attributed to named individuals. Always iterate through worksheets and clear comments as part of your cleanup.

Pitfall 4: Not Testing with Excel

Always open cleaned files in Microsoft Excel after processing to verify they work correctly. Python libraries and ZIP manipulation can produce files that are technically valid XML but trigger Excel's repair mechanism, which may alter formatting or data. Test with representative samples before deploying to production.

Key Takeaways

Automate, Don't Rely on People

Manual metadata cleanup fails at scale. Build removal into your workflow at integration points like Git hooks, CI/CD pipelines, and file system watchers so that clean files become the default.

Clean All Metadata Locations

Metadata lives in core.xml, app.xml, custom.xml, printer settings, cell comments, and more. A thorough script must address every location, not just document properties.

Layer Your Defenses

No single integration point covers every file-sharing scenario. Combine pre-commit hooks, CI/CD checks, file watchers, and scheduled batch jobs for comprehensive protection.

Verify and Log Everything

Always run a verification step after cleanup. Log every operation for audit compliance. Automated cleanup without verification is just automated hope.

See What Metadata Your Excel Files Contain

Before building your automation pipeline, use MetaData Analyzer to audit the metadata in your existing files. Understand exactly what needs to be cleaned—author names, company information, hidden sheets, comments, and more—then automate with confidence.