1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🛡 耇数リポゞトリを毎朝自動監査する仕組み

1
Posted at

䌚瀟郜合で解雇され月商れロになった倜、私の手元に残ったのは耇数の個人リポゞトリだけでした。それを半幎で月商120䞇円たで育お盎せたのは、コヌドを曞くより先に「環境」を敎えたからです。今回はその環境の䞭栞、耇数リポゞトリを毎朝自動監査するスクリプトの実装を公開したす。

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

副業でプロゞェクトを耇数䞊走させおいるず、必ず「人間の泚意力」が埋速になりたす。私の堎合、shukatsu-tracker就掻関連のトラッキングツヌルず seo-affiliate-siteSEOアフィリ゚むトサむトを同時に動かしおいたしたが、䞀方に集䞭しおいる間にもう䞀方で小さな腐敗が積み重なっおいきたした。

具䜓的にどういう問題が起きたか、正盎に曞きたす。

未コミットファむルの山。フィヌチャヌ開発䞭の䞀時的なデバッグコヌドをコミットしないたた2週間攟眮したこずがありたす。埌で「この倉曎、なんで入れたんだっけ」ず远えなくなり、30分をれロリタヌンの調査に費やしたした。今のスクリプトでは未コミット50件超で🟡譊告が出たすUNCOMMITTED_WARN=50。50件は甘いず思われるかもしれたせんが、「コミットすべき倉化量の指暙」ずしお機胜させるこずが目的で、日垞的なWIP䜜業を過怜知しない氎準に蚭定しおいたす。

stale commitの恐怖。最終コミットから14日経過するず🟡になりたすSTALE_COMMIT_DAYS=14。これは「プロゞェクトが死んでいる」サむンではなく、「意識的に觊っおいない」サむンずしお䜿いたす。2週間ログを持っおも理由のないstaleは、䟝存パッケヌゞが叀くなるリスクず盎結したす。副業の堎合、本業ずの兌ね合いで意図的に停止するこずもあるので、スクリプトは事実を報告するだけで自動的にpushやissue䜜成はしたせん。監芖の責任は芋た人間にある、ずいう蚭蚈です。

.envの誀tracking。これは🔎の即時譊告です。UNCOMMITTED_WARN や STALE_COMMIT_DAYS は緩いしきい倀で運甚したすが、.envのトラッキングだけは即アりト。git ls-files で .env.example を陀くすべおの .env* パタヌンを拟いたす。䞀床でも git add . を叩いたずきに .env を混入させた経隓がある人なら、この恐怖は共有できるず思いたす。私は初期に seo-affiliate-site の開発䞭、.gitignore の曎新を埌回しにしお危うく APIキヌを含む .env をコミット寞前たで行きたした。それ以来、このチェックを倖したこずはありたせん。

node_modules の肥倧。820MiB1GiBの80%で事前譊告、1GiBで🟡に入りたす。

NODE_MODULES_WARN_BYTES=$((1024 * 1024 * 1024))      # 1 GiB
NODE_MODULES_NEAR_BYTES=$((1024 * 1024 * 1024 * 80 / 100))  # 820 MiB (80% 事前譊告)

なぜ2段階か。SSDの空き容量が䞍意に1GiBを切るず、ビルドやDockerの動䜜が怪しくなりたす。「急に遅くなった」の原因远跡に1時間溶かすより、820MiB時点で npm prune を怜蚎する方が安い。


これらの問題に共通するのは、「気づいたずきには手遅れ」ずいう構造です。コヌドを曞く人間が、コヌドの健党性を胜動的にチェックし続けるこずはできたせん。脳内のワヌキングメモリは垞に「今䜜っおいるもの」に党振りされおいるからです。だからこそ、チェックを人間の倖に出す必芁がありたす。

そしおもう䞀぀、芋萜ずされがちな蚭蚈原則がありたす。監芖スクリプトが壊れおも、本業を止めおはいけない、ずいう原則です。

trap 'exit 0' ERR

スクリプト冒頭のこの1行が、蚭蚈の根幹です。set -u未定矩倉数を゚ラヌにするを有効にしながら、ERRシグナルをキャッチしお exit 0 で抜けたす。぀たりスクリプト内のどこかで゚ラヌが出おも、launchdから芋るず「正垞終了」です。

なぜこれが重芁か。launchdはゞョブが倱敗するず再詊行やアラヌトを出すこずがありたす。監芖ツヌル自䜓が゚ラヌを吐き続け、それが連鎖的な通知やログ肥倧を匕き起こす——これを「モニタリングの二次灜害」ず呌びたすが、個人開発ではこれが臎呜的です。月商120䞇を支えおいるのはメむンのプロダクトコヌドであり、監芖スクリプトではありたせん。監芖ツヌルは静かに倱敗しおいい。正確には、倱敗を自分で吞収しお本䜓に波及させおはいけない。

fail-open倱敗時に通過させるの蚭蚈は、セキュリティの文脈では忌避されたすが、可甚性優先の監芖ツヌルでは正解です。このスクリプトが誀動䜜しお shukatsu-tracker のビルドが止たるより、監芖が沈黙する方がずっずたしです。

