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
62 changes: 62 additions & 0 deletions .claude/skills/package-security-check/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
name: package-security-check
description: Check whether a package is secure before installing or adding it as a dependency. Use this skill EVERY time you are about to install a package or add a dependency — e.g. npm install, npm i, yarn add, pnpm add, pip install, adding entries to package.json or requirements.txt — or whenever the user asks whether a package is safe, secure, trustworthy, vulnerable, or asks for a security check/score of a package. Trigger even if the user doesn't mention security, because installing any third-party package should be preceded by this check.
---

# Package Security Check

Before installing any third-party package, verify it has no known vulnerabilities and a reasonable security health score. If it's clean, proceed with the installation. If not, STOP and show the user a warning, then let them decide.

## Workflow

1. **Run the check** before executing any install command:

```bash
python3 scripts/check_package.py <package-name> [version] [--ecosystem npm]
```

- If the user specified a version (e.g. `lodash@4.17.20`), pass it. Otherwise the script checks the latest version.
- Ecosystem defaults to `npm`. Also supports `PyPI`, `Go`, `Maven`, `NuGet`, `RubyGems`, `crates.io`.
- When installing multiple packages, check each one.

2. **Act on the exit code:**

- **Exit 0 (SECURE)** — proceed with the installation without asking. Briefly mention the check passed, e.g. "✓ express@4.19.2 — no known vulnerabilities, Scorecard 7.6/10. Installing…"
- **Exit 1 (WARNING)** — do NOT install yet. Show the user a clear warning and ask whether to proceed, pick a different version, or choose an alternative package. See the warning format below.
- **Exit 2 (ERROR)** — the check itself failed (network, package not found). Tell the user the check could not be completed and ask whether to proceed unverified. Never silently install after a failed check.

3. **If the user explicitly says to skip the check or install anyway**, respect that — this skill informs, it doesn't block.

## What the script checks

- **Known vulnerabilities** via OSV.dev — authoritative CVE/GHSA data for the exact version. Any hit triggers a warning.
- **OpenSSF Scorecard** via deps.dev — a 0–10 health heuristic (code review, maintenance, branch protection, etc.). Scores below 4.0 trigger a warning. This is a signal, not proof of insecurity — say so when warning about score alone.
- A missing Scorecard (no public GitHub repo, or repo not scanned) is common and NOT by itself a reason to warn.

## Warning format

When the verdict is WARNING, present it like this before asking how to proceed:

```
⚠️ Security warning: <package>@<version>

Known vulnerabilities:
- GHSA-xxxx [HIGH] Prototype pollution in ...
(fixed in <version>, if the script output mentions one)

OpenSSF Scorecard: 2.9/10 (low — weak maintenance/review signals)

Options:
1. Install a patched version (recommended if one exists)
2. Pick an alternative package
3. Install anyway
```

When a vulnerability is fixed in a newer version, recommend installing that version as the default suggestion.

## Notes & edge cases

- Scoped npm packages work as-is (`@nestjs/core`) — the script handles URL-encoding.
- For version *ranges* in package.json, check the version that would actually resolve (run `npm view <pkg> version` to find it if unsure).
- This check covers known vulnerabilities and project health. It does NOT detect zero-day malware or typosquatting — if a package name looks like a misspelling of a popular package (e.g. `expresss`, `lodahs`), flag that to the user separately.
- The APIs used (api.osv.dev, api.deps.dev) are free and require no authentication. If the environment blocks these domains, report exit code 2 behavior.
181 changes: 181 additions & 0 deletions .claude/skills/package-security-check/check_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""
check_package.py - Check whether a package is safe to install.

Data sources (all free, no API key):
1. OSV.dev -> known vulnerabilities (ground truth: CVEs/GHSAs)
2. deps.dev -> package metadata + OpenSSF Scorecard (health score 0-10)

Usage:
python3 check_package.py <package-name> [version] [--ecosystem npm]

Examples:
python3 check_package.py lodash 4.17.20
python3 check_package.py express
python3 check_package.py requests 2.19.0 --ecosystem PyPI

