Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- id: skillxray
name: 'skillxray (AI skill security scan)'
entry: skillxray
language: python
files: '(^|/)SKILL\.md$'
pass_filenames: true
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ It also speaks SARIF, so findings show up in the GitHub Security tab:
sarif_file: skillxray.sarif
```

### Pre-commit

You can also run skillxray as a pre-commit hook to block dangerous skills from being committed. Add this to your `.pre-commit-config.yaml`:

```yaml
repos:
- repo: https://github.com/munzzyy/skillxray
rev: v0.2.0
hooks:
- id: skillxray
```

### Output formats

- default — colored human report
Expand Down
13 changes: 7 additions & 6 deletions skillxray/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
from . import __version__
from .finding import Severity
from .report import render_human, render_json, render_sarif
from .scanner import scan_path, scan_git
from .scanner import scan_path, scan_paths, scan_git


def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="skillxray",
description="Security and hygiene scanner for AI agent skills (SKILL.md, plugins, MCP bundles).",
)
p.add_argument("target", nargs="?", default=".",
p.add_argument("target", nargs="*", default=["."],
help="path to a skill dir, a SKILL.md, or a directory of skills (default: .)")
p.add_argument("--git", metavar="URL",
help="clone a git repo (shallow, read-only) and scan it instead of a local path")
Expand Down Expand Up @@ -52,10 +52,11 @@ def main(argv=None) -> int:
if args.git:
result = scan_git(args.git, args.ref)
else:
if not os.path.exists(args.target):
print(f"skillxray: no such path: {args.target}", file=sys.stderr)
return 2
result = scan_path(args.target)
for target_path in args.target:
if not os.path.exists(target_path):
print(f"skillxray: no such path: {target_path}", file=sys.stderr)
return 2
result = scan_paths(args.target)
except RuntimeError as e:
print(f"skillxray: {e}", file=sys.stderr)
return 2
Expand Down
17 changes: 13 additions & 4 deletions skillxray/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@
from .rules.quality import hygiene_checks


def scan_path(path) -> ScanResult:
path = Path(path)
units = discover(path)
result = ScanResult(root=str(path))
def scan_paths(paths: list[str | Path]) -> ScanResult:
if not paths:
return ScanResult(root=".")

units = []
for p in paths:
units.extend(discover(Path(p)))

result = ScanResult(root=str(paths[0]) if len(paths) == 1 else "[multiple]")
result.units = len(units)
scanned = 0
hygiene: dict = {}
Expand All @@ -38,6 +43,10 @@ def scan_path(path) -> ScanResult:
return result


def scan_path(path) -> ScanResult:
return scan_paths([path])


def scan_git(url: str, ref: str | None = None) -> ScanResult:
"""Clone a repo shallowly into a temp dir and scan it. Read-only: nothing
from the cloned repo is executed, and git hooks are disabled during clone."""
Expand Down