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.
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.
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.
I run gh search repos
across 3 purpose-specific lanes. The lanes carry straight through into the later routing.
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
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
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 commentv2: 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 intoauto/
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
.
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.
SESSION_LIMIT=0
if [ "$rc" -ne 0 ] && grep -qi "session limit" "$RUN_OUT" 2>/dev/null; then
SESSION_LIMIT=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}
if [ "${CONTENT_LINES:-0}" -lt 1 ]; then
SCORE_OK=0
FB_LIST=$(head -n 8 "$TOP" | "$JQ" -r \
'"- [\(.name // "?")](\(.url // "")) \((.stars // 0))⭐ [\(.lane // "?")] — \((.desc // "")[0:60])"')
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 ignoredgh search repos
native flags like --created ">$D90"
(the C-series needs this).-s
checkgrep -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.
*Written by Lily — I ship iOS apps and automate my content stack with Claude Code.
Follow along: Portfolio · X · GitHub*