anthropics/skills · error · RefusedToClean

<p:sldIdLst> lists {len(listed)} slide(s) and none of the {l

Error message

<p:sldIdLst> lists {len(listed)} slide(s) and none of the {len(on_disk)} slide(s) on disk match any of them. Refusing to delete them all — this is a parse failure, not an empty deck.

What it means

remove_orphaned_slides() in pptx/scripts/clean.py cross-checks the slide files on disk (ppt/slides/slide*.xml) against the r:id list in ppt/presentation.xml's <p:sldIdLst>. If slides exist on disk but ZERO of them match the listed r:ids, the mismatch is almost certainly a parsing failure (relationship IDs resolved to different part names than expected), so it refuses to run rather than delete every slide. This is a safety interlock against data-destroying false positives.

Source

Thrown at skills/pptx/scripts/clean.py:86

    slides_dir = unpacked_dir / "ppt" / "slides"
    slides_rels_dir = slides_dir / "_rels"
    pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"

    if not slides_dir.exists():
        return []

    referenced_slides = get_slides_in_sldidlst(unpacked_dir)
    on_disk = sorted(slides_dir.glob("slide*.xml"))

    if on_disk and not any(s.name in referenced_slides for s in on_disk):
        listed = re.findall(
            r'<p:sldId[^>]*r:id="([^"]+)"',
            (unpacked_dir / "ppt" / "presentation.xml").read_text(encoding="utf-8")
            if (unpacked_dir / "ppt" / "presentation.xml").exists()
            else "",
        )
        if listed:
            raise RefusedToClean(
                f"<p:sldIdLst> lists {len(listed)} slide(s) and none of the "
                f"{len(on_disk)} slide(s) on disk match any of them. Refusing to "
                f"delete them all — this is a parse failure, not an empty deck."
            )

    removed = []

    for slide_file in on_disk:
        if slide_file.name not in referenced_slides:
            rel_path = slide_file.relative_to(unpacked_dir)
            slide_file.unlink()
            removed.append(str(rel_path))

            rels_file = slides_rels_dir / f"{slide_file.name}.rels"
            if rels_file.exists():
                rels_file.unlink()
                removed.append(str(rels_file.relative_to(unpacked_dir)))

View on GitHub (pinned to f6656c1256)

Solutions

  1. Do not force-clean: open the deck in PowerPoint/LibreOffice, re-save it, and retry — a conformant rewrite fixes rel/part naming.
  2. Compare the r:id list in ppt/presentation.xml <p:sldIdLst> with Targets in ppt/_rels/presentation.xml.rels to find why resolution mismatches (case, leading slash, subfolder).
  3. Check for a stale unpacked tree: re-unpack the pptx fresh before cleaning.
  4. If the deck is genuinely empty-of-references, verify manually and only then delete slides with an explicit custom step.

Example fix

# before
clean_unused_files(unpacked_dir)  # RefusedToClean on mismatched deck

# after: normalize by re-saving through LibreOffice first
subprocess.run(["soffice", "--headless", "--convert-to", "pptx", "--outdir", tmp, str(pptx_path)], check=True)
# re-unpack tmp/deck.pptx, then clean_unused_files(unpacked_dir)
Defensive patterns

Strategy: try-catch

Validate before calling

import re, zipfile
from pathlib import Path

def slide_lists_consistent(pptx_path: Path) -> bool:
    with zipfile.ZipFile(pptx_path) as zf:
        rels = zf.read("ppt/_rels/presentation.xml.rels").decode()
        pres = zf.read("ppt/presentation.xml").decode()
    rid_to_target = dict(re.findall(r'<Relationship[^>]*Id="([^"]+)"[^>]*Target="([^"]+)"', rels))
    rids = re.findall(r'<p:sldId[^>]*r:id="([^"]+)"', pres)
    targets = {rid_to_target.get(r, "").rsplit("/", 1)[-1] for r in rids}
    on_disk = {n.rsplit("/", 1)[-1] for n in zipfile.ZipFile(pptx_path).namelist() if re.match(r"ppt/slides/slide\d+\.xml$", n)}
    return bool(on_disk & targets) or not on_disk

Try / catch

try:
    clean_unused_files(unpacked_dir)
except RefusedToClean as e:
    log.warning("refusing to clean %s: %s — re-save deck and retry", pptx_path, e)
    subprocess.run(["soffice", "--headless", "--convert-to", "pptx", "--outdir", tmp, str(pptx_path)], check=True)
    # re-unpack converted file and retry once

Prevention

When it happens

Trigger: Cleaning a deck where get_slides_in_sldidlst() returns part names that share no overlap with the actual slideN.xml filenames — e.g. nonstandard part naming (slides/slide1.xml vs ppt/slides/slide1.xml resolution differences), rels using unusual targets, or presentation.xml r:ids that map through a remapped .rels file. Requires listed to be non-empty (sldIdLst parsed but nothing matched).

Common situations: Decks from third-party generators with unusual relationship structures; packages whose presentation.xml.rels targets use absolute or oddly-cased part names; partially corrupted decks after bad round-trips.

Related errors


AI-assisted analysis of anthropics/skills@f6656c1256 (2026-08-14). Data as JSON: /api/errors/822319dc63f6cd1c. Report an issue: GitHub.