diff --git a/src/skillspector/cli.py b/src/skillspector/cli.py index 83e3224..75a5c14 100644 --- a/src/skillspector/cli.py +++ b/src/skillspector/cli.py @@ -24,6 +24,7 @@ import json import os import shutil +import sys from enum import StrEnum from pathlib import Path from typing import Annotated @@ -38,6 +39,26 @@ logger = get_logger(__name__) + +def _ensure_utf8_streams() -> None: + """Reconfigure stdout/stderr to UTF-8 so Unicode report output does not crash. + + On Windows the default console encoding (e.g. cp1252) cannot encode the + box-drawing characters and icons used in the terminal report, which raises + UnicodeEncodeError. Reconfiguring with errors="replace" makes output robust + across platforms without crashing. + """ + for stream in (sys.stdout, sys.stderr): + reconfigure = getattr(stream, "reconfigure", None) + if reconfigure is not None: + try: + reconfigure(encoding="utf-8", errors="replace") + except (ValueError, OSError): + logger.debug("Could not reconfigure %s to UTF-8", stream) + + +_ensure_utf8_streams() + app = typer.Typer( name="skillspector", help="Security scanner for AI agent skills (LangGraph). Detect vulnerabilities before installation.", diff --git a/src/skillspector/nodes/build_context.py b/src/skillspector/nodes/build_context.py index 694a048..947ab88 100644 --- a/src/skillspector/nodes/build_context.py +++ b/src/skillspector/nodes/build_context.py @@ -87,7 +87,7 @@ def _walk_skill_files(skill_dir: Path) -> list[str]: continue try: rel = item.relative_to(skill_dir) - paths.append(str(rel)) + paths.append(rel.as_posix()) except ValueError: logger.debug("Skipping path (not under skill_dir): %s", item) continue diff --git a/tests/nodes/analyzers/test_semantic_developer_intent.py b/tests/nodes/analyzers/test_semantic_developer_intent.py index 0ddc704..097b9b1 100644 --- a/tests/nodes/analyzers/test_semantic_developer_intent.py +++ b/tests/nodes/analyzers/test_semantic_developer_intent.py @@ -342,7 +342,7 @@ def _build_file_cache(skill_dir: Path) -> dict[str, str]: for item in sorted(skill_dir.rglob("*")): if not item.is_file(): continue - rel = str(item.relative_to(skill_dir)) + rel = item.relative_to(skill_dir).as_posix() try: cache[rel] = item.read_text(encoding="utf-8", errors="replace") except OSError: diff --git a/tests/nodes/analyzers/test_semantic_security_discovery.py b/tests/nodes/analyzers/test_semantic_security_discovery.py index 85209f6..4883fec 100644 --- a/tests/nodes/analyzers/test_semantic_security_discovery.py +++ b/tests/nodes/analyzers/test_semantic_security_discovery.py @@ -317,7 +317,7 @@ def _build_file_cache(skill_dir: Path) -> dict[str, str]: for item in sorted(skill_dir.rglob("*")): if not item.is_file(): continue - rel = str(item.relative_to(skill_dir)) + rel = item.relative_to(skill_dir).as_posix() try: cache[rel] = item.read_text(encoding="utf-8", errors="replace") except OSError: diff --git a/tests/nodes/test_semantic_quality_policy.py b/tests/nodes/test_semantic_quality_policy.py index f80960b..7cce237 100644 --- a/tests/nodes/test_semantic_quality_policy.py +++ b/tests/nodes/test_semantic_quality_policy.py @@ -289,7 +289,7 @@ def _build_file_cache(skill_dir: Path) -> dict[str, str]: for item in sorted(skill_dir.rglob("*")): if not item.is_file(): continue - rel = str(item.relative_to(skill_dir)) + rel = item.relative_to(skill_dir).as_posix() try: cache[rel] = item.read_text(encoding="utf-8", errors="replace") except OSError: diff --git a/tests/test_mcp_least_privilege.py b/tests/test_mcp_least_privilege.py index 9e7852e..d9a045f 100644 --- a/tests/test_mcp_least_privilege.py +++ b/tests/test_mcp_least_privilege.py @@ -98,7 +98,7 @@ def _make_state(fixture_name: str) -> dict: continue if item.name.startswith(".") and not item.name.startswith(".claude"): continue - rel = str(item.relative_to(fixture_dir)) + rel = item.relative_to(fixture_dir).as_posix() components.append(rel) components.sort() diff --git a/tests/test_mcp_tool_poisoning.py b/tests/test_mcp_tool_poisoning.py index 7d5b524..9e3e25f 100644 --- a/tests/test_mcp_tool_poisoning.py +++ b/tests/test_mcp_tool_poisoning.py @@ -121,7 +121,7 @@ def _make_state( continue if item.name.startswith(".") and not item.name.startswith(".claude"): continue - rel = str(item.relative_to(fixture_dir)) + rel = item.relative_to(fixture_dir).as_posix() components.append(rel) components.sort()