Exit codes:
0 = SECURE (no known vulnerabilities, acceptable health score)
1 = WARNING (known vulnerabilities and/or low health score)
2 = ERROR (network failure, package not found, etc.)
"""

import argparse
import json
import sys
import urllib.parse
import urllib.request

OSV_API = "https://api.osv.dev/v1/query"
DEPSDEV_API = "https://api.deps.dev/v3"
SCORECARD_WARN_THRESHOLD = 4.0 # warn if OpenSSF score is below this
TIMEOUT = 15


def http_json(url, payload=None):
req = urllib.request.Request(
url,
data=json.dumps(payload).encode() if payload is not None else None,
headers={"Content-Type": "application/json",
"User-Agent": "package-security-check"},
method="POST" if payload is not None else "GET",
)
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
return json.loads(resp.read().decode())


def get_latest_version(name, ecosystem):
"""Resolve the default/latest version from deps.dev."""
url = f"{DEPSDEV_API}/systems/{ecosystem}/packages/{urllib.parse.quote(name, safe='')}"
data = http_json(url)
for v in data.get("versions", []):
if v.get("isDefault"):
return v["versionKey"]["version"]
versions = data.get("versions", [])
return versions[-1]["versionKey"]["version"] if versions else None


def query_osv(name, version, ecosystem):
"""Return list of known vulnerabilities for this package version."""
payload = {"package": {"name": name, "ecosystem": ecosystem}}
if version:
payload["version"] = version
data = http_json(OSV_API, payload)
return data.get("vulns", [])


def query_scorecard(name, version, ecosystem):
"""Return (score, repo) from deps.dev's OpenSSF Scorecard data, or (None, None)."""
url = (
f"{DEPSDEV_API}/systems/{ecosystem}/packages/"
f"{urllib.parse.quote(name, safe='')}/versions/{urllib.parse.quote(version, safe='')}"
)
data = http_json(url)
for project in data.get("relatedProjects", []):
project_key = project.get("projectKey", {}).get("id")
if not project_key:
continue
try:
pdata = http_json(
f"{DEPSDEV_API}/projects/{urllib.parse.quote(project_key, safe='')}")
except Exception:
continue
scorecard = pdata.get("scorecard")
if scorecard and "overallScore" in scorecard:
return scorecard["overallScore"], project_key
return None, None


def severity_label(vuln):
"""Best-effort severity extraction from an OSV record."""
sev = vuln.get("database_specific", {}).get("severity")
if sev:
return sev.upper()
for s in vuln.get("severity", []):
if s.get("type", "").startswith("CVSS"):
return f"CVSS {s.get('score', '?')}"
return "UNKNOWN"


def main():
parser = argparse.ArgumentParser()
parser.add_argument("package")
parser.add_argument("version", nargs="?", default=None)
parser.add_argument("--ecosystem", default="npm",
help="npm (default), PyPI, Go, Maven, NuGet, RubyGems, crates.io")
parser.add_argument("--json", action="store_true",
help="machine-readable output")
args = parser.parse_args()

name, version, eco = args.package, args.version, args.ecosystem
result = {"package": name, "ecosystem": eco, "version": version,
"vulnerabilities": [], "scorecard": None, "verdict": None, "notes": []}

# Resolve version if not given (needed for precise OSV + scorecard lookup)
if not version:
try:
version = get_latest_version(name, eco)
result["version"] = version
result["notes"].append(
f"No version specified; checked latest ({version}).")
except Exception as e:
result["notes"].append(f"Could not resolve latest version: {e}")

# 1. Known vulnerabilities (OSV) — this is the authoritative check
try:
vulns = query_osv(name, version, eco)
for v in vulns:
result["vulnerabilities"].append({
"id": v.get("id"),
"summary": (v.get("summary") or v.get("details", ""))[:140],
"severity": severity_label(v),
})
except Exception as e:
result["notes"].append(f"OSV query failed: {e}")
result["verdict"] = "ERROR"

