DEV Community

Lily
Lily

Posted on • Originally published at dev.to

Sending Claude Code on a Daily GitHub Patrol — Auto-Scoring Useful OSS and Skills

I kept meaning to browse GitHub for good tooling, but "whenever I felt like it" never turned into a habit. In the previous post, "Letting Claude Code Autonomously Improve Itself Unattended," I built the skeleton of a morning brief. This time I'll write about github-scout.sh, which piggybacks on that brief-generation job: every morning it patrols GitHub, auto-scores useful OSS and skills, and injects only the items that need attention into the brief on my desktop.

There are three design points. First, separate crawling (gh, free) from scoring (claude -p, MAX plan quota) so cost stays fixed. Second, mechanically judge a skill's safety by its file layout and route it into either auto-enablement or a review-required quarantine. Third, when scoring fails, don't stamp the seen ledger — leave it for the next morning's automatic retry.

The problem: I was browsing GitHub "by chance"

Some Claude Code skills are community-made and scattered across GitHub. The same goes for reference OSS for job-hunt trackers or Chrome extensions. But "manually searching when the mood strikes" isn't reproducible. I'd hit the same popular repos every time, and forget last week's discovery by the following week.

I can't count how much waste came from missing a useful skill and rewriting the same thing myself. The mechanism for a morning brief already exists. Automatically injecting GitHub discoveries into it was the shortest path to a fix.

Overall design: a 4-stage pipeline

It's written right there in the script's header comment.

# CRAWL(gh=無料) -> DEDUP(shell) -> ENRICH(gh contents) -> SCORE+ROUTE+INGEST(claude -p=MAX枠) -> LEDGER
Enter fullscreen mode Exit fullscreen mode

Cost is incurred only in the scoring phase. By capping the number of candidates, I fix the token consumption of scoring.

MAX_CANDIDATES=30   # claude へ渡す上位件数(コスト固定化)
MAX_ENRICH=8        # SKILL.md 中身を取りに行く skill 候補の上限
TIMEOUT_SEC=600
Enter fullscreen mode Exit fullscreen mode

The output destination is only under ~/.claude/. It never writes directly into the Vault (an iCloud-synced, TCC-protected area). This follows the same "only touch my own tooling directory outside protected areas" design principle as autopilot, and it's spelled out in the script's comments too.

# 出力:
#   - ブリーフ差し込み用: ~/.claude/logs/github-scout-latest.md
#   - 安全スキル(md のみ/read系権限): ~/.claude/skills/auto/<name>/ に自動有効化
#   - Bash/script 同梱スキル: ~/.claude/skills/auto/.incoming/<name>/ に隔離 + 要対応フラグ
#   - OSS/トレンド候補: ~/.claude/scout/watchlist.md に追記(clone しない)
Enter fullscreen mode Exit fullscreen mode

1. CRAWL: casting a net across 3 lanes

I run gh search repos across 3 purpose-specific lanes. The lanes carry straight through into the later routing.

# A: Claude Code スキル/プラグイン(本丸)
run_search skill "A1 topic:claude-code"   --topic claude-code --sort stars --order desc --limit 25
run_search skill "A2 claude code skills"  "claude code skills" --sort stars --order desc --limit 20
run_search skill "A3 claude agent skill"  "claude agent skill SKILL.md" --sort updated --order desc --limit 20

# B: 自分の開発に直結する OSS(autofill / maps scraper / turso / resume builder / job tracker 等)
run_search project "B1 chrome autofill"     "autofill" --language JavaScript --stars ">100" --sort stars --order desc --limit 12
run_search project "B3 turso"               "turso" --language TypeScript --stars ">50" --sort updated --order desc --limit 12
run_search project "B5 job tracker"         "job tracker" --stars ">30" --sort stars --order desc --limit 12
# ... B2/B4/B6/B7 略

# C: トレンド横断
run_search trend "C1 breakout 90d"  --created ">$D90" --stars ">800"  --sort stars   --order desc --limit 20
run_search trend "C2 hot now"       --updated ">$D3"  --stars ">3000" --sort updated --order desc --limit 20
Enter fullscreen mode Exit fullscreen mode

Each run_search records a in the coverage table on failure and keeps going. It doesn't stop even if not all sources succeed. As a rate-limit countermeasure, I put a sleep 1 after each search (GitHub Search API: 30 req/min).

