mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
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:
1 parent
9ca456318c
commit
bc2c583c44
7 files changed
+91
-5
No files matched your search
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user