Mostly safe — a couple of notes worth reading.
Scanned 5/3/2026, 7:02:35 PM·Cached result·Fast Scan·45 rules·How we decide ↗
AIVSS Score
Low
Severity Breakdown
0
critical
0
high
19
medium
1
low
MCP Server Information
Findings
This package carries a B grade with a safety score of 70/100 and presents 19 medium-severity issues centered on ANSI escape injection vulnerabilities (8 findings), server configuration weaknesses (7 findings), and resource exhaustion risks (4 findings). The ANSI injection flaws could allow attackers to manipulate terminal output or execute unintended commands, while the configuration issues suggest the server may not be hardened for production use. You should address these medium-risk findings before deploying this package in any sensitive environment.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 20 findings using rule-based analysis. Upgrade for LLM consensus across 5 judges, AI-generated remediation, and cross-file taint analysis.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasn’t shipped yet.
20 of 20 findings
20 findings
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 350 | file=sys.stderr, |
| 351 | ) |
| 352 | |
| 353 | print( |
| 354 | f"\nExpected tokens per query (first hit or {_EXPECTED_COST_CAP // 1000}k cap if no hit)", |
| 355 | file=sys.stderr, |
| 356 | ) |
| 357 | print(f"{'Method':<20} {'expected-tokens':>15} {'vs-semble':>10}", file=sys.stderr) |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 260 | summary = _build_summary(all_results, tools) |
| 261 | |
| 262 | print(file=sys.stderr) |
| 263 | print("Summary (across all query runs):", file=sys.stderr) |
| 264 | for tool, stats in summary.items(): |
| 265 | assert isinstance(stats, dict) |
| 266 | idx_str = f"{stats['avg_index_ms']:.0f}ms" if stats["avg_index_ms"] is not None else "N/A" |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 92 | t.path if not t.start_line else f"{t.path}:{t.start_line}-{t.end_line}" for t in task.all_relevant |
| 93 | ) |
| 94 | top_files = [r.chunk.file_path for r in results[:5]] |
| 95 | print( |
| 96 | f" [{category:<12}] ndcg@10={q_ndcg10:.3f} ranks={relevant_ranks}" |
| 97 | f" n_rel={n_relevant} q={task.query!r}", |
| 98 | file=sys.stderr, |
| 99 | ) |
| 100 | print(f" targets: {targets_str}", file=sys.stderr) |
| 101 | print(f" |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 411 | }, |
| 412 | } |
| 413 | if args.repo or args.language: |
| 414 | print(json.dumps(payload, indent=2)) |
| 415 | return |
| 416 | |
| 417 | out = save_results("token-efficiency", payload) |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 73 | t.path if not t.start_line else f"{t.path}:{t.start_line}-{t.end_line}" for t in task.all_relevant |
| 74 | ) |
| 75 | top_files = [r.chunk.file_path for r in results[:5]] |
| 76 | print( |
| 77 | f" [{category:<12}] ndcg@10={q_ndcg10:.3f} ranks={relevant_ranks}" |
| 78 | f" n_rel={n_relevant} q={task.query!r}", |
| 79 | file=sys.stderr, |
| 80 | ) |
| 81 | print(f" targets: {targets_str}", file=sys.stderr) |
| 82 | print(f" |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 95 | targets_str = ", ".join( |
| 96 | t.path if not t.start_line else f"{t.path}:{t.start_line}-{t.end_line}" for t in task.all_relevant |
| 97 | ) |
| 98 | print( |
| 99 | f" [{category:<12}] ndcg@10={q_ndcg10:.3f} ranks={relevant_ranks}" |
| 100 | f" n_rel={n_relevant} q={task.query!r}", |
| 101 | file=sys.stderr, |
| 102 | ) |
| 103 | print(f" targets: {targets_str}", file=sys.stderr) |
| 104 | print(f" top-5: {[result.chunk. |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 272 | output_path = Path(args.output) if args.output else None |
| 273 | existing = _load_existing(output_path) |
| 274 | |
| 275 | print("grepai (ollama/nomic-embed-text, 137M params)", file=sys.stderr) |
| 276 | print(f"{'Repo':<22} {'Language':<12} {'Index':>9} {'NDCG@10':>8} {'p50':>8}", file=sys.stderr) |
| 277 | print(f"{'-' * 22} {'-' * 12} {'-' * 9} {'-' * 8} {'-' * 8}", file=sys.stderr) |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 83 | if not results: |
| 84 | print("No results found.") |
| 85 | else: |
| 86 | print(_format_results(f"Search results for: {args.query!r} (mode={args.mode})", results)) |
| 87 | |
| 88 | elif args.command == "find-related": |
| 89 | chunk = _resolve_chunk(index.chunks, args.file_path, args.line) |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 134 | # `--` prevents `url` from being interpreted as a git option (e.g. `--upload-pack=...`). |
| 135 | cmd = ["git", "clone", "--depth", "1", *(["--branch", ref] if ref else []), "--", url, tmp_dir] |
| 136 | try: |
| 137 | result = subprocess.run(cmd, capture_output=True, text=True, stdin=subprocess.DEVNULL) |
| 138 | except FileNotFoundError: |
| 139 | raise RuntimeError("git is not installed or not on PATH") from None |
| 140 | if result.returncode != 0: |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 26 | repo_dir = BENCH_ROOT / name |
| 27 | if not (repo_dir / ".git").exists(): |
| 28 | return f"{name}: missing checkout at {repo_dir}" |
| 29 | head = subprocess.check_output(("git", "-C", str(repo_dir), "rev-parse", "HEAD"), text=True).strip() |
| 30 | if head != revision: |
| 31 | return f"{name}: expected {revision}, found {head}" |
| 32 | return None |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 202 | def current_sha() -> str: |
| 203 | """Return the current git HEAD SHA, or 'unknown' if unavailable.""" |
| 204 | try: |
| 205 | return subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() |
| 206 | except subprocess.CalledProcessError: |
| 207 | return "unknown" |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 7 | def _run(*args: str) -> None: |
| 8 | """Run a subprocess command, raising on non-zero exit.""" |
| 9 | subprocess.run(args, check=True) |
| 10 | |
| 11 | |
| 12 | def _sync_repo(name: str, url: str, revision: str) -> None: |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 47 | class Task: |
| 48 | repo: str |
| 49 | language: str |
| 50 | query: str |
| 51 | relevant: tuple[Target, ...] |
| 52 | secondary: tuple[Target, ...] |
| 53 | category: str |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" — the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 14 | @dataclass(frozen=True) |
| 15 | class Target: |
| 16 | path: str |
| 17 | start_line: int | None = None |
| 18 | end_line: int | None = None |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" — the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 28 | class RepoSpec: |
| 29 | name: str |
| 30 | language: str |
| 31 | url: str |
| 32 | revision: str |
| 33 | benchmark_root: str | None = None |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" — the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable — a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 22 | - uses: actions/checkout@v4 |
| 23 | |
| 24 | - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} |
| 25 | uses: actions/setup-python@v5 |
| 26 | with: |
| 27 | python-version: ${{ matrix.python-version }} |
| 28 | allow-prereleases: true |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version — Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable — a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 53 | run: pytest |
| 54 | |
| 55 | - name: Upload results to Codecov |
| 56 | uses: codecov/codecov-action@v4 |
| 57 | with: |
| 58 | token: ${{ secrets.CODECOV_TOKEN }} |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version — Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable — a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 28 | allow-prereleases: true |
| 29 | |
| 30 | - name: Install uv |
| 31 | uses: astral-sh/setup-uv@v6 |
| 32 | |
| 33 | - name: Create virtual environment |
| 34 | run: uv venv .venv |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version — Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable — a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 19 | python-version: ["3.10", "3.11", "3.12", "3.13"] |
| 20 | |
| 21 | steps: |
| 22 | - uses: actions/checkout@v4 |
| 23 | |
| 24 | - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} |
| 25 | uses: actions/setup-python@v5 |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version — Dependabot can do this automatically with `version-update-strategy: inc
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 107 | return False, (time.perf_counter() - started) * 1000 |
| 108 | finally: |
| 109 | try: |
| 110 | os.killpg(os.getpgid(watch_proc.pid), signal.SIGTERM) |
| 111 | except (ProcessLookupError, PermissionError): |
| 112 | pass |
| 113 | watch_proc.wait(timeout=5) |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.