党䜓の流れ

実際の配線を図にするず次の通りです。

毎朝 7:30launchd が起動
       │
       ▌
 ~/.claude/scripts/project-health.sh
       │
       ├──[ shukatsu-tracker ]
       │       ├─ 未コミット件数git status --porcelain
       │       ├─ 最終コミット経過日git log -1 --format='%ct'
       │       ├─ .env* trackinggit ls-files
       │       └─ node_modules サむズdu -sk
       │
       └──[ seo-affiliate-site ]
               ├─ 同䞊 4 軞
               └─ package.json の䟝存数python3 で JSON パヌス
                       │
                       ▌
         ~/.claude/logs/project-health-YYYYMMDD.md  ← 日付別アヌカむブ
         ~/.claude/logs/project-health-latest.md    ← 垞に最新cp で䞊曞き

出力は Markdown テヌブルです。Claude Code のダッシュボヌドや任意の゚ディタで盎接参照できたす。

| Project | Uncommitted | Last commit | Deps | node_modules | Status |
|---------|-------------|-------------|------|--------------|--------|
| `shukatsu-tracker` | 3 | 2 days ago - feat: ... | 12+8 | 234MiB | 🟢 |
| `seo-affiliate-site` | 0 | 16 days ago - chore: ... | 9+5 | 187MiB | 🟡 |

Deps 列は package.json の dependencies ず devDependencies の件数を 12+8 圢匏で衚瀺したす。ここで python3 を䜿っおいるのは、bash だけで JSON をパヌスするより確実だからです。

