cd /news/developer-tools/exit-codes-lie-when-pdf-extraction-y… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-104782] src=promptcube3.com β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

Exit codes lie when PDF extraction yields nothing

A developer's integration with Claude Code and markitdown falsely reported a PDF as empty because exit codes only indicate process completion, not content yield, and byte thresholds miss near-misses like a 39-character certificate. The author proposes a two-layer check combining structural validation and a density heuristic of 50 characters per page, plus sample verification with a cheap model, to catch extraction failures in any pipeline.

read3 min views2 publishedAug 20, 2026
Exit codes lie when PDF extraction yields nothing
Image: Promptcube3 (auto-discovered)

Claude Codecheerfully told me the document was empty. The PDF wasn't empty β€” it was a browser screenshot export with zero text layer. Every tool in the chain did exactly what it was designed to do, reported success, and the stacked successes produced a lie I believed.

The real failure point #

markitdown isn't broken. It extracts embedded text and deliberately doesn't OCR β€” documented design decision. pdfminer

and PyMuPDF both report 0 characters. The converter exiting non-zero on empty documents would be wrong in a more annoying way.

The bug lives in every integration that treats exit code as evidence of yield. Exit code answers "did the process complete?" I read it as "did we get the text?" Different questions, different answers for scanned pages.

$ markitdown screenshot.pdf -o out.md
$ echo $?
0
$ wc -c out.md
0 out.md

My integration checked the return code, saw success, cached the result, handed the model a path to an empty file. The model read nothing and concluded the document was empty. Reasonable from its position.

Why byte thresholds fail #

The obvious fix β€” reject anything under 500 bytes β€” catches the screenshot. It misses the case that actually cost me time: a course completion certificate. One decorative graphic, one title line as real text. Conversion yields:

Certificate of Completion

Thirty-nine characters. Sails through emptiness checks, gets cached as successful, and the model reports your certificate says "Certificate of Completion" and nothing else.

Near-misses are the expensive failures: slide decks exported as page images with footers, contracts scanned at an angle with headers that OCR'd once. They all return some characters.

What actually works #

I've settled on a two-layer check that catches both failure modes:

1. Structural validation β€” does the output contain paragraph-like structures, not just isolated lines? A real document has sentences, line breaks, recurring patterns. A certificate has one line.

2. Density heuristic β€” characters per page. If a 10-page PDF yields 200 characters total, extraction failed regardless of exit code. Threshold varies by domain (legal > technical > certificates), but 50 chars/page is a starting baseline.

def extraction_quality(text: str, page_count: int) -> bool:
    if len(text) < 50 * page_count:
        return False
    paragraphs = [p for p in text.split('\n\n') if len(p.strip()) > 20]
    return len(paragraphs) >= max(2, page_count // 3)

3. Sample verification β€” feed the first 500 chars to a cheap model with a strict prompt: "Does this look like meaningful document content or extraction artifacts?" Costs pennies, catches the certificate case every time.

The pattern generalizes #

This isn't PDF-specific. Any pipeline where a transformer can succeed while producing useless output has this shape: OCR, speech-to-text, HTML-to-markdown, code transpilers. The fix is always the same β€” measure yield, not completion.

What's your threshold strategy? I've seen teams use word count, unique word ratio, even embedding distance from a "garbage" centroid. Curious what's worked in production.

Next Hard Gates Beat Long Prompts β†’

All Replies (3οΌ‰ #

pdftotext

returns 0 but outputs nothing unless you force OCR first

── more in #developer-tools 4 stories Β· sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/exit-codes-lie-when-…] indexed:0 read:3min 2026-08-20 Β· β€”