Claude reads PDFs natively. The Read tool takes a PDF file path and a pages parameter and returns what the model actually sees — rendered pages, layout, scanned text, signatures, stamps, handwriting. It’s a vision-based read; the model treats each page as an image with overlaid text.

That’s the foundation aaicase is built on. Don’t convert PDFs to text. Don’t extract layout. Don’t pre-process. Read the PDF as the document.

The rule was documented in four places:

Each one said some variation of: “NEVER use pdf-parse, pdftotext, pdf2json, pdftoppm, or any text-extraction library. PDFs go through the Read tool.”

The rule was clear. But nothing enforced it. If a future commit imported pdf-parse or shelled out to pdftotext, the build would succeed, the change would ship, and the model would silently start processing text rather than vision — losing layout, missing scanned content, producing different classifications than what the model actually sees during the rest of its work.

The check

New scripts/check-no-pdf-extraction.mjs. Banned token list:

const BANNED = [
  { needle: "pdf-parse",   why: "PDF text extraction (forbidden)" },
  { needle: "pdftotext",   why: "PDF text extraction (forbidden)" },
  { needle: "pdf2json",    why: "PDF text extraction (forbidden)" },
  { needle: "pdftoppm",    why: "PDF rasterization (forbidden)" },
  { needle: "pdfimages",   why: "PDF image extraction (forbidden)" },
  { needle: "pdf.js",      why: "PDF text extraction library (forbidden)" },
  { needle: "pdfjs-dist",  why: "PDF text extraction library (forbidden)" },
  { needle: "mupdf",       why: "PDF library (forbidden)" },
  { needle: "mutool",      why: "mupdf CLI (forbidden)" },
  { needle: "tesseract",   why: "OCR (forbidden — vision-based read)" },
  { needle: "pdfsandwich", why: "PDF OCR pipeline (forbidden)" },
  { needle: "ocrmypdf",    why: "PDF OCR pipeline (forbidden)" },
];

Allowlist: documentation files that mention these tokens to forbid them. The system prompt has to be able to say “don’t use X” without tripping the check.

Wired into npm run check, which the deploy script runs before packing.

What it caught immediately

First run. Two violations in bin/aaicase.mjs:

if (/pdftotext/.test(cmd)) return "Extracting PDF text...";
if (/pdfseparate/.test(cmd)) return "Splitting PDF...";

These were status-line labels — strings the CLI would display if the running command matched a regex. The intent: if the model ever shelled out to pdftotext, surface a friendly label.

The label’s existence implied running pdftotext was a normal occurrence the CLI was prepared for. That’s exactly the kind of casual normalization the architecture forbids. The presence of the label is itself the bug — even if the code path never fires.

The fix was deleting both labels. The CLI no longer has a friendly name for pdftotext. If the model ever shells out to one, the status line will fall through to a generic label and the unusual operation stands out.

The trade-off

Why a CI guard instead of a runtime guard?

The runtime guard (refuse to run if the model tries to invoke pdftotext) is more defensive but also more fragile. The model might shell out via a less-obvious wrapper (a Python script that calls pdftotext), or via direct Node import of pdf-parse buried in a dependency. Catching that at runtime requires either intercepting all child_process spawns or scanning every imported module — both expensive and easy to circumvent.

The CI guard is static. It runs against the source tree. Every commit goes through it. Any future change that introduces a banned import or invocation fails the build before it can ship. Less defensive at runtime, more thorough at build time — and you can’t deploy a bypass without seeing it in the diff.

Three CI guards now

v4.5.130-132 added the PDF extraction check. The shipped CI suite for aaicase now runs three:

  1. check-no-content-regex — no regex on content-bearing fields. Tag/role classification has to go through Claude.
  2. check-no-firm-data — no identifying firm/staff/client data in shipped files (v4.5.122-123).
  3. check-no-pdf-extraction — no PDF text-extraction libraries (v4.5.130-132).

Each one started as a documented rule that was enforced by discipline. Each one now has a build-time check that prevents regression. The pattern: rules degrade. Checks don’t.