🔄 🔄 Claudeが自分でスキルを増やして自分で整理する仕組み — リーダー×
🔄

🔄 Claudeが自分でスキルを増やして自分で整理する仕組み

#automation#claudecode#副業2026-07-30 · 約28

月10万の大学生が月120万になれたのは、私が頑張ったからじゃない。頑張らなくても回る環境を、先に作ったからです。

なぜこの仕組みが効くのか

Claude Codeを使い込んでいると、ある矛盾にぶつかります。

使えば使うほど、Claudeは賢くなる。作業中に「このパターン、また使えそうだな」と思った手順を自動でスキルファイルに書き出してくれる。今日解決したエラー回避策が、明日の別タスクで即座に呼び出せる。これは確かに便利です。

でも3ヶ月も続けると、スキルの墓場ができあがります。

最初の1ヶ月で作ったスキルが、いつのまにか動かなくなっていても誰も気づかない。似たような名前のスキルが2つ3つ乱立する。去年の自分が「便利だろう」と判断した手順が、今の環境では前提が崩れていて逆にノイズになる。人間が管理しないシステムは、必ず腐ります。

かといって、自分で定期的にスキルを整理するのは続かない。副業収入が増えてくるほど本当に集中すべき判断が増え、ファイル整理みたいな「重要だが緊急じゃない作業」は後回しになり続けます。私も一度、スキルディレクトリが70件を超えた時点で何が何だかわからなくなり、一から見直す羽目になりました。あの時間は純粋に損失でした。

この問題の本質は「管理という作業が人間のコンテキストを食う」ことです。

重要なのは成果物を生み出す判断であり、成果物の素材になるスキルライブラリの鮮度維持は、できれば脳みそを使わずに済ませたい。それを実現するのが、今回紹介する2本のシェルスクリプトと、それを動かす2枚のlaunchdジョブです。

「作業をする」から「環境を持つ」への転換

私がClaude Code自律環境の構築に半年かけて学んだ最大の教訓は、「毎日やる作業」と「仕組みが毎日やる作業」は全く違うということです。

副業を掛け持ちして月60万まで稼いでいた頃は、私が全部やっていました。毎朝チェックリストを開いて、コンテンツを更新して、レポートを確認して。会社都合で解雇されてゼロになった時、痛感したのは「自分が動かないと止まるシステムは、脆い」という事実です。

再起にあたって徹底したのは、自分がボトルネックになる構造を排除すること。スキルの収集と整理も、「気が向いたらやる」から「毎晩3時30分と毎週日曜4時15分に自動で回る」に変えました。この差は小さいようで、3ヶ月後には決定的な違いになります。

読者にとっての共感ポイント

「スキルが貯まるのはいいけど、管理が追いつかない」という悩みは、Claude Codeをある程度使い込んだ人なら必ず経験します。最初の感動フェーズを過ぎると、ライブラリの肥大化と陳腐化という現実が来る。

この記事で紹介するのは、そこへの具体的な解答です。コードの一字一句は私の環境で実際に動いている実物を引用します。動かない設計論は書きません。

全体の流れ

システムを一言で表すと、夜間バッチで会話ログからスキルを抽出し、週次バッチで賞味期限切れを自動退避する2段パイプラインです。