# 2. Health score (OpenSSF Scorecard via deps.dev) — heuristic signal
if version:
try:
score, repo = query_scorecard(name, version, eco)
if score is not None:
result["scorecard"] = {"score": score, "repo": repo}
else:
result["notes"].append(
"No OpenSSF Scorecard available (no scored GitHub repo).")
except Exception as e:
result["notes"].append(f"Scorecard lookup failed: {e}")

# Verdict
if result["verdict"] != "ERROR":
vuln_count = len(result["vulnerabilities"])
low_score = (result["scorecard"] is not None
and result["scorecard"]["score"] < SCORECARD_WARN_THRESHOLD)
result["verdict"] = "WARNING" if (
vuln_count or low_score) else "SECURE"

# Output
if args.json:
print(json.dumps(result, indent=2))
else:
v = result["verdict"]
icon = {"SECURE": "[OK]", "WARNING": "[!!]", "ERROR": "[??]"}[v]
print(f"{icon} {v}: {name}@{result['version'] or '?'} ({eco})")
if result["vulnerabilities"]:
print(
f"\nKnown vulnerabilities ({len(result['vulnerabilities'])}):")
for vu in result["vulnerabilities"]:
print(f" - {vu['id']} [{vu['severity']}] {vu['summary']}")
if result["scorecard"]:
sc = result["scorecard"]
flag = " (below threshold!)" if sc["score"] < SCORECARD_WARN_THRESHOLD else ""
print(
f"\nOpenSSF Scorecard: {sc['score']}/10{flag} repo: {sc['repo']}")
for note in result["notes"]:
print(f"note: {note}")

sys.exit({"SECURE": 0, "WARNING": 1, "ERROR": 2}[result["verdict"]])


if __name__ == "__main__":
main()
50 changes: 50 additions & 0 deletions .claude/skills/react-doctor/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
name: react-doctor
description: Use when finishing a feature, fixing a bug, before committing React code, or when the user types `/doctor`, asks to scan, triage, or clean up React diagnostics. Covers lint, accessibility, bundle size, architecture. Includes a regression check and a full local-triage workflow that fetches the canonical playbook.
version: "1.2.0"
---

# React Doctor

Scans React codebases for security, performance, correctness, and architecture issues. Outputs a 0–100 health score.

## After making React code changes:

Run `npx react-doctor@latest --verbose --diff` and check the score did not regress.

If the score dropped, fix the regressions before committing.

## For general cleanup or code improvement:

Run `npx react-doctor@latest --verbose` (without `--diff`) to scan the full codebase. Fix issues by severity — errors first, then warnings.

## /doctor — full local triage workflow

When the user types `/doctor`, says "run react doctor", or asks for a full triage / cleanup pass (not just a regression check), fetch the canonical local-triage playbook and follow every step in it:

```bash
curl --fail --silent --show-error \
--header 'Cache-Control: no-cache' \
https://www.react.doctor/prompts/react-doctor-agent.md
```

The playbook is the single source of truth — a scan → filter → triage → fix → validate loop that edits the working tree directly (never commits, never opens PRs). Updating the prompt at its source updates every agent on its next fetch — no skill reinstall needed.

Pair it with the matching per-rule prompts at `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md` (fetched on demand inside the playbook) so each fix uses the canonical, reviewer-tested recipe.

## Configuring or explaining rules

When the user wants to understand a rule, disagrees with one, or wants to disable / tune which rules run (not fix code), read [references/explain.md](references/explain.md) and follow it. Start with `npx react-doctor@latest rules explain <rule>`, then apply the narrowest control via `npx react-doctor@latest rules disable|set|category|ignore-tag …`, which edits your `doctor.config.*` (or `package.json#reactDoctor`).

## Command

```bash
npx react-doctor@latest --verbose --diff
```

| Flag | Purpose |
| ----------- | --------------------------------------------- |
| `.` | Scan current directory |
| `--verbose` | Show affected files and line numbers per rule |
| `--diff` | Only scan changed files vs base branch |
| `--score` | Output only the numeric score |
72 changes: 72 additions & 0 deletions .claude/skills/react-doctor/references/explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Explaining and configuring rules