Note: The B-series queries initially ANDed together every word across multiple topics, and zero-hit results kept piling up. As the comment v2: 2026-06-10実測調整 records, I loosened them to core term + language + star floor. Compound queries can only be dialed in by trying them.

The run_search function appends lane-labeled JSONL to raw.jsonl.

run_search() {
  local lane="$1"; shift; local label="$1"; shift
  local json
  json=$("$GH" search repos "$@" --json fullName,description,stargazersCount,url,language,pushedAt,createdAt 2>>"$LOG")
  if [ -n "$json" ] && echo "$json" | "$JQ" -e 'type=="array"' >/dev/null 2>&1; then
    n=$(echo "$json" | "$JQ" 'length')
    echo "$json" | "$JQ" -c --arg lane "$lane" '.[] | {lane:$lane, name:.fullName, ...}' >> "$RAW"
    COVERAGE="${COVERAGE}\n| ${label} | ✅ | ${n} |"
  else
    COVERAGE="${COVERAGE}\n| ${label} | ❌ | 0 |"
  fi
  sleep 1
}
Enter fullscreen mode Exit fullscreen mode

2. DEDUP: exclude via the seen ledger and existing skills

Using ~/.claude/scout/seen.tsv (repo fullName, scoring date), I filter out anything already scored, and I also apply weak duplicate detection against the existing skill names under auto/.

SEEN_SET="$WORK/seen.set"
cut -f1 "$SEEN" 2>/dev/null | sort -u > "$SEEN_SET"

"$JQ" -s -c 'unique_by(.name) | sort_by(-.stars) | .[]' "$RAW" 2>/dev/null \
  | while IFS= read -r line; do
      nm=$(echo "$line" | "$JQ" -r '.name')
      grep -qxF "$nm" "$SEEN_SET" 2>/dev/null && continue
      echo "$line" >> "$FRESH"
    done
Enter fullscreen mode Exit fullscreen mode

unique_by(.name) | sort_by(-.stars) dedupes, then orders by stars descending before taking the diff. Narrowing to the top MAX_CANDIDATES=30 is the point where the cost ceiling is fixed.

3. ENRICH: fetch SKILL.md only for skill candidates

To raise scoring quality, I fetch the root SKILL.md contents and file listing only for the top MAX_ENRICH=8 items where lane=skill. Everything else is scored on metadata alone (stars/desc/language).

if [ "$lane" = "skill" ] && [ "$enr_count" -lt "$MAX_ENRICH" ]; then
  enr_count=$((enr_count+1))
  skillmd=$("$GH" api "repos/$name/contents/SKILL.md" \
    --jq '.content' 2>/dev/null | base64 -d 2>/dev/null | head -c 6000)
  files=$("$GH" api "repos/$name/contents" \
    --jq '.[].name' 2>/dev/null | tr '\n' ',' | head -c 500)
  if echo "$files" | grep -qiE '\.sh,|\.js,|hooks,|scripts,|install|setup\.'; then
    has_script="true"
  fi
fi
Enter fullscreen mode Exit fullscreen mode

The has_script flag set here is used by the later safety decision.

4. SCORE + ROUTE: hand scoring and real file operations to claude -p

I pass the ENRICHed candidates together into a prompt and instruct it to score ★1-5 and perform file operations at the same time. Routing goes down 3 paths.

A) Auto-adopting a skill (lane=skill, skillmd present, ★4 or higher)

The safety rules in the prompt become the logic verbatim.

安全判定:
  - allowed-tools が無い、または Read/Grep/Glob/WebFetch/WebSearch のみ → 【安全】
  - has_script=true、または allowed-tools に Bash/Write/Edit 等が含まれる → 【要レビュー】
- 【安全】: ~/.claude/skills/auto/<kebab名>/ を作り SKILL.md を書く。
            先頭に provenance を必ず足す:
            <!-- source: github-scout | repo: <name> | url: <url> | adopted: DATE -->
            既存の auto/ に同名/酷似スキルがあればスキップ(重複禁止)。fork/mirror は取り込まない。
- 【要レビュー】: .incoming/<kebab名>/ に SKILL.md を置く(有効化しない)。
                  ブリーフの『要対応』に列挙。
Enter fullscreen mode Exit fullscreen mode

