From 60aca564ad601495a3aac7181c6a015640dcc772 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 03:43:53 +0000 Subject: [PATCH] feat(scripts): read what a skill ships, not what it says Every verdict reviewed so far rests on SKILL.md, which is the skill's description of itself. The behaviour lives in the scripts it invokes: buddy-card extracts a Claude OAuth token from the Keychain and then runs scripts/buddy-algorithm.js with it, and that script has never been read. A skill whose stated purpose differs from what its code does is precisely the supply-chain case worth finding, and reviewing only the prose cannot find it. --ls probes for a file listing (no documented endpoint, so try the plausible ones and report what answers). --fetch takes slug=path pairs so the referenced scripts can be read directly. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DNoTXU8k3pfSBzR7aJubqL --- .github/workflows/inspect-detail-shape.yml | 10 +++- scripts/inspect-detail-shape.py | 56 ++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/.github/workflows/inspect-detail-shape.yml b/.github/workflows/inspect-detail-shape.yml index 3beff76..ee46300 100644 --- a/.github/workflows/inspect-detail-shape.yml +++ b/.github/workflows/inspect-detail-shape.yml @@ -21,6 +21,11 @@ on: description: "Print each skill's SKILL.md instead, to check a verdict against the file" type: boolean default: false + mode: + description: "ls = probe for a file listing; fetch = slugs are slug=path pairs" + type: choice + options: ["", "ls", "fetch"] + default: "" permissions: contents: read @@ -46,8 +51,11 @@ jobs: SLUGS: ${{ github.event.inputs.slugs }} SURVEY: ${{ github.event.inputs.survey }} CONTENT: ${{ github.event.inputs.content }} + MODE: ${{ github.event.inputs.mode }} run: | - if [ "$CONTENT" = "true" ]; then + if [ -n "$MODE" ]; then + python scripts/inspect-detail-shape.py --$MODE $SLUGS + elif [ "$CONTENT" = "true" ]; then python scripts/inspect-detail-shape.py --content $SLUGS elif [ "$SURVEY" = "true" ]; then python scripts/inspect-detail-shape.py --survey $SLUGS diff --git a/scripts/inspect-detail-shape.py b/scripts/inspect-detail-shape.py index 52cd2b3..b9938d9 100755 --- a/scripts/inspect-detail-shape.py +++ b/scripts/inspect-detail-shape.py @@ -177,6 +177,55 @@ async def dump_content(slugs: list[str], max_chars: int | None = None) -> None: await asyncio.sleep(0.55) + +async def list_files(slug: str) -> None: + """Try to enumerate the files a skill actually ships. + + SKILL.md is the skill's *description of itself*. The behaviour lives in the + scripts it invokes, and a skill whose stated purpose differs from what its + code does is exactly the supply-chain case worth finding -- so a verdict + based on SKILL.md alone is a reading of the marketing copy, not the + product. There is no documented listing endpoint, so probe the plausible + ones and report what answers. + """ + candidates = [ + f"{BASE_URL}/skills/{slug}/files", + f"{BASE_URL}/skills/{slug}/tree", + f"{BASE_URL}/skills/{slug}/contents", + f"{BASE_URL}/skills/{slug}?include=files", + ] + async with httpx.AsyncClient(timeout=20.0, follow_redirects=True) as client: + print(f"\n{'=' * 70}\n{slug}: probing for a file listing\n{'=' * 70}", flush=True) + for url in candidates: + try: + resp = await client.get(url) + body = resp.text[:600] + print(f" {resp.status_code} {url}\n {body}\n", flush=True) + except Exception as exc: + print(f" ERR {url}: {type(exc).__name__}: {exc}", flush=True) + await asyncio.sleep(0.55) + + +async def fetch_paths(specs: list[str]) -> None: + """Fetch explicit ``slug=path`` files, e.g. ``buddy-card=scripts/algo.js``.""" + async with httpx.AsyncClient(timeout=20.0, follow_redirects=True) as client: + for spec in specs: + slug, _, path = spec.partition("=") + path = path or "SKILL.md" + print(f"\n{'=' * 70}\n{slug} :: {path}\n{'=' * 70}", flush=True) + try: + resp = await client.get( + f"{BASE_URL}/skills/{slug}/file", params={"path": path} + ) + if resp.status_code != 200: + print(f" HTTP {resp.status_code}: {resp.text[:200]}", flush=True) + else: + print(resp.text, flush=True) + except Exception as exc: + print(f" {type(exc).__name__}: {exc}", flush=True) + await asyncio.sleep(0.55) + + async def main() -> int: args = sys.argv[1:] if args and args[0] == "--survey": @@ -185,6 +234,13 @@ async def main() -> int: if args and args[0] == "--content": await dump_content(args[1:]) return 0 + if args and args[0] == "--ls": + for slug in args[1:]: + await list_files(slug) + return 0 + if args and args[0] == "--fetch": + await fetch_paths(args[1:]) + return 0 slugs = args if not slugs: