前回の記事 では自動生成されたスキルのライフサイクル管理(stale → archive の 2 段退避)を書きました。今回はその手前 ―― 会話ログを毎晩 3:30 に読んで、再利用できる手順だけを抽出し SKILL.md として書き出す harvest バッチの内部構造を全部さらします。
いま ~/.claude/skills/auto/ には 104 個のスキルが積み上がっています。そのほぼ全部が skill-harvest.sh が無人で生成したものです。
困りごと:3349 本のログを毎晩全部読ませたら枠が消し飛ぶ
~/Documents/my-knowledge-base/raw/conversations/ には今日時点で 3349 本の会話ログが蓄積されています。毎晩それを全部 LLM に渡せば、確かにスキル候補は拾えます。ただしコストは?
3349 本 × 1 万トークン/本(平均見積り)= 3300 万トークン。Max 枠を毎晩これで食いつぶせば、他のジョブが全滅します。
これを 2 つの仕組みで潰しています。
- ウォーターマーク方式:前回処理済みのファイルは二度と読まない
-
バジェットキャップ:1 回あたりの LLM 消費を
--max-budget-usd 1.20で上限固定
2 つを組み合わせることで、毎晩の harvest を「差分 3 本・45KB 固定」に収めています。
全体パイプライン:5 フェーズ
~/.claude/scripts/skill-harvest.sh は次の流れで動きます。
1. ウォーターマーク読み込み → 差分ログを 3 本だけ取得
2. system-reminder ノイズを除去 → バイト上限でダイジェスト生成
3. claude -p ヘッドレス → ステージングに SKILL.md を書かせる
4. author:auto を保証 → AUTO ディレクトリへシェルがコピー
5. ウォーターマーク更新 → ログ追記して終了
パラメータは 4 つだけです(skill-harvest.sh 冒頭より)。
MAX_LOGS=3 # 1回で扱うログ本数
PER_LOG_BYTES=15000 # ログ1本あたりの取り込み上限バイト
BUDGET_USD=1.20 # 暴走防止コストキャップ
TIMEOUT_SEC=600 # 壁時計タイムアウト
ウォーターマーク方式で重複処理を防ぐ
~/.claude/skills/auto/.harvest-watermark が前回 harvest 完了時のタイムスタンプを記録しています。
# 前回以降に更新されたログを収集(初回は最新 MAX_LOGS 件)
if [[ -f "$WM" ]]; then
newlogs=("${(@f)$(find "$LOGS" -name '*.md' -newer "$WM" 2>/dev/null)}")
else
newlogs=("${(@f)$(ls -t "$LOGS"/*.md 2>/dev/null)}")
fi
# mtime 降順で上位 MAX_LOGS 件に絞る
newlogs=("${(@f)$(ls -t "${newlogs[@]}" 2>/dev/null | head -$MAX_LOGS)}")
find -newer "$WM" で「ウォーターマークより新しい」ファイルだけを抽出します。3349 本のうち昨晩から今朝に更新されたのは通常 3〜5 本 ―― 処理対象は桁違いに少なくなります。
終了直前で必ず watermark を更新します。
touch "$WM"
exit 0
実際のログで確認できます。
[2026-07-05 03:30:05] harvest start: 3 log(s), digest= 18255B
[2026-07-05 03:35:54] harvest done (exit 0, created=0)
毎晩 3 本・18KB ほどを処理しています。
$1.20 バジェットキャップで暴走コストを制御する
claude -p の --max-budget-usd フラグに上限を渡します。
"$CLAUDE" -p "$PROMPT" \
--model sonnet \
--permission-mode acceptEdits \
--allowedTools "Write Edit Read" \
--max-budget-usd "$BUDGET_USD" >> "$LOG" 2>&1 < /dev/null
$BUDGET_USD=1.20 を超えた時点で claude が自動 abort します。今回は Claude Max 定額枠で回るため $1.20 が実費に直結するわけではありませんが、枠の消費量を固定して autopilot 等の他ジョブへの影響を限定する意味があります。
バジェットキャップだけでは壁時計時間を縛れません。TIMEOUT_SEC=600 の壁時計タイムアウトは perl -e 'alarm N; exec @ARGV' で実装しています。
( cd "$STAGING" && perl -e 'alarm shift @ARGV; exec @ARGV' "$TIMEOUT_SEC" \
"$CLAUDE" -p "$PROMPT" ... )
タイムアウト時は exit 142(SIGALRM = 128+14)で落ちます。実際のログに残っています。
[2026-07-08 03:30:05] harvest start: 3 log(s), digest= 35159B
[2026-07-08 03:40:05] harvest done (exit 142, created=0)
[2026-07-09 03:30:06] harvest start: 3 log(s), digest= 35159B
[2026-07-09 03:40:15] harvest done (exit 142, created=0)
10 分で強制終了しています。タイムアウト時も created=0 で記録されるため翌日の harvest は継続して動きます。
macOS 標準の coreutils には GNU の timeout コマンドがありません。perl -e 'alarm N; exec @ARGV' が macOS でポータブルに使える代替手段です。gtimeout(Homebrew coreutils)でも動きますが、plist の PATH に依存するためより脆い。
system-reminder ノイズを事前フィルタしてプロンプトを軽量化する
会話ログには Claude Code の <system-reminder> タグやスキル・ツール一覧行がそのまま書き込まれています。これをプロンプトに突っ込むと、手順的知識と無関係なノイズがトークンの大半を占めます。
grep -v -e 'system-reminder' -e '^- [a-z0-9].*:' "$f" 2>/dev/null | head -c $PER_LOG_BYTES
除外しているのは 2 種類です。
-
system-reminder:<system-reminder>を含む行を丸ごと除去 -
'^- [a-z0-9].*:':スキル・ツール一覧の- skill-name: description形式の行を除去
これで 1 本のログから最大 $PER_LOG_BYTES=15000 バイトだけを取り込みます。MAX_LOGS=3 と組み合わせると、1 回の harvest で LLM に渡すのは最大 **45,000 バイト(約 45KB)**に固定されます。ログが何本に増えてもプロンプトサイズは変わりません。
ステージング経由でセキュアにファイルを書き込む
~/.claude/ 配下は Claude Code 起動中に書き込み保護されることがあります。claude -p に直接 ~/.claude/skills/auto/ への書き込みをさせると permission denied で落ちます。
STAGING=$(mktemp -d -t skill-harvest-stg)
( cd "$STAGING" && perl -e 'alarm shift @ARGV; exec @ARGV' "$TIMEOUT_SEC" \
"$CLAUDE" -p "$PROMPT" \
--permission-mode acceptEdits \
--allowedTools "Write Edit Read" \
--max-budget-usd "$BUDGET_USD" >> "$LOG" 2>&1 < /dev/null )
mktemp -d で /tmp に一時ディレクトリを作り、cd "$STAGING" した状態で claude -p を走らせます。プロンプトには「カレントディレクトリ直下に ./kebab-name/SKILL.md を作れ(絶対パス禁止)」と明示しています。
- ファイル: ./<kebab-name>/SKILL.md (カレントディレクトリ直下に作る。絶対パス禁止)
claude が書き終わったあと、シェル側が $STAGING を走査して AUTO ディレクトリへコピーします。
for sd in "$STAGING"/*(/N); do
[[ -f "$sd/SKILL.md" ]] || continue
name="${sd:t}"
[[ "$name" == .* ]] && continue
# author: auto を保証(無ければ frontmatter 直後に挿入)
grep -q '^author:[[:space:]]*auto' "$sd/SKILL.md" || python3 - "$sd/SKILL.md" <<'PY'
import sys,re
p=sys.argv[1]; s=open(p).read()
if s.startswith('---'):
s=re.sub(r'^---\n', '---\nauthor: auto\n', s, count=1)
else:
s='---\nname: %s\nauthor: auto\nversion: 1.0.0\n---\n'%__import__("os").path.basename(__import__("os").path.dirname(p))+s
open(p,'w').write(s)
PY
[[ -e "$AUTO/$name" ]] || cp -R "$sd" "$AUTO/$name"
done
rm -rf "$STAGING"
author: auto が frontmatter にない場合は python3 ワンライナーで挿入してから移送します。この author: auto が curate の対象判定キーになるため必須です。
launchd 登録
plist のスケジュール部分です(~/Library/LaunchAgents/com.lily.skill-harvest.plist)。
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>3</integer>
<key>Minute</key><integer>30</integer>
</dict>
毎日 3:30 に起動します。ProcessType: Background で動き、ログは ~/.claude/logs/com.lily.skill-harvest.log に吐かれます。
登録と即時テスト:
launchctl load ~/Library/LaunchAgents/com.lily.skill-harvest.plist
launchctl start com.lily.skill-harvest
踏んだ落とし穴
-
launchd の PATH が極小:素の launchd 環境では
claudeが見つからない。plist のEnvironmentVariables/PATHに nvm の bin と~/.local/binを明示的に追加しないと即 exit する -
~/.claudeへの直書きが permission denied:Claude Code 起動中は保護される。staging + シェルコピーで回避 -
timeoutコマンドが macOS 標準にない:perl -e 'alarm N; exec @ARGV'で代替 -
watermark を
touchし忘れると全ログを毎回再スキャン:終了直前のtouch "$WM"は必ずexit 0の直前に置く -
exit 142がcreated=0と同じ見た目で記録される:タイムアウトを異常終了と区別できないため、ログの start-done の時刻差(正常: 数分、タイムアウト: 10 分ちょうど)で判別する癖をつける -
同名スキルが既にあると上書きできない:
[[ -e "$AUTO/$name" ]]でコピーをスキップしているため、既存スキルの更新(patch)はプロンプト側で「重複なら既存を Read して patch せよ」と指示して対応
実ログで見る「空振りが正常」
今週の harvest ログです。
[2026-07-05 03:30:05] harvest start: 3 log(s), digest= 18255B
この会話ログ抜粋を確認しました。INDEX.md はファイル一覧のみで
手順的知識なし。tsuzuke-ios の 2 件は既存の
xcodegen-project-yml-security-review スキルでカバー済み。
該当なし
[2026-07-05 03:35:54] harvest done (exit 0, created=0)
[2026-07-06 03:30:05] harvest start: 3 log(s), digest= 18255B
一回性レビュー+抽出基準に非該当。よってファイルは作成しませんでした。
[2026-07-06 03:30:45] harvest done (exit 0, created=0)
[2026-07-07 03:30:05] harvest start: 3 log(s), digest= 35008B
[2026-07-07 03:32:28] CREATED: note-bokumolily-factual-review
[2026-07-07 03:32:28] harvest done (exit 0, created=1)
5 日中 4 日が created=0 です。空振りは異常ではなく、抽出基準が正しく機能している証拠です。既存スキルとの重複・一度きりの個人事情・雑談はすべて弾かれます。弾く判断を LLM が自律的に行っているため、乱造になりません。
created=0 の harvest はコスト(Max 枠)をほぼ消費しません。claude -p を起動してプロンプトを読んで「該当なし」を返す分だけです。「空振りが安い」ことが夜間バッチを毎晩回せる理由でもあります。
まとめ
-
ウォーターマーク方式:
.harvest-watermarkの mtime を基準にfind -newerで差分ログだけを抽出。3349 本あっても毎晩 3 本だけ処理 -
バジェットキャップ二重:
--max-budget-usd 1.20+perl alarm 600で LLM の消費量と実行時間を両方に上限固定 -
ノイズフィルタ先出し:
grep -v 'system-reminder'を前処理に入れることでプロンプトを 45KB 固定に圧縮 -
ステージング経由書き込み:
mktemp -dでシェルが仲介し、~/.claudeへの直書き競合を回避 -
空振りが正常:抽出基準を絞ることで
created=0の日が続くのが健全な動作
スキルが 100 個を超えて curate 側の重要性が増してきた話は 前作 に書いています。次回は、この 2 つのバッチが積み上げたスキル・メモリが Claude Code の起動を重くし始めた話 ―― コンテキスト注入量の監査と削減を書く予定です。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
- 制作物・記事は bokuwalily.com にまとめています🖥️
- AIで「寝てても回る仕組み」を作って月120万にした話は noteの有料記事 に💰
- OSS: github.com/bokuwalily 🐙
- 最新情報・お問い合わせは X @bokuwalily へ🌍
皆さんの ❤️ やシェアが励みになります!