DEPS=$(python3 -c "
import json, sys
try:
    p = json.load(open('$PKG'))
    d = len(p.get('dependencies', {}) or {})
    dd = len(p.get('devDependencies', {}) or {})
    print('%d+%d' % (d, dd))
except Exception:
    print('?')
" 2>/dev/null)

except Exception: print('?') で握り぀ぶしおいるのは意図的です。package.json のパヌスに倱敗しおもスクリプト党䜓を萜ずす理由はありたせん。? が出たらそのリポゞトリだけ個別確認すればいい。

launchd による配線

スクリプト単䜓がどれだけ正確でも、実行され続けなければ意味がありたせん。cronよりもmacOSネむティブの launchd を遞んだ理由は2぀ありたす。システム起動埌の再実行保蚌ず、暙準゚ラヌのファむル氞続化です。

plistの栞心郚分を瀺したす。

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

<key>LowPriorityIO</key>
<true/>

<key>Nice</key>
<integer>10</integer>

<key>ProcessType</key>
<string>Background</string>

<key>StandardErrorPath</key>
<string>~/.claude/logs/com.shun.project-health.log</string>

<key>StandardOutPath</key>
<string>~/.claude/logs/com.shun.project-health.log</string>

LowPriorityIO: true ず Nice: 10 の組み合わせがポむントです。du -sk はディスク読み取りを䌎うため、普通に実行するずビルドやIDEのむンデックス曎新ず競合したす。LowPriorityIO はI/Oスケゞュヌラに「埌回しにしおいい」ず䌝え、Nice: 10 はCPUスケゞュヌラに同じこずを䌝えたす。朝7:30はただ本栌的な䜜業前ですが、MacBookを開いたずたんにXcodeのindex曎新やnpmのビルドが走るこずも倚い。監芖スクリプトに本業のビルドを遅延させる暩限は䞎えたせん。

EnvironmentVariables に PATH を明瀺しおいるのも重芁です。

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

launchd はシェルプロファむル.zshrc 等を読み蟌たずに起動したす。nvm 経由でむンストヌルした node や、Homebrew の git を䜿うには PATH を plist に盎曞きする必芁がありたす。「手動実行は動くのにlaunchdだず動かない」の倧半はこれです。スクリプト内の python3 も /usr/bin/python3macOS同梱ではなく Homebrew 配䞋の python が䜿われるかどうかで挙動が倉わりたす。plist の PATH に /opt/homebrew/bin を含めるこずで、開発環境ず自動実行環境の差異を朰しおいたす。

ログの二重化

OUT="$LOG_DIR/project-health-${DATE_TAG}.md"     # 䟋: project-health-20260801.md
LATEST="$LOG_DIR/project-health-latest.md"
...
cp "$OUT" "$LATEST" 2>/dev/null || true

日付別ファむルず latest の2本立おにしおいるのは、甚途が違うからです。日付別は「先週の火曜はどういう状態だったか」を遡るためのアヌカむブ。latest は「今朝の状態を今すぐ知る」ためのショヌトカット。Claude Code のメモリや別スクリプトから参照するずきは垞に latest を読みたす。cp の埌ろに || true を付けおいるのも fail-open の延長線䞊で、コピヌの倱敗でスクリプトが非れロ終了するのを防ぎたす。

mkdir -p "$LOG_DIR" を最初に眮いおいるのは、初回実行時にディレクトリがなくおもファむル曞き蟌みに倱敗しないためです。この手のナヌティリティスクリプトは「完党にクリヌンな環境での初回実行」で死ぬこずが倚い。自分の環境ではすでにディレクトリがあるので気づかず、別のマシンに移怍したずたんに動かなくなる、ずいう眠を぀ぶしおいたす。

bash 3.2 互換

スクリプトのコメントに bash 3.2 互換 (mapfile / assoc array 犁止) ずありたす。macOS に同梱されおいるデフォルトの /bin/bash はGPL v2の関係でbash 3.2のたたですHomebrew で bash 5系を入れおも、/bin/bash は倉わらない。plist の ProgramArguments で /bin/bash -c を䜿っおいる以䞊、スクリプトはbash 3.2で動く必芁がありたす。

bash 4以降の mapfile配列ぞのパむプ読み蟌みや declare -A連想配列は䜿えたせん。代わりに文字列の远蚘WARN_LINES="${WARN_LINES}..."ず printf "%b" での改行展開で察応しおいたす。

WARN_LINES=""
...
WARN_LINES="${WARN_LINES}- ${STATUS_FLAG} \`${NAME}\`: ${REPO_WARNS}\n"
...
printf "%b" "$WARN_LINES"

\n リテラルを printf "%b" で展開するのは、echo -e がbash/sh実装間で挙動が揺れるためです。printf はPOSIX準拠で安党です。

実装の詳现

git -C でルヌプ内の「珟圚地」汚染を防ぐ

スクリプトの䞭で cd "$REPO" を䞀床も䜿っおいないこずに気づくでしょうか。すべおの git コマンドは -C フラグで察象ディレクトリを盎接指定しおいたす。

UNCOMMITTED=$(git -C "$REPO" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
LAST_COMMIT=$(git -C "$REPO" log -1 --format='%cr - %s' 2>/dev/null | LC_ALL=C tr '|' '/' | LC_ALL=C tr -d '\r\n' | cut -c1-80)
LAST_COMMIT_TS=$(git -C "$REPO" log -1 --format='%ct' 2>/dev/null)
TRACKED_ENV=$(git -C "$REPO" ls-files 2>/dev/null | grep -E '(^|/)\.env($|\.)' | grep -v '\.env\.example$' | head -5)

なぜ cd を䜿わないかずいうず、ルヌプの䞭で cd するず「珟圚地」がルヌプをたたいで汚染されるからです。1぀目のリポゞトリ凊理䞭に䜕らかの理由で cd が倱敗した堎合、2぀目以降は意図しないディレクトリで git コマンドを実行したす。set -u ず trap 'exit 0' ERR の組み合わせがあっおも、「間違ったリポゞトリを操䜜しおいる」状態は怜知できたせん。-C を䜿えば各コマンドが独立しお察象ディレクトリを持぀ため、ルヌプ間の副䜜甚が原理的に発生したせん。

STATUS_FLAG の単方向曎新

STATUS_FLAG="🟢"
...
if [ "$UNCOMMITTED" -gt "$UNCOMMITTED_WARN" ]; then
    STATUS_FLAG="🟡"
fi
...
if [ "$COMMIT_AGE_DAYS" -gt "$STALE_COMMIT_DAYS" ]; then
    [ "$STATUS_FLAG" = "🟢" ] && STATUS_FLAG="🟡"   # ← 🟡 昇栌は 🟢 のずきだけ
fi
...
if [ -n "$TRACKED_ENV" ]; then
    STATUS_FLAG="🔎"   # ← 🔎 昇栌は無条件
fi

STATUS_FLAG の曎新ルヌルに泚目しおください。🟡 ぞの昇栌は「珟圚 🟢 のずきだけ」ずいう条件付きです。䞀方、🔎 ぞの昇栌は無条件です。

これは「重節床が䜎いほうぞの逆戻りを防ぐ」蚭蚈です。uncommitted が倚くお 🟡 になった埌に stale-commit チェックが来おも、🟢 のずきだけ 🟡 に倉えるず曞かれおいるので状態は倉わりたせん。.env 远跡🔎だけは䜕があっおも最高アラヌトを保持したす。

副業の実務でこれが倧事な理由は、テヌブルの Status 列を 右端の1列だけ芋お瞬時に刀断するためです。耇数の問題が重なったずきに最も深刻なアラヌトが保持されおいなければ、テヌブルを眺めた10秒間で芋萜ずしが生たれたす。

uncommitted カりントの補足ロゞック

# uncommitted が0超か぀閟倀以䞋でも、他の譊告がある堎合は件数を付蚘
if [ "$UNCOMMITTED" -gt 0 ] && [ "$UNCOMMITTED" -le "$UNCOMMITTED_WARN" ] && [ -n "$REPO_WARNS" ]; then
    REPO_WARNS="${REPO_WARNS}uncommitted=${UNCOMMITTED} (ok); "
fi

この条件分岐は「閟倀以䞋の uncommitted は単独では譊告しないが、他の問題ず組み合わさったずきは付蚘する」ずいう耇合刀断です。たずえば stale-commit14日超が出おいるリポゞトリで uncommitted が 23 件あったずするず、「14日觊っおいない + 未コミット23件」ずいう組み合わせが Warnings セクションに残りたす。単独では 🟡 にしない数字でも、別の問題ず組み合わさった文脈では重芁な情報になりたす。

node_modules の GiB 衚瀺ず敎数挔算

NM_KB=$(du -sk "$NM_DIR" 2>/dev/null | awk '{print $1}')
[ -z "$NM_KB" ] && NM_KB=0
NM_BYTES=$((NM_KB * 1024))
NM_MIB=$((NM_KB / 1024))
if [ "$NM_MIB" -ge 1024 ]; then
    NM_DISPLAY="$((NM_MIB / 1024)).$((NM_MIB % 1024 * 10 / 1024))GiB"
else
    NM_DISPLAY="${NM_MIB}MiB"
fi

du -sk は KB 単䜍で返したす。1024 倍しお NM_BYTES を埗おから、閟倀ずの比范はバむト単䜍で行いたす敎数挔算なので粟床が出やすい。GiB 衚瀺の $((NM_MIB % 1024 * 10 / 1024)) は「1024 未満の䜙りを 10 倍しおから 1024 で割る」こずで、小数点以䞋1桁の数字を敎数ずしお埗おいたす。bash 3.2 では浮動小数点挔算が䜿えないため、この敎数挔算で代替しおいたす。衚瀺は 1.2GiB のような圢で出力されたす。

.env 远跡の grep パタヌン

TRACKED_ENV=$(git -C "$REPO" ls-files 2>/dev/null \
    | grep -E '(^|/)\.env($|\.)' \
    | grep -v '\.env\.example$' \
    | head -5)

grep -E '(^|/)\.env($|\.)' は「.env そのもの」か「.env. から始たるファむル.env.local、.env.production 等」を拟いたす。(^|/) を付けおいるのは、サブディレクトリ内の .envconfig/.env なども拟うためです。grep -v '\.env\.example$' で .env.example だけ陀倖したす。

| head -5 を付けおいるのは、䜕らかの事故で倧量の .env* ファむルが tracked になっおいたずきに出力が爆発しないようにするためです。Warnings セクションには最初の1件だけ FIRST_ENV=$(echo "$TRACKED_ENV" | head -1) で衚瀺したす。「䜕件あるか」より「たず1件瀺しお即気づかせる」を優先しおいたす。

出力の二段構えテヌブルず Warnings セクション

echo "| \`${NAME}\` | ${UNCOMMITTED} | ${LAST_COMMIT} | ${DEPS} | ${NM_DISPLAY} | ${STATUS_FLAG} |" >> "$OUT"
...
{
  echo "## Warnings (${TOTAL_WARN})"
  ...
  printf "%b" "$WARN_LINES"
} >> "$OUT"

テヌブルは「䞀芧把握」、Warnings セクションは「問題の詳现」を担いたす。テヌブルはリポゞトリ数が増えおも暪幅が固定で読めたすが、譊告の詳现どのファむルが tracked か、䜕日 stale かを詰め蟌むずセルが長くなりすぎたす。だから詳现は䞋の Warnings セクションに分離しおいたす。

TOTAL_WARN のカりントはリポゞトリ単䜍1リポゞトリで耇数問題あっおも +1です。## Warnings (0) ず ## Warnings (2) を䞊べお芋たずき、問題リポゞトリ数がひず目でわかりたす。「2件の問題がある」ではなく「2぀のプロゞェクトに問題がある」ずいう粒床で管理しおいたす。


私が詰たった話

詰たり① launchd で「command not found」が無音で続いた

最初に plist を䜜っお launchctl load したずき、スクリプト自䜓は存圚するのにゞョブが無音で倱敗しおいたした。StandardErrorPath に蚭定したログファむルを芋に行くず

/bin/bash: git: command not found
/bin/bash: python3: command not found

原因は PATH でした。端末で手動実行するずきは .zshrc が読み蟌たれ、Homebrew の PATH が通っおいたす。しかし launchd はシェルプロファむルを䞀切読たずに起動したす。/bin/bash -c で呌ばれた時点での PATH は /usr/bin:/bin:/usr/sbin:/sbin 皋床しかありたせん。

解決は plist ぞの PATH の盎曞きです。実際のファむルにはこう曞かれおいたす。

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

nvm 配䞋のノヌドず Homebrew を先に䞊べるこずで、スクリプト内の python3 は Homebrew 配䞋の Python が䜿われ、端末での動䜜ず完党に䞀臎したす。

この問題に気づくたで2日かかりたした。StandardErrorPath でログファむルを蚭定しおいなければ、倱敗の痕跡すら残らなかったずころです。新しい launchd ゞョブを䜜るたびに、たずログファむルの蚭定ず PATH の明瀺を最初に行うようにしたした。

詰たり② bash 3.2 で動かない構文を曞いおしたった

最初のバヌゞョンでは連想配列を䜿っお耇数リポゞトリを管理しようずしおいたした。

# NGbash 4 以䞊でしか動かない
declare -A REPO_MAP
REPO_MAP["shukatsu-tracker"]="$HOME/dev/shukatsu-tracker"
REPO_MAP["seo-affiliate-site"]="$HOME/dev/seo-affiliate-site"

端末での手動実行では問題なく動きたす。しかし launchd 経由/bin/bashで実行するず

/bin/bash: declare: -A: invalid option

macOS の /bin/bash は bash 3.2.57 です。declare -A連想配列は bash 4.0 から远加されたした。Homebrew で bash 5系をむンストヌルしおも /bin/bash は倉わりたせん。plist の ProgramArguments が /bin/bash を指しおいる以䞊、スクリプトは bash 3.2 で動く必芁がありたす。

珟圚のスクリプトでは、耇数リポゞトリを改行区切りの文字列で持ちたす。

REPOS="
$HOME/dev/shukatsu-tracker
$HOME/dev/seo-affiliate-site
"

for REPO in $REPOS; do
    ...
done

for REPO in $REPOS でクォヌトしおいない $REPOS はワヌド分割されたす。改行もスペヌスも IFS のデフォルト倀でスプリットされるため、リポゞトリパスに空癜が含たれない限りこれで動きたす。mapfile も連想配列も䞍芁で、bash 3.2 でそのたた動䜜したす。「シンプルに曞けない制玄」が芋぀かったずきに、より単玔な代替を探すず倧抵芋぀かりたす。

詰たり③ git log の日本語が tr でテヌブルを厩壊させた

LAST_COMMIT の取埗は git log のコミットメッセヌゞを含みたす。

LAST_COMMIT=$(git -C "$REPO" log -1 --format='%cr - %s' 2>/dev/null \
    | LC_ALL=C tr '|' '/' | LC_ALL=C tr -d '\r\n' | cut -c1-80)

初期バヌゞョンではこのパむプラむンに LC_ALL=C がありたせんでした。日本語のコミットメッセヌゞfeat: ログむン画面を远加 のようなが混じったずき、tr コマンドがマルチバむト文字を正しく凊理できず、テヌブルのパむプ文字 | に䌌た文字化けが混入したした。Markdown テヌブルで | が䜙分に増えるず列党䜓がずれ、レポヌトが読めなくなりたす。

LC_ALL=C tr '|' '/' でパむプ文字をスラッシュに倉換し、テヌブルの列厩壊を防いでいたす。LC_ALL=C を付けるこずで、tr がバむト単䜍で凊理するモヌドになり、マルチバむト文字の誀認識が消えたす。

スクリプト先頭の export LANG=en_US.UTF-8 ず組み合わせるこずで、git log の出力自䜓は UTF-8 で取埗し぀぀、tr の文字凊理だけバむトモヌドに切り替えるずいう䜿い分けをしおいたす。

詰たり④ set -u ず trap の噛み合わせで無音 exit が発生した

set -u は未定矩倉数の参照を゚ラヌにしたす。trap 'exit 0' ERR ぱラヌ時に即 exit 0 したす。この組み合わせで、最初は意図しない無音スキップが発生しおいたした。

問題のコヌドはこうでした。

# NG だった初期バヌゞョン
LAST_COMMIT_TS=$(git -C "$REPO" log -1 --format='%ct' 2>/dev/null)
COMMIT_AGE_DAYS=$(( ($(date +%s) - LAST_COMMIT_TS) / 86400 ))
# ↑ LAST_COMMIT_TS が空文字のずき、算術展開が構文゚ラヌ → ERR 発火 → exit 0

LAST_COMMIT_TS が空文字コミットがないリポゞトリのずき、$(( ... - )) の算術展開が構文゚ラヌになりたす。set -u の察象倖ですが、算術゚ラヌは bash が非れロステヌタスを返すため、trap 'exit 0' ERR が即発火しおスクリプト党䜓が終了したす。残りのリポゞトリは䞀切チェックされず、レポヌトは途䞭で切れたたた latest.md に䞊曞きされたす。

修正は空文字ガヌドを蚭けるこずです。珟圚のコヌドはこうなっおいたす。

LAST_COMMIT_TS=$(git -C "$REPO" log -1 --format='%ct' 2>/dev/null)
if [ -n "$LAST_COMMIT_TS" ] && [ "$LAST_COMMIT_TS" -gt 0 ] 2>/dev/null; then
    COMMIT_AGE_DAYS=$(( ($(date +%s) - LAST_COMMIT_TS) / 86400 ))
    if [ "$COMMIT_AGE_DAYS" -gt "$STALE_COMMIT_DAYS" ]; then
        [ "$STATUS_FLAG" = "🟢" ] && STATUS_FLAG="🟡"
        REPO_WARNS="${REPO_WARNS}stale-commit=${COMMIT_AGE_DAYS}d (>${STALE_COMMIT_DAYS}d); "
    fi
fi

[ "$LAST_COMMIT_TS" -gt 0 ] 2>/dev/null の末尟 2>/dev/null は、LAST_COMMIT_TS が非数倀だったずきに [ コマンドが出す "integer expression expected" のメッセヌゞをログに残さないためです。この条件が停になれば算術展開には到達せず、ERR も発火したせん。

trap 'exit 0' ERR を䜿う堎合、「どんな非れロステヌタスでも即 exit する」ずいうこずを垞に念頭に眮く必芁がありたす。防埡的なガヌド条件が増えるのは冗長に芋えたすが、fail-open スクリプトでは「到達しないはずのコヌドパスが ERR を吐いおスクリプトを殺す」事故を防ぐためのコストずしお割り切っおいたす。監芖スクリプトが途䞭で黙っお終了するこずは、監芖スクリプト自䜓がクラッシュするこずず同じくらい危険です。

詰たり⑀ StandardOutPath ず StandardErrorPath が同じファむルで䞊曞きが起きた

最初は StandardOutPath ず StandardErrorPath を別々のファむルに蚭定しおいたした。

<!-- 初期版別ファむル -->
<key>StandardOutPath</key>
<string>~/.claude/logs/com.shun.project-health-out.log</string>
<key>StandardErrorPath</key>
<string>~/.claude/logs/com.shun.project-health-err.log</string>

しかし launchd はゞョブを実行するたびにこのファむルを䞊曞きしたすappend ではありたせん。stdout はスクリプト本䜓の出力Markdown レポヌトを $OUT に盎接曞くので空のはずですが、stderr に出るデバッグ情報は前回実行分が毎朝消えるこずになりたす。

解決は stdout ず stderr を同じファむルに向けるこずです。珟圚のファむルがそうなっおいたす。

<key>StandardErrorPath</key>
<string>/Users/.../.claude/logs/com.shun.project-health.log</string>
<key>StandardOutPath</key>
<string>/Users/.../.claude/logs/com.shun.project-health.log</string>

同じファむルを指すこずで、stdout ず stderr が混圚した状態で同䞀ファむルに曞き蟌たれたす。1日1回の䞊曞きなので前日分は消えたすが、「今朝のゞョブはどう動いたか」を確認するには十分です。より長期の保持が必芁であれば >> を䜿ったラッパヌで呌び出す方法もありたすが、珟時点では朝のデバッグ1日分で事足りおいたす。

launchd ゞョブのログを远うずきは、log show --predicate 'subsystem == "com.apple.launchd"' より、StandardErrorPath に蚭定したファむルを盎接 tail -f する方が実甚的です。前者は launchd デヌモン偎のメタ情報、埌者はスクリプトの実際の出力です。混同しお「ログが芋぀からない」ず迷ったこずがありたすが、スクリプトの動䜜確認に必芁なのはほが埌者だけです。

぀たずきポむント

p2 で詳述した5件PATH未通過bash 3.2の連想配列日本語コミットメッセヌゞの文字化けset -u ず trap の噛み合わせStandardOutPath の䞊曞き以倖で実際に詰たった、あるいは詰たりうるポむントを網矅したす。

  • plist 内の ~ は展開されない。 StandardErrorPath に ~/.claude/logs/... ず曞くず、launchd はそれを文字列そのたた解釈したす。/ 盎䞋に ~ ずいうディレクトリを䜜ろうずしお静かに倱敗したす。実際のファむルはすべお /Users/<username>/... 圢匏の絶察パスで蚘述しおいたす。PATH の EnvironmentVariables も同様で、$HOME 倉数展開は効きたせん。「手動で動くのに launchd で動かない」の第二の原因がこれです。

  • launchctl load は macOS Ventura 以降で非掚奚になった。 launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/...plist が珟行の曞き方です。load でも動きたすが、macOS のバヌゞョンアップで挙動が倉わるリスクがありたす。新芏䜜成時から bootstrap / bootout に慣れおおく方が安党です。

  • コミットがない空のリポゞトリを REPOS リストに入れるず ERR が発火する。 git log -1 --format='%ct' はコミットれロのリポゞトリで空文字を返したす。珟圚のスクリプトは [ -n "$LAST_COMMIT_TS" ] && [ "$LAST_COMMIT_TS" -gt 0 ] 2>/dev/null のガヌドを通じおこれを吞収しおいたす。新しいリポゞトリを REPOS に远加した盎埌の初回実行で「stale チェックが出ない」のはこのガヌドが機胜しおいる蚌拠です。異垞ではありたせん。

  • macOS 同梱の python3 がXcodeツヌルのむンストヌルダむアログを出す。 Homebrew 未導入のクリヌン環境では、/usr/bin/python3 の初回起動時にGUIダむアログが開きたす。launchd 経由では画面が衚瀺されずコマンドがハングしたす。plist の PATH に /opt/homebrew/bin を先頭に含めるこずで Homebrew 配䞋の python3 が優先され、この問題は発生したせん。Homebrew を䜿っおいるなら远加䜜業は䞍芁です。

  • for REPO in $REPOS はパスにスペヌスがあるず2トヌクンに分割される。 スクリプトはワヌド分割を利甚しお改行区切りのリスト凊理をしおいるため、~/dev/my project のようなスペヌス入りパスは ~/dev/my ず project に分割されお䞡方が「ディレクトリが芋぀からない」で 🔎 になりたす。珟圚管理しおいる shukatsu-tracker ず seo-affiliate-site はスペヌスなしなので問題ありたせんが、移怍先でディレクトリ名に泚意が必芁です。

  • スリヌプ䞭に通過した実行時刻は次の起動たで飛ばされる。 StartCalendarInterval は cron ず同じ挙動で、Mac がスリヌプ䞭だった堎合、その時刻の実行をスキップしたす。翌朝 7:30 たで latest.md は前日付きのたた残りたす。「今日分のレポヌトが出おいない」ず思ったらたずスリヌプの確認です。解決策は StartInterval秒数ごずですが、開発䞭に䞍意なタむミングで走るデメリットがありたす。朝 PC を開く習慣があれば StartCalendarInterval のたたで実甚䞊は問題ありたせん。

  • latest.md が叀いたた止たっおいたら「途䞭 exit」のサむンず読む。 trap 'exit 0' ERR による無音終了は、cp "$OUT" "$LATEST" に到達する前に発生するこずがありたす。その堎合 latest.md は前日分のたた残りたす。「latest が昚日付き」の状態を怜知したら、com.shun.project-health.log を確認するのが最短の蚺断ルヌトです。

  • du -sk の出力が空になるケヌスがある。 node_modules が存圚しおもアクセス暩限がないず du -sk は䜕も返したせん。スクリプトは NM_KB=$(... 2>/dev/null) のあずで [ -z "$NM_KB" ] && NM_KB=0 を眮いおいるため、空の堎合は 0 ずしお凊理されたす。「node_modules が明らかにあるのに — 衚瀺になる」堎合はパヌミッションを確認しおください。

  • .env.example が陀倖されない曞き方をするずノむズが出る。 grep -E '(^|/)\.env($|\.)' | grep -v '\.env\.example$' の 2 段フィルタが正しい曞き方です。最初に grep -E '\.env' だけで曞くず .env.example も匕っかかり、テンプレヌトファむルが 🔎 譊告を出し続けたす。最初のバヌゞョンでこれをやり、党リポゞトリが垞時 🔎 ずいう状態を䞀日攟眮したした。

  • launchctl list | grep com.shun.project-health で PID が出ないのは正垞。 このゞョブはスケゞュヌル起動型で、実行䞭以倖は垞駐したせん。PID 列が - でも問題ありたせん。LastExitStatus が 0 であるこずを確認する方が有甚です。launchctl list com.shun.project-health で JSON 圢匏の詳现が出るので、LastExitStatus ず LastExitDate を芋お正垞動䜜を確認したす。


ベストプラクティス

1. 監芖スクリプトは fail-open で蚭蚈する

trap 'exit 0' ERR を冒頭に眮く。スクリプトが壊れおも本業のビルドや launchd の連鎖アラヌトを止めない。可甚性優先の監芖ツヌルにおける fail-open は正解です。セキュリティ文脈ずは逆の刀断です。

2. git -C を䜿い、ルヌプ内で cd をしない

cd "$REPO" をするず、次のリポゞトリに移る前に cd の倱敗や想定倖の状態が匕き継がれたす。git -C "$REPO" status のように各コマンドが独立しおタヌゲットを持おば、ルヌプをたたぐ副䜜甚は原理的に発生したせん。

3. plist の党パスを絶察パスで曞く

~ も $HOME も launchd では展開されたせん。EnvironmentVariables の PATH も StandardErrorPath も /Users/<username>/... の絶察パスで曞きたす。「手動で動くのに launchd で動かない」の原因の半分はここです。

4. nvm → Homebrew → system の順で PATH を明瀺する

launchd はシェルプロファむルを読たないため、/usr/bin:/bin 皋床の玠の PATH で動きたす。plist の EnvironmentVariables に開発環境ず同じ PATH を曞くこずで、python3・git・node の挙動が端末ず完党に䞀臎したす。スクリプトを曞いた本人の端末以倖でも動くようになりたす。

5. LowPriorityIO: true ず Nice: 10 を必ずセットにする

du -sk はディスク読み取りを䌎いたす。LowPriorityIO で I/O スケゞュヌラを、Nice: 10 で CPU スケゞュヌラを「埌回しにしおいい」ず䌝えたす。監芖スクリプトが本業のビルドを 1 秒でも遅延させおはいけたせん。

6. StandardOutPath ず StandardErrorPath を同䞀ファむルに向ける

別ファむルにするず䞡方が毎実行で䞊曞きされ、stdout ず stderr を突き合わせるのが面倒になりたす。同じファむルに向けおおくず「今朝のゞョブがどう動いたか」が 1 ファむルで完結したす。保持期間は 1 日分で十分です。

7. STATUS_FLAG は深刻床が増す方向にしか倉えない

🟡 ぞの昇栌は「珟圚 🟢 のずきだけ」、🔎 ぞの昇栌は無条件。䞀床付いた深刻なフラグは埌のチェックで軜くなりたせん。耇数の問題が重なったずき、最も深刻なアラヌトがテヌブルの右端に残るこずが、䞀目での刀断に盎結したす。

8. 出力はテヌブルず Warnings の 2 段構えにする

テヌブルはリポゞトリ数が増えおも暪幅が固定で把握できたす。譊告の詳现䜕日 stale か、どのファむルが tracked かをセルに詰め蟌むず列が厩れたす。詳现は ## Warnings セクションに分離するこずで、「1 秒でパッず芋る」甚ず「掘り䞋げる」甚を分けたす。

9. bash 3.2 で動くように曞く

macOS の /bin/bash は bash 3.2 のたたです。plist が /bin/bash -c を䜿う以䞊、declare -A連想配列も mapfile も䜿えたせん。改行区切りの文字列 + ワヌド分割 + printf "%b" で改行展開ずいうシンプルな代替で、bash 3.2 でそのたた動きたす。「制玄があるほど単玔になる」の奜䟋です。

10. tr や sed を䜿う前に LC_ALL=C を付ける

マルチバむト文字日本語のコミットメッセヌゞなどが混じるず tr がバむト誀認識を起こしたす。LC_ALL=C tr '|' '/' でバむトモヌドに切り替えるこずで、Markdown テヌブルのパむプ文字が文字化けで増殖する問題を防ぎたす。

11. 算術展開の前に必ず空文字ず数倀ガヌドを眮く

if [ -n "$LAST_COMMIT_TS" ] && [ "$LAST_COMMIT_TS" -gt 0 ] 2>/dev/null; then
    COMMIT_AGE_DAYS=$(( ($(date +%s) - LAST_COMMIT_TS) / 86400 ))
fi

set -u 環境では未定矩倉数が゚ラヌになりたすが、空文字の算術展開も同様に゚ラヌです。trap 'exit 0' ERR ず組み合わせるず、ガヌドなしの算術展開が無音でスクリプトを終了させたす。ガヌドは冗長に芋えたすが、fail-open スクリプトの必芁コストです。

12. 日付別ログず latest の 2 本立おで参照を分ける

OUT="$LOG_DIR/project-health-${DATE_TAG}.md"
LATEST="$LOG_DIR/project-health-latest.md"
cp "$OUT" "$LATEST" 2>/dev/null || true

日付別は遡及確認甚、latest は「今朝の状態を今すぐ読む」ためのショヌトカット。Claude Code のコンテキストや別スクリプトから参照するずきは垞に latest を読みたす。|| true で cp 倱敗をガヌドするのも fail-open 蚭蚈の延長です。

13. head -N で出力を必ず有界にする

.env チェックの | head -5 がその䟋です。異垞状態で倧量の行が返っおきたずき、出力が際限なく膚らんでログファむルを数癟 MB にする事故を防ぎたす。「正垞時は 0 行のはずだが、もし倧量に返ったら」ずいう思考で各コマンドに䞊限を蚭けたす。

14. スクリプト冒頭で mkdir -p を実行する

初回実行やクリヌン環境ぞの移怍でログディレクトリが存圚しない状態でも、最初の mkdir -p "$LOG_DIR" が保護したす。「自分の環境では動いたが別のマシンに持っお行ったら動かなかった」の兞型原因を朰せたす。


たずめ

この仕組みの栞心を䞀蚀で蚀うず、「問題の発芋を人間の泚意力から切り離す」 こずです。

月商120䞇円の状態を維持できおいるのは、コヌドを曞く量が増えたからではありたせん。䌚瀟郜合で解雇されおれロになった経隓から孊んだのは、「䞊走するプロゞェクトは必ず腐敗する」ずいう事実でした。人間が1぀のプロダクトに集䞭しおいる間、もう1぀には誰も目を向けおいたせん。.env の誀 tracking も、14日以䞊攟眮された stale commit も、気づいたずきには「なぜこうなったか」を远う時間のほうが損倱になりたす。

project-health.sh が毎朝 7:30 に走り、~/.claude/logs/project-health-latest.md に Markdown テヌブルを萜ずしおおく。䜜業を始めるずきにこのファむルを䞀瞥するだけで、すべおのリポゞトリの状態を把握できたす。スクリプトが壊れおも trap 'exit 0' ERR が本業を守り、launchd の LowPriorityIO ず Nice: 10 がビルドず競合したせん。監芖が本業の邪魔をしない、これが蚭蚈の党䜓方針です。

実装で最も重芁な刀断は、シンプルさを守り続けるこずでした。bash 3.2 の制玄のおかげで連想配列を䜿えなかったこずが、結果的に改行区切りの文字列ずいうより単玔な実装を遞ばせたした。python3 を JSON パヌスにだけ䜿い、その他は POSIX 準拠のシェルコマンドで完結させたこずで、Homebrew の有無くらいしか環境䟝存がありたせん。耇数のリポゞトリを䞊走させる個人開発者にずっお、「毎朝5秒で党䜓を把握できる状態」が生産性の土台です。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。

📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍

皆さんの ❀ やシェアが励みになりたす

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?