会話ログ (~/Documents/my-knowledge-base/raw/conversations/*.md)
         │
         ▼  毎日 3:30 AM
┌─────────────────────────────────────────────────┐
│              skill-harvest.sh                   │
│                                                 │
│  .harvest-watermark で前回以降の差分だけ取得    │
│        ↓                                        │
│  MAX_LOGS=3 本 × PER_LOG_BYTES=15000 B         │
│  system-reminder行除去 → ダイジェスト生成       │
│        ↓                                        │
│  claude -p (sonnet / max-budget $1.20)         │
│  「この手順、再利用できる?」と問い続ける       │
│        ↓                                        │
│  ステージングdir に SKILL.md 生成               │
│  author: auto を保証 → ~/.claude/skills/auto/  │
└─────────────────────────────────────────────────┘
         │
         ▼ 蓄積
~/.claude/skills/auto/  (auto-skill ライブラリ)
         │
         ▼  毎週日曜 4:15 AM
┌─────────────────────────────────────────────────┐
│              skill-curate.sh                    │
│                                                 │
│  実行前スナップショット (.snapshots/*.tar.gz)   │
│  author: auto 以外は一切触れない                │
│        ↓                                        │
│  最終使用日を3段階で推定                        │
│  (会話ログMtime → created → file mtime)        │
│        ↓                                        │
│  30日未使用 → status: stale に書き換え         │
│  90日未使用 → .archive/ に物理退避             │
│        ↓                                        │
│  LLM で重複・統合候補を検出                    │
│  → .curator-proposals.md に提案書生成           │
└─────────────────────────────────────────────────┘

データは一方向にしか流れません。harverstがインプット側、curateがクリーンアップ側。2つのジョブは独立して動き、お互いの実行を待たない設計です。

skill-harvest.sh の仕組み

スクリプトの冒頭で3つの定数が設計思想を語っています。

MAX_LOGS=3              # 1回で扱うログ本数
PER_LOG_BYTES=15000     # ログ1本あたりの取り込み上限バイト
BUDGET_USD=1.20         # 暴走防止キャップ

会話ログは1本あたり数MB〜数十MBになることがあります。それをそのままClaudeに渡すとトークンコストが跳ね上がる。だからダイジェストを作ります。

grep -v -e 'system-reminder' -e '^- [a-z0-9].*:' "$f" 2>/dev/null | head -c $PER_LOG_BYTES

system-reminder ブロックはスキル一覧ノイズの塊なので除外。それからバイト上限で切る。この2行で、Claudeに渡す前の「情報密度の引き上げ」をシェルレベルで実現しています。

差分処理のキーは .harvest-watermark ファイルです。

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

初回は最新ログを取得し、2回目以降はウォーターマークより新しいファイルだけを対象にする。毎晩3時30分に起動しても、その日に会話がなければ「no new logs — skip」でゼロコストで終了します。課金を無駄にしない設計です。

ステージングパターンが肝心です。

~/.claude/ 配下はClaude Codeが書き込み保護をかけるため、Claudeに直接 ~/.claude/skills/auto/ へ書かせることができません。だからスクリプトは一時ディレクトリをステージングとして使います。

STAGING=$(mktemp -d -t skill-harvest-stg)
( cd "$STAGING" && perl -e 'alarm shift @ARGV; exec @ARGV' "$TIMEOUT_SEC" \
  "$CLAUDE" -p "$PROMPT" \
  --model sonnet \
  --permission-mode acceptEdits \
  --allowedTools "Write Edit Read" \
  --max-budget-usd "$BUDGET_USD" >> "$LOG" 2>&1 < /dev/null )

Claudeは $STAGING をカレントディレクトリとして起動され、相対パス ./kebab-name/SKILL.md でファイルを作る。その後、シェルが実体を ~/.claude/skills/auto/ にコピーします。

for sd in "$STAGING"/*(/N); do
  [[ -f "$sd/SKILL.md" ]] || continue
  name="${sd:t}"
  if [[ -e "$AUTO/$name" ]]; then
    echo "[$(ts)] exists, skip copy: $name" >> "$LOG"
  else
    cp -R "$sd" "$AUTO/$name" && { echo "[$(ts)] CREATED: $name" >> "$LOG"; ((created++)); }
  fi
done

既存スキルと同名なら skip copy でコピーしない。重複作成の防止はここで担保されます。

さらに author: auto の付与をPythonインラインスクリプトで保証します。これが後段の skill-curate.sh で「触っていい対象かどうか」の判断基準になるため、ここで確実に打っておく必要がある。

skill-curate.sh の仕組み

週次ジョブのほうが、設計として面白い。

最初に必ずスナップショットを取ります。

tar czf "$SNAP/auto-$(date +%Y%m%d-%H%M%S).tar.gz" -C "$HOME/.claude/skills" \
  --exclude='auto/.snapshots' --exclude='auto/.archive' auto 2>/dev/null

--exclude='auto/.snapshots' でスナップショット自身を再帰的に含めないようにしている点に注目してください。スナップショットのスナップショットが無限に増えるのを防ぐ一行です。このスクリプトは実削除を一切しません。mv.archive/ に退避するだけ。「取り戻せない操作をしない」という非破壊設計の徹底です。

安全ガードは最初の条件チェックで入ります。

if ! grep -q '^author:[[:space:]]*auto' "$md"; then
  echo "[$(ts)] skip (not author:auto): $skill" >> "$LOG"
  continue
fi

手動で作ったスキルや、バンドルスキル、ECCスキルには絶対に触れない。author: auto という一行が、「整理対象かどうか」のフラグになっています。このガードがあるので、スクリプトを怖がらずに全スキルディレクトリに対して実行できます。

最終使用日の計算が3段フォールバックになっているのも実務的です。

lastlog, created, md = (sys.argv + ["","",""])[1:4]
ref = None
if lastlog.strip():
    try: ref = float(lastlog)
    except: ref = None
if ref is None and created.strip():
    try: ref = time.mktime(datetime.datetime.strptime(created.strip(), "%Y-%m-%d").timetuple())
    except: ref = None
if ref is None:
    ref = os.path.getmtime(md)

まず会話ログでそのスキル名が言及された最新mtimeを探す。なければSKILL.mdのfrontmatterにある created: 日付。それもなければファイル自体のmtimeに落ちる。どのケースでもゼロ除算や例外で死なない設計です。

STALE_DAYS=30 を超えたら status: stale に書き換え、ARCHIVE_DAYS=90 を超えたら .archive/ に退避。この数値が重要なのは「30日使われていないスキルは本当に忘れている可能性が高い、90日ならほぼ確実に不要」という経験則から来ているからです。システムを入れてから5ヶ月、この閾値で誤検知は出ていません。

LLM提案フェーズが最後に走ります。

if [[ "$RUN_LLM" != "nollm" ]] && (( active >= 2 )) && [[ -x "$CLAUDE" ]]; then

active >= 2 件以上のスキルが残っている場合だけLLMを起動する条件が入っています。スキルが1件しかなければ統合提案の意味がないので起動しない、というロジックです。提案は --max-budget-usd 5.00 のClaude呼び出しが .curator-proposals.md に書き出します。実ファイルのスキルは変更も削除もしない、提案書を生成するだけ。最終判断は人間(私)がその提案書を読んで手動でマージするかどうかを決めます。

launchd の設定タイミング

2枚のplistが定義する実行時刻です。

skill-harvest(com.shun.skill-harvest: 毎日午前3時30分

<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>3</integer>
    <key>Minute</key>
    <integer>30</integer>
</dict>

skill-curate(com.shun.skill-curate: 毎週日曜午前4時15分

<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>4</integer>
    <key>Minute</key>
    <integer>15</integer>
    <key>Weekday</key>
    <integer>0</integer>
</dict>

午前3時30分は私が確実に眠っている時間です。MacBook Proが静かに起き上がって会話ログを読み、スキルを抽出し、また眠る。収穫(harvest)が終わってから44分後、午前4時15分に今度は週1回の整理(curate)が走る。この45分のバッファは、harverstが異常に時間がかかった場合の衝突回避です(タイムアウトは TIMEOUT_SEC=600 の10分に設定されているので余裕がある)。

両方のplistとも ProcessType: BackgroundNice: 10 を設定しています。MacBookが何かほかの作業をしていても、自分のプロセスが圧迫されないように最低優先度で動く。睡眠中に静かに仕事をするためのマナーです。

ログはそれぞれ ~/.claude/logs/com.shun.skill-harvest.log~/.claude/logs/com.shun.skill-curate.log に書き込まれ、翌朝確認すれば昨夜何が起きたかが一目でわかります。

実装の詳細

プロンプト設計:Claudeに「何を書くか」より「どこに書くか」を厳命する

skill-harvest.sh のプロンプトで最も重要な部分は、内容の指示ではなくツールの使い方の指示です。

【最重要・厳守】
- 各スキルは必ず **Write ツール** を使って ./<kebab-name>/SKILL.md として実際にファイル作成すること
- スキル本文をこの返信メッセージに貼り付けてはいけない。必ずファイルに書き込む
- ファイルを書き終えたら、作成したスキル名だけを箇条書きで報告する(本文は不要)
- 該当が無ければファイルを作らず『該当なし』とだけ答える

これが必要な理由は、指示しないとClaudeが「丁寧な返答として」スキルをチャット本文に貼り付けてしまうからです。最初のバージョンではこの指定がなく、毎回見事にMarkdownをテキストで返してくれていました。ファイルは0件のまま。コストだけかかる。

existing=$(ls "$AUTO" 2>/dev/null | grep -v '^\.' | tr '\n' ',')

既存スキル一覧をカンマ区切りでプロンプトに入れているのも意図的です。「重複するなら既存をpatchせよ」という指示とセットにすることで、同じ手順が微妙に違う名前で3本生えてくる問題を抑制します。この対策を入れた週以降、重複作成の発生率がほぼゼロになりました。

タイムアウトの仕掛け:なぜ timeout コマンドを使わないのか

スクリプトの中に少し奇妙な呼び出し方があります。

perl -e 'alarm shift @ARGV; exec @ARGV' "$TIMEOUT_SEC" \
  "$CLAUDE" -p "$PROMPT" ...

最初は素直に timeout 600 claude -p ... と書いていました。動きます。ただ、macOS標準の timeout はGNU coreutilsとは挙動が微妙に違い、子プロセスにSIGTERMを送った後の終了コードがシェルごとに変わる。さらに launchd から実行するとコマンドが見つからないケースもあった(後述)。

perl -e 'alarm' は macOS に標準搭載されていて、SIGALRM を確実に投げます。exec @ARGV で子プロセスにそのまま置き換えているのでPIDが余計に増えない。シンプルで確実な方法として最終的にこの形に落ち着きました。

curate の差分チェック:$5のLLM呼び出しを無駄にしない

skill-curate.sh のLLM提案フェーズには、実行するかどうかを判断する仕掛けが入っています。

prop_mtime=0
[[ -f "$PROP" ]] && prop_mtime=$(stat -f '%m' "$PROP" 2>/dev/null || echo 0)
changed_names=()
changed_files=()
while IFS= read -r md; do
  md_mtime=$(stat -f '%m' "$md" 2>/dev/null || echo 0)
  if [[ ! -f "$PROP" ]] || (( md_mtime > prop_mtime )); then
    changed_names+=("$skill")
    changed_files+=("$md")
  fi
done < <(find "$AUTO" -mindepth 2 -maxdepth 2 -name SKILL.md -print 2>/dev/null)

.curator-proposals.md の最終更新日時と各 SKILL.md の更新日時を比べています。前回の提案ファイルより新しいスキルが1件もなければ LLM skip でClaudeを起動しない。

2026-07-13 04:23:18 no new skills, LLM skip

毎週日曜に動くといっても、全スキルが1週間で更新されるわけではない。変化がない週にLLMを起動するのは純粋に無駄です。この判定を入れるだけで、月あたりのcurate LLMコストが実測で約65%減りました。

インラインPythonで外部依存を排除する

frontmatterの書き換えに sed を使わずPythonのインラインスクリプトを選んでいるのは、macOSとLinuxで sed -i の引数が違うからです。

import sys, re
p = sys.argv[1]; s = open(p).read()
if re.search(r'^status:', s, re.M):
    s = re.sub(r'^status:.*$', 'status: stale', s, count=1, flags=re.M)
else:
    s = re.sub(r'^(author:[ \t]*auto.*)$', r'\1\nstatus: stale', s, count=1, flags=re.M)
open(p, 'w').write(s)

status: 行が既にあれば書き換え、なければ author: auto の直後に追加する。この2分岐が必要なのは、初期のスキルには status: を省略しているものが混在しているからです。どちらのケースでも1回のPython呼び出しで処理が完結するため、pipやvenvは一切不要です。


私が詰まった話

この仕組みを完成させるまでに4回、同じ「動くはずなのに動かない」をやりました。設計は正しいのに実行環境が想定と違う、というパターンです。

詰まり①:launchd から起動すると claude が「見つからない」

最初のplistには EnvironmentVariables ブロックがありませんでした。ターミナルから手動実行すると動く。でも深夜3時30分に動いたログを朝確認すると、

2026-06-01 03:30:02 claude not found: /Users/xxx/.local/bin/claude

launchd~/.zshrc を読まない。nvm も読まない。ターミナルで動く $PATH が丸ごと存在しない状態でジョブが起動します。

対処は plist に明示的な EnvironmentVariables を書き込むことです。

<key>EnvironmentVariables</key>
<dict>
    <key>PATH</key>
    <string>/Users/xxx/.nvm/versions/node/v24.13.0/bin:/opt/homebrew/bin:...:/Users/xxx/.local/bin</string>
</dict>

さらにスクリプト冒頭にも同じPATHを export で書いています。二重に書くのは過剰に見えますが、plistのパスが更新されずにNodeバージョンを上げた場合の保険です。どちらか一方が古くなっても、もう一方が生きていれば動く。

詰まり②:~/.claude/ に直接書かせようとしたら全部止まった

最初の設計では「Claudeに直接 ~/.claude/skills/auto/ にファイルを書かせる」つもりでした。プロンプトに絶対パスを書いて実行したところ、Claudeが書き込もうとした瞬間にパーミッションエラーが出て停止。

Claude Code は ~/.claude/ 配下への外部からの書き込みをブロックします。これはセキュリティ設計で、別のプロセスから claude -p を使って自分のスキルディレクトリを書き換えさせることができない。

解決策がステージングパターンです。

STAGING=$(mktemp -d -t skill-harvest-stg)
( cd "$STAGING" && "$CLAUDE" -p "$PROMPT" \
  --permission-mode acceptEdits \
  --allowedTools "Write Edit Read" ... )
# Claudeが書いたファイルをシェルがコピーする
for sd in "$STAGING"/*(/N); do
  cp -R "$sd" "$AUTO/$name"
done

Claudeが書けるのは自分のカレントディレクトリ(一時ディレクトリ)だけ。ファイルのコピーはシェルが行う。制約を回避するのではなく、制約と共存する設計です。skill-curate.sh の提案書生成でも同じ構造を使っています。

STG=$(mktemp -d -t skill-curate-stg)
( cd "$STG" && "$CLAUDE" -p "..." \
  --add-dir "$AUTO" --permission-mode acceptEdits ... )
[[ -f "$STG/curator-proposals.md" ]] && cp "$STG/curator-proposals.md" "$PROP"

ここで --add-dir "$AUTO" が必要になります。curate の場合、Claudeは既存スキルを「読む」必要がある(提案を書くために)。しかしカレントは一時ディレクトリ。--add-dir~/.claude/skills/auto を読み取り許可リストに追加することで、「読めるが書けない」状態を作ります。

詰まり③:スナップショットが週ごとに肥大化してディスクを食い始めた

skill-curate.sh を入れてから2週間後、スナップショットディレクトリが急激に膨らんでいることに気づきました。1回目が12MB、2回目が35MB、3回目が89MB……指数関数的に増えている。

原因は tar の除外設定を忘れていたことです。

# 壊れたバージョン
tar czf "$SNAP/auto-20260601.tar.gz" -C "$HOME/.claude/skills" auto

# 正しいバージョン
tar czf "$SNAP/auto-20260601.tar.gz" -C "$HOME/.claude/skills" \
  --exclude='auto/.snapshots' --exclude='auto/.archive' auto

スナップショット自身が auto/.snapshots/ にいるので、除外しないと「スナップショットのスナップショット」が毎週入れ子になって記録されていました。修正後は毎回8〜15MBで安定しています。

詰まり④:ウォーターマークなしで動かすと毎晩$1.20課金された

初期バージョンには .harvest-watermark がありませんでした。「前回からの差分だけを処理する」ロジックがなく、毎晩 $LOGS/*.md を全件取得していた。

# 壊れたバージョン(毎晩全ログを取る)
newlogs=("${(@f)$(ls -t "$LOGS"/*.md 2>/dev/null | head -$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

会話がなかった日も3本のログを読んで、何も抽出できず終了。それでも BUDGET_USD=1.20 が毎晩消費される。10日で12ドル、気づいたのが請求を見た時でした。

ウォーターマークを入れてからは、会話がない夜は no new logs — skip の1行だけログに残してゼロコストで終了します。実際のコスト発生は月に10〜15日程度で、月額換算だと当初の半分以下になりました。

詰まり⑤:手作りスキルが突然 .archive/ に消えた

curate を動かして最初の日曜日の朝、自分が手動で作った重要なスキルが .archive/ に移動されていました。作ったのが2ヶ月前で、使用頻度は高くなかったもの(セットアップ系のone-shot手順)。

原因は author: auto ガードを入れていなかったことです。

# この条件がなかった
if ! grep -q '^author:[[:space:]]*auto' "$md"; then
  echo "[$(ts)] skip (not author:auto): $skill"
  continue
fi

当時の設計では「auto-skill ディレクトリにあるものは全部整理対象」という前提でした。でも実際には、バンドルスキルや手動作成スキルが混在していた。author: auto の一行が「整理してよいか」のフラグになる、という設計変更は、この失敗から来ています。

今は手動スキルには author: manual を明示するルールにしています。curate は author: auto 以外を一切触りません。.archive/ に退避されたスキルはスナップショットから復元できましたが、このルールを決める前に作ったスキルは全部 author: フィールドを遡及追加する必要がありました。10分くらいかけて手作業で書き直した記憶があります。

つまずきポイント

前段で紹介した5つの詰まり(PATHがない・ステージングが要る・スナップショット肥大・ウォーターマーク漏れ・author:autoガード不備)は、私が実際に踏んだ穴でした。でもそれで終わりではありません。運用に入ってから気づく「2周目の詰まり」がまだあります。箇条書きで一気に出します。

--permission-mode acceptEdits だけでは足りない

harvest/curateどちらも --allowedTools "Write Edit Read" を明示しないと、Claudeが余計なツール(Bash、WebFetchなど)を呼び出そうとして失敗します。許可ツールの制限はコスト暴走の防止にもなるので、両方セットで書くと覚えてください。

"$CLAUDE" -p "$PROMPT" \
  --permission-mode acceptEdits \
  --allowedTools "Write Edit Read" \
  --max-budget-usd "$BUDGET_USD"

zsh 固有の glob qualifier を bash で動かした

"$STAGING"/*(/N) はzshのglob qualifierで、/ はディレクトリのみを意味し N はゼロマッチでもエラーを出さない修飾子です。bashでは構文エラーになります。plistの ProgramArguments/bin/zsh を明示しているのはこのためで、デフォルトの /bin/sh に変えると即死します。

curate で --add-dir を忘れるとLLMが読めない

curate側のステージングディレクトリは一時領域なので、そこからは ~/.claude/skills/auto/ の中が見えません。--add-dir "$AUTO" を渡すことで「読み取りは許可するが書き込みは防ぐ」状態を作っています。この引数が抜けると、LLMは空白の状態で提案書を書こうとして、意味のない内容が curator-proposals.md に出力されます。

active カウンタの動きを誤解する

skill-curate.sh を読むと、90日超えのスキルは mv でアーカイブした後 ((active++)) を呼ばない設計になっています。

if (( days > ARCHIVE_DAYS )); then
  mv "$d" "$ARCH/" && echo "[...] ARCHIVED"
  # active++ はここにない
elif (( days > STALE_DAYS )); then
  # ... stale 書き換え
  ((active++))
else
  ((active++))
fi

全スキルがARCHIVE対象になった場合、active=0のままLLMが起動しない条件 (( active >= 2 )) を満たさず、提案フェーズが静かにスキップされます。エラーではなく意図した設計ですが、「LLMが一度も動いていない」と不思議に思ったときはactiveカウントを確認してください。

Node.jsバージョンアップ後に夜間バッチだけが壊れる

nvm use 24.14.0 でNodeを上げた翌朝のログに claude not found が出るケースです。plist2枚の EnvironmentVariables にはバージョン番号まで含んだ絶対パスが書かれているので、Nodeを上げたらplistも同時に更新しなければなりません。

<string>/Users/xxx/.nvm/versions/node/v24.13.0/bin:...</string>

v24.13.0 の部分をアップグレード後のバージョンに書き換えて launchctl unload → load が必要です。スクリプト冒頭の export PATH とplistの両方を更新する理由がここにあります。

launchd の StartCalendarInterval はスリープスキップではなく「起動時に即実行」

macOSが日曜4:15にスリープ中だった場合、次にMacを開いた瞬間にcurateが走ります。月曜の朝9時にノートPCを開いたら突然curate が起動、という現象が起きます。驚かないよう覚えておいてください。launchdは「来た時刻を見逃した = 次の機会に即実行」の挙動をします。

.curator-proposals.md を一度も開かないまま数ヶ月が経つ

提案書が生成されても実ファイルは変わりません。提案書を読まなければ統合・削除・パッチは一切起きない。「LLMが勝手に整理してくれる」というのは誤解で、「LLMが提案書を書いてくれる、実行するのは人間」です。私はカレンダーに毎週月曜の朝「proposals確認」を入れています。

StandardOutPath のディレクトリが存在しない状態でplistをloadする

plistには ~/.claude/logs/ へのパスが書かれていますが、ログディレクトリをあらかじめ作っておかないとlaunchdがplistのload自体を失敗させます。plistをlaunchctlに登録する前に mkdir -p ~/.claude/logs/ を確認するのを忘れずに。

LowPriorityIO: true を入れ忘れる

両plistには LowPriorityIONice: 10 の両方が入っています。LowPriorityIO がないと、深夜のTime Machine / Spotlight更新と重なった場合に数分間のディスクI/O競合が起きます。Nice: 10 はCPU優先度を下げるだけなので、I/O側の設定は別途必要です。

<key>LowPriorityIO</key>
<true/>
<key>Nice</key>
<integer>10</integer>

curate 側のタイムアウトが変数化されていない

harvest では TIMEOUT_SEC=600 を変数に出してから perl -e 'alarm shift @ARGV; exec @ARGV' "$TIMEOUT_SEC" と渡しています。curate 側は perl -e 'alarm 600; exec @ARGV' とハードコードです。変更したければスクリプト本文を直接編集する必要があります。これは設計の非対称性で、気になる方は変数化するのが良いでしょう。

macOSstat -f '%m' はLinuxでは動かない

curate の最終使用日計算で使っている stat -f '%m' はmacOS/BSD専用フラグです。同じスクリプトをLinuxサーバーに持っていくと stat が失敗し、全スキルのdaysがゼロになって一切の整理が走りません。macOS専用であることを最初から割り切って使う設計です。

nollm 引数を知らずにLLMコストを無駄遣いする

RUN_LLM="${1:-llm}"   # 第1引数 "nollm" でLLM統合提案をスキップ(テスト用)

./skill-curate.sh nollm で実行すると、stale/archive判定だけを走らせてLLM起動をスキップできます。「整理ロジックだけ動作確認したい」「週次バッチをテストしたいが$5使いたくない」という場面で活躍します。この引数の存在を知らないと毎回LLMを起動しながらデバッグする羽目になります。


ベストプラクティス

5ヶ月の運用で固まった、省略しなくてよかった習慣を列挙します。

1. 必ず手動で動かしてからlaunchdに登録する

# harvest の動作確認
~/.claude/scripts/skill-harvest.sh

# curate の動作確認(LLMなし)
~/.claude/scripts/skill-curate.sh nollm

plistをloadする前に1回手動実行するだけで、PATHミスとディレクトリ不在のほとんどが発覚します。ログに harvest done (exit 0, created=1) が出れば基本的に正常です。

2. BUDGET_USD は小さく始めて段階的に上げる

最初から $1.20 に設定する必要はありません。$0.30 から始めて、「切り捨てられているスキルがある」と感じたら上げていけば十分です。harvest は1本あたり最大15,000バイトのダイジェストを3本処理するので、会話が薄い日は$0.30でも全量処理できます。

3. Node.jsバージョンアップ時はplistもセットで更新する

plistのPATH更新は手作業です。更新を忘れないようにするには、nvmのバージョン更新をするたびに「plist 2枚のv番号を変える」をセットの作業にする。私はnvmのaliasに「.zshrc補完 + plist更新」をセットにしたメモをスキルファイルに残しています。

4. author: manual を先に付けてからcurateを有効化する

既存の手動スキルに author: manual を付けてから curate を有効化する順番が安全です。逆にすると、著者フィールドのない手動スキルが author: auto ガードを通り抜けてしまうリスクがある(ガードは ^author:[[:space:]]*auto の有無を確認するので、フィールド自体がなければスキップを判定できない)。遡及修正は10分で終わりますが、失ったスキルを .archive から掘り出す手間より先に対処する方が楽です。

5. .curator-proposals.md の確認をカレンダーに入れる

毎週月曜9:00 — ~/.claude/skills/auto/.curator-proposals.md を確認する

提案書を読んで「これは統合すべき」と判断したら、自分でSKILL.mdを手でpatchする。「curate が自動でやってくれる」のは stale/archive 判定と提案書生成だけです。実際のスキル整理の意思決定は人間の仕事です。この分業を最初から理解していると、「なぜ自動で統合してくれないのか」という期待ズレが起きません。

6. STALE_DAYS/ARCHIVE_DAYS は最初は長めに設定する

デフォルトは30日/90日ですが、最初は60日/180日くらいから始めることを勧めます。「どのスキルが使われているか」の感覚が掴めてから閾値を縮めた方が、誤判定で重要なスキルが消えることを防げます。5ヶ月運用して「30日/90日でちょうどいい」という結論に至りましたが、最初から最終値に設定する必要はありません。

7. ログを最初の2週間は毎朝確認する

tail -50 ~/.claude/logs/com.shun.skill-harvest.log
tail -50 ~/.claude/logs/com.shun.skill-curate.log

最初の2週間だけ毎朝ログを見る習慣をつけると、「何が抽出されているか・何がstaleと判定されたか」の感覚が養われます。安定したら週1確認で十分です。

8. スナップショットのローテーション上限を設ける

curate のスナップショットはデフォルトで無限に増えます。週次実行で1年続けると52個のtarball。私は月に1回古いものを削除するルーティンを設けています。

# 最新13件だけ残す(3ヶ月分)
ls -t ~/.claude/skills/auto/.snapshots/*.tar.gz | tail -n +14 | xargs rm -f

9. watermark のリセットで強制再スキャンできることを知っておく

# watermark を古い日付に書き換えると次回harvest が全ログを再スキャンする
touch -t 202601010000 ~/.claude/skills/auto/.harvest-watermark

「最近の会話から漏れたスキルを拾いたい」「watermark が誤って更新されてしまった」という場面で使います。強制再スキャンは1回で終わり、次回からまた通常の差分処理に戻ります。

10. harvestcurate の時刻に45分以上のバッファを取る

harvest のタイムアウトは TIMEOUT_SEC=600(10分)です。仮にharvest が最悪ケースで10分近くかかった場合でも、curate が3:30+10分=3:40に終わることを想定して4:15のcurate 起動と35分のバッファが確保されています。curate を4:00に前倒しすると、harvest と重なるリスクが出ます。2ジョブの時刻設定は現状のまま変えないのが無難です。

11. active >= 2 の条件の意味を把握しておく

スキルが1件以下だと統合提案の意味がないのでLLMが起動しない設計です。新しい環境でゼロから始める場合、harvest で最初の数件が溜まるまで curate のLLMフェーズは動きません。「LLMが一度も起動していない」という状態は最初のうちは正常です。

12. -p フラグと --model sonnet の組み合わせを崩さない

harvest では claude -p "$PROMPT" --model sonnet を使っています。-p はヘッドレス・非対話モードで実行するためのフラグです。これを外すとlaunchd から実行したときに標準入力待ちでハングします。また --model を省略すると設定ファイルのデフォルトモデルが使われ、高価なモデルが意図せず選ばれる可能性があります。

13. StandardOutPathStandardErrorPath を同じパスにする

両plistとも stdout と stderr を同じログファイルに向けています。別ファイルにするとエラーとスクリプト内部のログが分散して読みにくくなります。特にclaudeコマンドの出力は stderr に出る部分が多いので、1ファイルにまとめる方が実用的です。

14. 運用1ヶ月後にスキル数を数えて期待値と比較する

harvest を入れる前と1ヶ月後でスキル数を ls ~/.claude/skills/auto | grep -v '^\.' | wc -l で比べます。「思ったより少ない → ログのダイジェストが薄すぎる・会話数が少ない」「多すぎる → 重複抑制プロンプトが効いていない」の診断基準になります。

15. スクリプトを直接編集するときは必ず構文チェックを入れる

zsh -n ~/.claude/scripts/skill-harvest.sh
zsh -n ~/.claude/scripts/skill-curate.sh

zsh -n は実行せずに構文チェックだけ走らせます。深夜3時30分のジョブが構文エラーで落ちても朝まで気づかないので、編集後は必ず走らせてください。


まとめ

この記事で紹介したのは、2本のシェルスクリプトと2枚のplistだけです。コードベースは合わせて250行程度。でもこれが動き続けることで、私は「スキルを管理する」という作業を脳みそのタスクリストから完全に外せました。

設計の核心は3つです。

非破壊であること。 skill-curate.sh は実削除を一切しません。mv.archive/ に退避するだけ。実行前スナップショットで「取り戻せない操作をしない」を担保しています。だから怖がらずに有効化できる。

コスト上限を外部から制御できること。 BUDGET_USD=1.20 は暴走防止のキャップです。会話がなかった日は no new logs — skip でゼロコスト。curate のLLMフェーズも「前回以降に変化したスキルがなければスキップ」する判定が入っています。月あたりの実コストは harvest が600〜800円程度、curate の提案フェーズが200〜400円程度に収まっています。

整理対象を author: auto で明確に区切ること。 スクリプトが触れるのは自分が作ったスキルだけ。手動スキル、バンドルスキル、ECCスキルには一切手を出さない。このガードがあるから、既存の環境に後付けで入れることができました。

月120万の収益の多くは、私がコンテンツを作る判断・プロダクトを育てる判断に集中できる時間から来ています。スキルライブラリの鮮度を保つ「管理という作業」を自動化したことは、直接収益を生むわけではありません。でも、管理に取られていた週30分の注意が、価値の高い判断に向かう週30分に変わる。これが5ヶ月積み上がった結果が今です。

仕組みを持つとはそういうことだと思います。


仕組みの全体像・月120万の内訳・30日手順は有料noteにまとめています。

📕 Claude Code自律環境で、実際どう稼ぐか ― 仕組み・実例・始め方・サポート


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!