Note: Skills written into auto/ are automatically enabled at the next session startup. .incoming/ is not enabled. If I want to enable one, the design is to promote it after review with /scout-promote <name>. Anything bundled with scripts (has_script=true) is always reviewed by a human.

B) OSS/project candidates (lane=project, or lane=skill with empty skillmd, ★4 or higher)

Just append to ~/.claude/scout/watchlist.md. The format is - [name](url) ★N — reason in 30 chars or fewer (DATE). No cloning.

C) Trends (lane=trend, ★3 or higher)

Just a single line in the brief. No file operations.

The section injected into the brief looks like this.

## 🔭 GitHub Scout (2026-06-17)
### ⚡ 自動で入れたスキル
- foo-skill ★4 — 何ができるか1行(有効化済み)
### ⚠️ 要対応(あなたの確認待ち)
- bar-skill ★5 — 何ができるか / なぜ要レビューか / 有効化するなら: /scout-promote bar-skill
### 💡 採用候補(OSS) — watchlist追記済み
- baz-repo ★4 — 刺さりどころ1行
### 📈 トレンド横断
- trending-repo ★3 — 何が新しいか1行
Enter fullscreen mode Exit fullscreen mode

5. Piggybacking on the morning brief

Scout is not its own independent launchd job. It's called as step 2.55, immediately after brief generation (step 2.5) in vault-auto-ingest.sh.

# vault-auto-ingest.sh 抜粋 (step 2.55)
# 2.55 GitHub Scout 巡回 → 発見/要対応をブリーフに差し込む。scout 本体は ~/.claude(保護外)のみ書く。
run_to 900 bash "$HOME/.claude/scripts/github-scout.sh" >> "$LOG" 2>&1 \
  || echo "[$(ts)] github-scout 失敗(継続)" >> "$LOG"

SCOUT_SEC="$HOME/.claude/logs/github-scout-latest.md"
BRIEF_SRC="$VAULT/wiki/today-brief.md"
if [ -s "$SCOUT_SEC" ] && [ "$SCOUT_SEC" -nt "$START_STAMP" ] && \
   [ -s "$BRIEF_SRC" ] && [ "$BRIEF_SRC" -nt "$START_STAMP" ]; then
  { echo ""; echo "---"; echo ""; cat "$SCOUT_SEC"; } >> "$BRIEF_SRC"
fi
Enter fullscreen mode Exit fullscreen mode

On failure, || echo "...(継続)" lets it move on to the next step. It doesn't halt the whole brief generation.

A freshness check (-nt "$START_STAMP") prevents an old file from being mistakenly appended. vault-auto-ingest.sh fires across multiple slots at 4:55 / 8:15 / 10:15 / 12:15, but after success a DONE_MARKER makes subsequent slots return immediately, so once the whole brief (scout included) succeeds once, it isn't run redundantly.

6. Failure tolerance: not stamping the seen ledger is the key to retries

What controls what happens in the scoring phase is the SCORE_OK flag and SESSION_LIMIT detection.

# Claude MAX 枠切れ(session limit)を専用検出
SESSION_LIMIT=0
if [ "$rc" -ne 0 ] && grep -qi "session limit" "$RUN_OUT" 2>/dev/null; then
  SESSION_LIMIT=1
  # リセットまで <=8 分なら待って1回リトライ
  if [ "$wait_s" -ge 0 ] && [ "$wait_s" -le 480 ]; then
    log "session limit — ${wait_s}s 待って1回リトライ (reset=${RESET_STR:-?})"
    sleep "$wait_s"
    run_claude; rc=$?
  else
    log "session limit — reset 遠い/不明(${wait_s}s)。待たず seen 非commit で次回巡回に委ねる"
  fi
fi
Enter fullscreen mode Exit fullscreen mode

When the scoring result is thin (fewer than one - line), I set SCORE_OK=0 and enter a 3-stage fallback.

