fix(tools): confine verify_pdf --dump-text and drop the bare pdftotext allowance

settings.json pre-approves verify_pdf.py with any arguments, and
--dump-text wrote the PDF's text layer to any path it was given,
creating missing parent directories and overwriting existing files.
Reproduced on master through the real CLI: a one-page PDF's text was
written to a new directory outside the repo with no prompt. The CLI now
writes only a .txt under the repo's cv/ or cover_letters/ (where
apply.md Step 5d puts it) and refuses a symlinked target.

Bash(pdftotext:*) is dropped for the same reason: Poppler writes to any
output path, and `pdftotext <file> -` prints any PDF on disk into the
session. verify_pdf.py already wraps it; the documented manual fallback
still works and now asks first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MaverickHQandClaude Opus 5.5 authored and Mads Lorentzen committed 2026-10-27 16:07:33 +01:00
1 parent 9ca456318c
commit bc2c583c44
7 files changed
+91 -5

No files matched your search

+2 -1
View File
@@ -82,7 +82,8 @@ ALLOWED_PERMISSIONS = {
"Bash(python3 tools/verify_pdf.py:*)",
"Bash(python tools/verify_layout.py:*)",
"Bash(python3 tools/verify_layout.py:*)",
"Bash(pdftotext:*)",
# No bare Bash(pdftotext:*): it writes its output to any path it is given,
# and tools/verify_pdf.py already wraps it with that path confined.
# The browser-header retry in 09-web-research.md. fetch_page.py is GET-only
# with a fixed argv and refuses to write inside the repo, which is why the
# grant names the script and not `curl`.
+29 -1
View File
@@ -24,10 +24,33 @@ import unicodedata
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# `.claude/settings.json` pre-approves this tool with any arguments, so where
# `--dump-text` may write is part of that permission. The workflow only dumps a
# CV's text layer beside it (`apply.md` Step 5d writes `cv/main_<company>_<role>.txt`);
# a path anywhere else - a shell rc file, a script the allowlist runs - would be
# an overwrite nobody was asked about.
DUMP_TEXT_DIRS = ("cv", "cover_letters")
class VerificationError(Exception):
"""Raised when a generated PDF does not satisfy its checks."""
def check_dump_target(path, root=ROOT):
"""Return why `--dump-text` may not write to `path`, or None if it may."""
path = Path(path)
if path.suffix != ".txt":
return "must name a .txt file"
if path.is_symlink():
return "must not be a symlink"
parent = path.resolve().parent
if not any(parent.is_relative_to((root / d).resolve()) for d in DUMP_TEXT_DIRS):
return "must be inside this repo's " + " or ".join(f"{d}/" for d in DUMP_TEXT_DIRS)
return None
def run_tool(command):
try:
return subprocess.run(
@@ -302,7 +325,7 @@ def build_parser():
parser.add_argument(
"--dump-text",
type=Path,
help="write the extracted text layer to this path (UTF-8)",
help="write the extracted text layer to this .txt path under cv/ or cover_letters/ (UTF-8)",
)
parser.add_argument(
"--ascii-dates",
@@ -332,6 +355,11 @@ def _force_utf8_output() -> None:
def main(argv=None):
_force_utf8_output()
args = build_parser().parse_args(argv)
if args.dump_text is not None:
reason = check_dump_target(args.dump_text)
if reason:
print(f"Error: --dump-text {args.dump_text}: {reason}", file=sys.stderr)
return 1
try:
extractor, text, pages = verify_pdf(
args.pdf,