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

> Source: <https://dev.to/bokuwalily/sending-claude-code-on-a-daily-github-patrol-auto-scoring-useful-oss-and-skills-5foc>
> Published: 2026-07-13 00:00:03+00:00

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](https://zenn.dev/bokuwalily/articles/claude-autopilot)," 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.

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.

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

``` php
# CRAWL(gh=無料) -> DEDUP(shell) -> ENRICH(gh contents) -> SCORE+ROUTE+INGEST(claude -p=MAX枠) -> LEDGER
```

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
```

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 しない)
```

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
```

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
}
```

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
```

`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.

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
```

The `has_script`

flag set here is used by the later safety decision.

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 を置く（有効化しない）。
                  ブリーフの『要対応』に列挙。
```

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行
```

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
```

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.

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
```

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
```

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
```

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.

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>
```

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.

`v2: 2026-06-10実測調整`

).`gh`

/`jq`

`--created`

/`--updated`

into the query string for `gh search`

gets them ignored`gh search repos`

native flags like `--created ">$D90"`

(the C-series needs this).`-s`

check`grep -cE '^- '`

(implemented in the pre-dawn of 2026-06-11 after a socket death).`grep -c`

returns exit 1 on zero matches`|| echo 0`

produces `0\n0`

and breaks the integer comparison. The script comment explicitly says "footgun。付けない" (footgun. don't add it).`has_script`

flag from the file listing.`gh`

, free) from scoring (`claude -p`

, MAX plan quota), and fix cost with `MAX_CANDIDATES=30`

.`seen.tsv`

to prevent next-day resurfacing.`.incoming/`

, and immediately enable in `auto/`

only the ones with read-only permissions.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](https://zenn.dev/bokuwalily/articles/context-slimming).

*Written by **Lily** — I ship iOS apps and automate my content stack with Claude Code.

Follow along: [Portfolio](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*
