garrytan/gstack · warning · StrictModeError

image not found: ${src} (resolved to ${filePath})

Error message

image not found: ${src} (resolved to ${filePath})

What it means

StrictModeError thrown by inlineLocalImages() when a local image src resolves to a filePath that does not exist on disk (fs.existsSync is false), under --strict. In non-strict mode the same condition produces a warn and a missing-image placeholder; strict makes it fatal so broken image links fail the build rather than shipping a degraded PDF.

Source

Thrown at make-pdf/src/diagram-prepass.ts:635

    // decodeURIComponent throws on malformed escapes (foo%zz.png) — a broken
    // URL must degrade to the missing-image path, not crash the run.
    let decodedSrc = src;
    try {
      decodedSrc = decodeURIComponent(src);
    } catch { /* keep raw src */ }

    const filePath = src.startsWith("file:")
      ? fileURLToPath(src)
      : isDrivePath
        ? path.resolve(src)
        : path.resolve(opts.inputDir, decodedSrc);

    const cached = memo.get(filePath);
    if (cached !== undefined) return rewriteImgTag(tag, cached);

    if (!fs.existsSync(filePath)) {
      const msg = `image not found: ${src} (resolved to ${filePath})`;
      if (opts.strict) throw new StrictModeError(msg);
      opts.warn(msg);
      return buildMissingImagePlaceholder(src);
    }

    // Out-of-tree reads are legal (local CLI semantics — like pandoc) but
    // never silent: an agent PDF-ing untrusted markdown should not quietly
    // embed ~/.ssh/config into a shareable document. --strict makes it fatal.
    // Compare REAL paths — a symlink inside the input dir pointing outside
    // would otherwise pass a string-prefix check (Codex adversarial finding).
    // Runs after the existence check: realpath of a missing file can't
    // resolve, and on macOS /var vs /private/var would false-positive.
    const inputRoot = safeRealpath(path.resolve(opts.inputDir)) + path.sep;
    const realFilePath = safeRealpath(filePath);
    if (!realFilePath.startsWith(inputRoot)) {
      const msg = `image resolves OUTSIDE the input directory: ${src} → ${realFilePath}`;
      if (opts.strict) throw new StrictModeError(msg + " — move it under the markdown's directory or drop --strict");
      opts.warn(msg);
    }

View on GitHub (pinned to 94993f7401)

Solutions

  1. Verify the image exists at the resolved path printed in the message.
  2. Fix the markdown src to match the actual relative path from the markdown's directory.
  3. Ensure the assets directory is copied alongside the markdown in CI.
  4. Drop --strict to degrade to a warn + placeholder if a missing image is acceptable.

Example fix

<!-- before: file does not exist -->
![diagram](./arch.png)  <!-- actually committed as architecture.png -->

<!-- after -->
![diagram](./architecture.png)
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs';
import path from 'node:path';

function findMissingImages(markdown: string, inputDir: string): string[] {
  const re = /!\[[^\]]*\]\((?!https?:|data:)([^)]+)\)/g;
  const missing: string[] = [];
  let m: RegExpExecArray | null;
  while ((m = re.exec(markdown))) {
    const p = path.resolve(inputDir, decodeURIComponent(m[1]));
    if (!fs.existsSync(p)) missing.push(m[1]);
  }
  return missing;
}

const missing = findMissingImages(markdown, inputDir);
if (missing.length) throw new Error(`missing images: ${missing.join(', ')}`);

Type guard

import { StrictModeError } from './diagram-prepass';
function isStrictModeError(e: unknown): e is StrictModeError {
  return e instanceof StrictModeError;
}

Prevention

When it happens

Trigger: inlineLocalImages() with opts.strict=true, an <img> whose decoded src path does not exist relative to opts.inputDir (or as a file:// URL / drive path). The existence check fails and the strict branch throws before any realpath or stat call.

Common situations: Markdown references an image that was never committed; a renamed asset whose markdown was not updated; wrong inputDir (CWD-relative resolution); a typo in the filename; a build that copies markdown but not its assets folder.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/d823cd0d7e5439ce. Report an issue: GitHub.