Explain React Doctor rules and edit `doctor.config.*` safely. Use this when a user
wants to understand a rule or change which rules run — not for fixing diagnostics
(that is the main `react-doctor` skill / `/doctor`).

Triggers: "why did this rule fire", "I disagree with this rule", "turn this rule off",
"stop flagging X", "too noisy", "disable design rules".

## Workflow

1. Identify the rule key from the diagnostic (e.g. `react-doctor/no-array-index-as-key`).
2. Explain it before changing anything:

```bash
npx react-doctor@latest rules explain react-doctor/no-array-index-as-key
```

3. Pick the narrowest control that matches the user's intent (see decision guide).
4. Apply it with a `rules` subcommand (edits your `doctor.config.*` or `package.json#reactDoctor` in place, preserving other fields and formatting).
5. Validate the change did what they wanted:

```bash
npx react-doctor@latest --verbose --diff
```

## Commands

```bash
npx react-doctor@latest rules list # every rule + its effective severity
npx react-doctor@latest rules list --configured # only what your config changed
npx react-doctor@latest rules list --category Performance # filter by category
npx react-doctor@latest rules explain <rule> # why it matters + how to configure
npx react-doctor@latest rules disable <rule> # rule never runs
npx react-doctor@latest rules enable <rule> # turn back on at its recommended severity
npx react-doctor@latest rules set <rule> warn # off | warn | error
npx react-doctor@latest rules category "React Native" off # whole category
npx react-doctor@latest rules ignore-tag design # skip a rule family (design, test-noise, …)
npx react-doctor@latest rules unignore-tag design
```

Rule references accept the full key (`react-doctor/no-danger`), the bare id (`no-danger`), or a legacy key (`react/no-danger`).

## Decision guide

Match the control to the intent — prefer the narrowest one:

- **User disagrees with one rule / it's a false positive for them** → `rules disable <rule>` (sets `rules.<key> = "off"`; the rule stops running everywhere). This is the default for "I don't want this rule".
- **Rule is fine but wrong severity** → `rules set <rule> warn` or `rules set <rule> error`.
- **A disabled-by-default rule they want on** → `rules enable <rule>`.
- **A whole area is unwanted** (e.g. all React Native rules) → `rules category "<Category>" off`.
- **A behavioral family is noisy** (`design`, `test-noise`, `migration-hint`) → `rules ignore-tag <tag>`.
- **Keep it locally but hide from PR comment / score / CI gate only** → do NOT disable. Edit `surfaces` in your config (`surfaces.prComment.excludeRules`, `surfaces.score.excludeTags`, `surfaces.ciFailure.excludeCategories`). The rule still shows in local `cli` output.

How the layers combine: `ignore.tags` disables every rule carrying that tag **before** linting, so a tagged rule stays off even if `rules`/`categories` set it to `warn`/`error` (a rule-level override cannot re-enable a tag-ignored rule). For rules that aren't tag-disabled, `rules` overrides `categories` overrides the rule's default. `surfaces` is visibility-only and never changes whether a rule runs.

## Config shape

Config lives in `doctor.config.ts` (or `.js`/`.mjs`/`.cjs`/`.json`/`.jsonc`), or the `reactDoctor` key in `package.json`. The `rules` commands edit whichever exists — TS/JS edits preserve formatting (via magicast) — and create `doctor.config.json` when none does, stamping `$schema`:

```ts
// doctor.config.ts
export default {
rules: { "react-doctor/no-array-index-as-key": "off" },
categories: { "React Native": "warn" },
ignore: { tags: ["design"] },
};
```

## Educating the user

When explaining a rule, lead with the "Why it matters" guidance from `rules explain` and, when they want depth, the per-rule recipe at `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md`. Only after they understand it should you offer to disable it — many "bad" rules are catching real issues.
26 changes: 26 additions & 0 deletions .github/workflows/react-doctor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: React Doctor

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
push:
branches: [master]

permissions:
contents: read
pull-requests: write
issues: write
statuses: write

concurrency:
group: react-doctor-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
react-doctor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: millionco/react-doctor@v2
Loading