CONTENT_LINES=$(grep -cE '^- ' "$OUT" 2>/dev/null); CONTENT_LINES=${CONTENT_LINES:-0}
# ※ grep -c は0件でも「0」を出力し exit 1 → `|| echo 0` は 0\n0 を生んで整数比較を壊す footgun。付けない。
if [ "${CONTENT_LINES:-0}" -lt 1 ]; then
  SCORE_OK=0
  # 1段目: 今回クロールの TOP 候補(生データ)を jq で整形
  FB_LIST=$(head -n 8 "$TOP" | "$JQ" -r \
    '"- [\(.name // "?")](\(.url // "")) \((.stars // 0))⭐ [\(.lane // "?")] — \((.desc // "")[0:60])"')
  # 2段目: TOP が空なら直近 watchlist の ★4/★5 を引く
  # 3段目: それでも空なら明示的な失敗行(無言の空欄にしない)
fi
Enter fullscreen mode Exit fullscreen mode

In the final LEDGER step, I stamp seen.tsv only on scoring success.

if [ "${SCORE_OK:-1}" -eq 1 ]; then
  while IFS= read -r line; do
    nm=$(echo "$line" | "$JQ" -r '.name')
    printf '%s\t%s\n' "$nm" "$TODAY" >> "$SEEN"
  done < "$TOP"
else
  log "採点未成立(SCORE_OK=0) — seen 非commit。次回巡回で再採点させる。"
fi
Enter fullscreen mode Exit fullscreen mode

When SESSION_LIMIT hits and the reset is far off, it's non-committed just like SCORE_OK=0. The same candidates automatically resurface in the next morning's patrol.

Coverage table: spelling out the misses

A per-source coverage table is attached in a <details> at the end of the brief.

<details><summary>scout カバレッジ</summary>

| ソース | 状態 | 件数 |
|---|---|---|
| A1 topic:claude-code | ✅ | 25 |
| B1 chrome autofill | ✅ | 12 |
| C1 breakout 90d | ❌ | 0 |
...

_raw 234 → fresh 41 → 採点 30。seen累計 187。_
</details>
Enter fullscreen mode Exit fullscreen mode

Following the "Multi-source Coverage" policy in CLAUDE.md, failed sources are surfaced with rather than hidden. "採点 30" (scored 30) is proof the MAX_CANDIDATES cap is in effect, letting you read off that cost is fixed.

Pitfalls I hit

  • All-word AND search hitting 0 results → loosen to core term + language + star floor. The B-series queries were tuned by actual measurement (the comment reads v2: 2026-06-10実測調整).
  • launchd's minimal PATH can't find gh/jq → same countermeasure as autopilot: export nvm's node, Homebrew, and local bins all together.
  • Mixing --created/--updated into the query string for gh search gets them ignored → pass them as gh search repos native flags like --created ">$D90" (the C-series needs this).
  • A claude OUT that's only a 35-byte header slips past the -s check → I made it judge candidate line count with grep -cE '^- ' (implemented in the pre-dawn of 2026-06-11 after a socket death).
  • grep -c returns exit 1 on zero matches → adding || echo 0 produces 0\n0 and breaks the integer comparison. The script comment explicitly says "footgun。付けない" (footgun. don't add it).
  • A script-bundled skill can look safe if you only read the front matter → complement with the has_script flag from the file listing.
  • Treating SESSION_LIMIT the same as an error causes a false 🚨 alert → branch on a dedicated flag so "waiting on quota" and "scoring bug" get different messages.

Summary

  • Separate crawling (gh, free) from scoring (claude -p, MAX plan quota), and fix cost with MAX_CANDIDATES=30.
  • Cast a net with 3 lanes (A skills / B OSS / C trends) × multiple queries, and use seen.tsv to prevent next-day resurfacing.
  • Quarantine script-bundled skills in .incoming/, and immediately enable in auto/ only the ones with read-only permissions.
  • Instead of making it a standalone launchd job, piggyback it as step 2.55 of morning brief generation.
  • On scoring failure, a 3-stage fallback plus a non-committed seen means automatic retry the next morning.
  • The coverage table hides no misses.

The parts I've built across this series (4-layer memory / self-propagating skills / context reduction / launchd / autopilot / long-term memory) all connect into one here. GitHub discoveries ride into the brief, and good skills become usable automatically from the next day. A loop where the environment "reads and grows" is now wired end to end.

Next time, I wrote about how context injection ballooned as these daily-accumulating skills and briefs grew — The Context Audit That Cut It From 228KB to 48KB.


Written by **Lily* — I ship iOS apps and automate my content stack with Claude Code.
Follow along: Portfolio · X · GitHub*

Top comments (0)