garrytan/gstack · error · Error

pdf: --from-file ${payloadPath} must be under ${SAFE_DIRECTO

Error message

pdf: --from-file ${payloadPath} must be under ${SAFE_DIRECTORIES.join(' or ')} (security policy). Copy the payload into the project tree or /tmp first.

What it means

Thrown by parsePdfFromFile() when the resolved path of the --from-file payload fails validateReadPath(), meaning it is outside all configured SAFE_DIRECTORIES (typically the project tree and /tmp). This is a security policy enforcement — it prevents the CLI from reading arbitrary files on the filesystem via the --from-file shortcut, maintaining parity with load-html path validation.

Source

Thrown at browse/src/meta-commands.ts:148

  if (result.format && (result.width || result.height)) {
    throw new Error('pdf: --format is mutex with --width/--height');
  }
  if (result.pageNumbers && result.footerTemplate) {
    throw new Error('pdf: --page-numbers is mutex with --footer-template (page-numbers writes the footer itself)');
  }

  return result;
}

export function parsePdfFromFile(payloadPath: string): ParsedPdfArgs {
  // Parity with load-html --from-file (browse/src/write-commands.ts) and
  // the direct load-html <file> path: every caller-supplied file path
  // must pass validateReadPath so the safe-dirs policy can't be skirted
  // by routing reads through the --from-file shortcut.
  try {
    validateReadPath(path.resolve(payloadPath));
  } catch {
    throw new Error(
      `pdf: --from-file ${payloadPath} must be under ${SAFE_DIRECTORIES.join(' or ')} (security policy). Copy the payload into the project tree or /tmp first.`
    );
  }
  const raw = fs.readFileSync(payloadPath, 'utf8');
  let json: any;
  try {
    json = JSON.parse(raw);
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    throw new Error(`pdf: --from-file ${payloadPath} is not valid JSON (${msg}).`);
  }
  if (json === null || typeof json !== 'object' || Array.isArray(json)) {
    throw new Error(`pdf: --from-file ${payloadPath} must be a JSON object, got ${Array.isArray(json) ? 'array' : typeof json}.`);
  }
  const out: ParsedPdfArgs = {
    output: json.output || `${TEMP_DIR}/browse-page.pdf`,
    format: json.format,
    width: json.width,

View on GitHub (pinned to 94993f7401)

Solutions

  1. Copy or move the payload JSON file into the project tree (under the project root)
  2. Copy or move the payload into /tmp
  3. If running in a different project, ensure the path is relative to the current project root

Example fix

# before
$B pdf --from-file ~/configs/pdf.json

# after
cp ~/configs/pdf.json /tmp/pdf.json
$B pdf --from-file /tmp/pdf.json
Defensive patterns

Strategy: validation

Validate before calling

// Validate path is under a safe directory before calling
import path from 'path';
const resolved = path.resolve(payloadPath);
const isSafe = SAFE_DIRECTORIES.some(dir => resolved.startsWith(path.resolve(dir)));
if (!isSafe) {
  throw new Error(`Path must be under ${SAFE_DIRECTORIES.join(' or ')}`);
}

Try / catch

try {
  const parsed = parsePdfFromFile(payloadPath);
} catch (e) {
  if (e.message.includes('security policy')) {
    fs.copyFileSync(payloadPath, '/tmp/pdf-config.json');
    parsed = parsePdfFromFile('/tmp/pdf-config.json');
  }
}

Prevention

When it happens

Trigger: Passing a --from-file path that resolves outside SAFE_DIRECTORIES, e.g., a file in the user's home directory, /etc, or any non-project/non-tmp location.

Common situations: User stores PDF config JSON in their home directory or a shared system path. Or an attacker tries to exfiltrate system files by pointing --from-file at sensitive paths.

Related errors


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