Reject unsafe pathspecs

ober

2ed895f04b9c8073e2cedede3ac3e3fef057663b

diff --git a/README.md b/README.md
index 0da9fef..7bc22b1 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,8 @@ is retained as a label boundary only and does not create a separate verdict.
 
 By default, scans skip `vendor/`, `generated/`, `dist/`, `node_modules/`, and
 `.git/` paths. Use `--include PATH` or `--file PATH` to inspect one of those
-paths explicitly.
+paths explicitly. `--include` and `--file` accept repository-relative pathspecs;
+absolute paths and `..` components are refused before invoking Git.
 
 Resource limits default to 500 changed files per commit, 20,000 added lines
 analyzed per commit, and 50,000 bytes per AI note. When a limit is hit, output
diff --git a/main-binary.ss b/main-binary.ss
index b7ee1b8..b87fe2f 100644
--- a/main-binary.ss
+++ b/main-binary.ss
@@ -128,6 +128,21 @@
       '()))
 (def (pathspec-args file)
   (if file (list "--" file) '()))
+(def (parent-path-component? part)
+  (same-public-string? part ".."))
+
+(def (repo-relative-pathspec? path)
+  (and (string? path)
+       (not (string-empty? path))
+       (not (path-absolute? path))
+       (not (for/or ([part (string-split path #\/)])
+              (parent-path-component? part)))))
+
+(def (validate-pathspec! path)
+  (if (and path (not (repo-relative-pathspec? path)))
+      (begin
+        (displayln (str "error: refusing path outside repository: " path))
+        (exit 2))))
 
 (def (range-spec from to)
   (cond [(and from to) (str from ".." to)]
@@ -901,19 +916,21 @@
   (let ([root (repo-root (options-path opts))])
     (if (not root)
         (begin (displayln "error: path is not inside a Git repository") (exit 2))
-        (let* ([commit (options-commit opts)]
-               [revs (commit-list root (options-count opts) commit (options-from opts) (options-to opts) (options-file opts))]
-               [findings (scan-repo root revs (options-file opts) (options-min-lines opts)
-                                    (options-metadata-only? opts) (options-heuristics-only? opts))])
-          (cond [(string=? (options-command opts) "stats") (display-stats findings)]
-                [(string=? (options-command opts) "verify-authorship") (display-json root findings)]
-                [(string=? (options-command opts) "explain")
-                 (if (pair? findings)
-                     (if (string=? (options-format opts) "json")
-                         (displayln (json-string (finding-json root (car findings))))
-                         (display-explain (car findings)))
-                     (begin (displayln "error: commit not found") (exit 3)))]
-                [else (display-findings root findings (options-format opts))])))))
+        (begin
+          (validate-pathspec! (options-file opts))
+          (let* ([commit (options-commit opts)]
+                 [revs (commit-list root (options-count opts) commit (options-from opts) (options-to opts) (options-file opts))]
+                 [findings (scan-repo root revs (options-file opts) (options-min-lines opts)
+                                      (options-metadata-only? opts) (options-heuristics-only? opts))])
+            (cond [(string=? (options-command opts) "stats") (display-stats findings)]
+                  [(string=? (options-command opts) "verify-authorship") (display-json root findings)]
+                  [(string=? (options-command opts) "explain")
+                   (if (pair? findings)
+                       (if (string=? (options-format opts) "json")
+                           (displayln (json-string (finding-json root (car findings))))
+                           (display-explain (car findings)))
+                       (begin (displayln "error: commit not found") (exit 3)))]
+                  [else (display-findings root findings (options-format opts))]))))))
 
 (if (or (null? cli-args) (member "--help" cli-args) (member "-h" cli-args))
     (usage)
diff --git a/tests/fixture-smoke.sh b/tests/fixture-smoke.sh
index da2aa5c..572172e 100755
--- a/tests/fixture-smoke.sh
+++ b/tests/fixture-smoke.sh
@@ -214,6 +214,18 @@ git clone -q --depth 1 "file://$fixture" "$shallow_repo"
 shallow_json=$("$root/bin/jerboa-aigit" scan "$shallow_repo" --format json --count 1)
 printf '%s\n' "$shallow_json" | grep -q 'repository is shallow; parent history may be unavailable'
 
+if "$root/bin/jerboa-aigit" scan "$fixture" --file ../outside >/tmp/jerboa-aigit-pathspec.out 2>&1; then
+  echo "parent-directory pathspec should fail" >&2
+  exit 1
+fi
+grep -q 'refusing path outside repository' /tmp/jerboa-aigit-pathspec.out
+
+if "$root/bin/jerboa-aigit" scan "$fixture" --include /tmp/outside >/tmp/jerboa-aigit-absolute-pathspec.out 2>&1; then
+  echo "absolute pathspec should fail" >&2
+  exit 1
+fi
+grep -q 'refusing path outside repository' /tmp/jerboa-aigit-absolute-pathspec.out
+
 if "$root/bin/jerboa-aigit" scan "$fixture/nope" >/tmp/jerboa-aigit-invalid.out 2>&1; then
   echo "invalid repository path should fail" >&2
   exit 1