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
- Copy or move the payload JSON file into the project tree (under the project root)
- Copy or move the payload into /tmp
- 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
- Always store PDF config payloads in the project tree or /tmp
- Never reference absolute paths outside the project root in --from-file
- Run validateReadPath before invoking if unsure about directory safety
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
- load-html: --from-file ${payloadPath} must be under ${SAFE_D
- Path must be within: ${SAFE_DIRECTORIES.join(', ')}
- Invalid skill name "${name}". Must be lowercase letters/digi
- Invalid file path in stageSkill: "${relPath}".
- commitSkill: staged dir "${opts.stagedDir}" is a symlink — r
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/613665266e73c9ab.
Report an issue: GitHub.