「Claude Codeを無人で自律改善させる」前作との続きで、今回は環境の肥大化を自覚する仕掛けの話です。
`~/.claude/` 配下にスキルが積み重なり、launchdジョブが増え、プロジェクトが二桁を超えたとき、「全体が何層あって今何が動いているか」を即答できなくなりました。毎朝 `ls` を打つのではなく、Obsidianを開けば俯瞰できる状態を目指して、`~/.claude/scripts/env-map.sh` を書きました。
困りごと:環境が増えると全体像を忘れる
現状のスナップショットはこうです。
- plugin bundleのスキル: 1,004個
- 自己生成スキル(auto/): 77個
- launchdジョブ(`com.shun.*.plist`): 24本
- 把握しているプロジェクト: 11本
スキル・ジョブ・プロジェクトそれぞれの最終更新やgit状態がバラバラで、毎回確認しに行くコストが積み重なります。「今日のautopilotは何をした?」「あのプロジェクトのブランチは?」を1ページで答えられる場所が欲しかった。
設計:3層Mermaid + Obsidianへ出力
図にするのは3層です。
出力先はObsidian Vault(`~/Documents/claude-obsidian/wiki/meta/environment-map.md`)。vault-auto-ingest(4:55)が拾って自動コミットするので、バージョン管理もタダで付いてきます。
スクリプトの設計方針はコメントに書いてあります。
# 何が起きても生成を完走させる(個別の収集失敗は "?" で degrade)。set -e は使わない。
set -uo pipefail`set -e` を使わないのがポイント。MCP接続確認などで部分的に失敗しても `?` を埋めて最後まで出力させます。
launchd最小PATH対策
launchdから起動するとPATHは `/usr/bin:/bin:/usr/sbin:/sbin` 程度しかない。`node`も`claude`も`jq`も見つかりません。スクリプトの先頭でPATHを明示的に組み立てます。
NVM_BIN="$(ls -d "$HOME"/.nvm/versions/node/*/bin 2>/dev/null | sort -V | tail -1)"
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:${NVM_BIN:+$NVM_BIN:}/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"nvmはnodeバージョンを上げるとパスが変わるので、`sort -V | tail -1` で最新版binを動的に解決しています。固定パスを書くと次のバージョンアップで壊れます。
launchdの最小PATHで詰まるのは「ユーザが入れたCLI全般」です。`~/.local/bin`(uv・claude等)→ Homebrew → nvm の順でフォールバックを積んでおくと、どの環境構成でも当たります。
何を図にすると俯瞰が効くか
Claude環境の収集
AUTO_SKILLS="$(ls "$HOME/.claude/skills/auto/" 2>/dev/null | grep -vc README)"
AGENTS="$(find "$HOME/.claude/plugins" "$HOME/.claude/agents" -path '*/agents/*.md' \
-o -path "$HOME/.claude/agents/*.md" 2>/dev/null | wc -l | tr -d ' ')"
PLUGINS="$(jq -r '.enabledPlugins // {} | length' "$SETTINGS" 2>/dev/null || echo '?')"
HOOK_EVENTS="$(jq -r '.hooks // {} | keys | length' "$SETTINGS" 2>/dev/null || echo '?')"
LAUNCHD="$(ls "$HOME/Library/LaunchAgents/"com.shun.*.plist 2>/dev/null | wc -l | tr -d ' ')"MCPだけは注意が必要です。`claude mcp list`はライブ接続を試みるので起動が遅く、失敗することもある。タイムアウトで押さえます。
MCP_OK="?"
if have claude; then
_mcp="$(timeout 12 claude mcp list 2>/dev/null)"
[ -n "$_mcp" ] && MCP_OK="$(printf '%s' "$_mcp" | grep -c 'Connected')"
fi`timeout 12` で12秒経ったら諦め、`?` のままにします。生成は止めません。
プロジェクトの状態
リポジトリごとにbranch・最終コミット日・未コミット変更数を取得して、ノードの色に反映させます。
proj_meta() {
local path="$1"
PROJ_EXISTS=0; PROJ_BRANCH="-"; PROJ_LAST="-"; PROJ_DIRTY=0
[ -d "$path" ] || return
PROJ_EXISTS=1
if git -C "$path" rev-parse --git-dir >/dev/null 2>&1; then
PROJ_BRANCH="$(git -C "$path" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '-')"
PROJ_LAST="$(git -C "$path" log -1 --format=%cd --date=format:%Y-%m-%d 2>/dev/null || echo '-')"
PROJ_DIRTY="$(git -C "$path" status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
fi
}未コミット変更があるプロジェクトはオレンジ、ディスクに存在しないプロジェクトは赤で表示します。
echo ' classDef dirty fill:#3a2a00,stroke:#e8a33d,color:#fff;'
echo ' classDef gone fill:#3a1a1a,stroke:#e06666,color:#fff;'朝Obsidianを開いただけで「どのプロジェクトにコミット漏れがあるか」が色で分かります。
Mermaidラベルのサニタイズ
ホスト名やチップ名に `()` や `[]` が入るとMermaidのパースが壊れます。全ラベルをサニタイズ関数に通します。
san() { printf '%s' "$1" | tr '"[]()#|<>' ' ' | tr '\n' ' ' | sed 's/ */ /g; s/ *$//'; }`Apple M4 Pro (14-core)` のような文字列も安全に出力されます。
生成される図のイメージ
graph LR
PC["💻 PC 環境<br/>MacBook · macOS 15.x"]
CC["🤖 Claude 環境<br/>plugins 14 · launchd 24"]
PJ["📦 プロジェクト環境<br/>11 repos"]
PC --> CC
CC -->|builds / runs| PJ
PC -.->|ローカル開発| PJClaude環境の内訳はこうなります(実測値)。
graph TD
CC["🤖 Claude Code"]
SK["🧩 Skills"]
SK --> SKa["auto: 77"]
SK --> SKp["plugin: 1004"]
CC --> SK
CC --> AG["🎭 Agents"]
CC --> PL["🔌 Plugins enabled"]
CC --> HK["🪝 Hooks"]
CC --> MCP["🔗 MCP connected"]
CC --> AUTO["⚡ launchd 24 jobs"]launchd設定
<key>StartCalendarInterval</key>
<array>
<dict><key>Hour</key><integer>4</integer><key>Minute</key><integer>50</integer></dict>
<dict><key>Hour</key><integer>8</integer><key>Minute</key><integer>10</integer></dict>
</array>
<key>RunAtLoad</key><false/>4:50に発火するのは、4:55のvault-auto-ingestより前に出力を置くためです。ingestがそのままObsidianへコミットしてくれるのでgit操作が不要。8:10は夜間スリープで4:50がスキップされた場合のキャッチアップです。冪等なので多重発火しても無害。`RunAtLoad: false` でplist読み込み時の即時発火は抑制します。
ログは1本のファイルに集約します。
<key>StandardOutPath</key>
<string>~/.claude/logs/env-map.launchd.log</string>
<key>StandardErrorPath</key>
<string>~/.claude/logs/env-map.launchd.log</string>最後の行に `generated (N lines)` が出ればOKです。
[2026-06-20 04:50:03] environment-map.md generated (218 lines)踏んだ落とし穴
- `set -e`を入れるとMCP timeout時点で終了 → `set -uo pipefail`のみ。MCPやgit失敗は`?`や`-`で継続
- NVM固定パスを書いたらNode更新で壊れた → `sort -V | tail -1`で最新版binを動的解決
- Mermaidラベルに`()`が入ると図がパースエラー → `san()`関数を全ラベルに通す。チップ名で踏んだ
- vault-ingestより遅れると生成物が前日分 → 4:50に先行発火、8:10にキャッチアップの二重設定
- プロジェクトパス変更に気づかない → `(未検出)`赤ノードで可視化。PRIの変更時にリストも更新する
- 出力先ディレクトリが存在しないと失敗 → `mkdir -p "$(dirname "$OUT")"` を `mv` の直前に置く
- ノードIDのカンマ結合が空白のままだとMermaidが壊れる → `sed 's/ /,/g'` で整形してから`class`に渡す
まとめ
- スキル・launchd・プロジェクトが二桁を超えたらMermaid図で俯瞰する場所を1枚用意する
- launchd起動時の最小PATHはスクリプト先頭で明示的に組み立てる。nvmはバージョン昇格を想定して動的解決
- MCP接続確認など遅い処理は**`timeout`で囲い、失敗は`?`でdegrade**させて完走を優先する
- 出力先をvault-ingest前に置くとバージョン管理もコミットも自動でついてくる
- プロジェクトの未コミット変更をノード色で可視化すると、朝Obsidianを開いただけでアクションが分かる
本稿と死活監視(automation-health-check)を組み合わせると、「何が動いていて・何が壊れていて・何が積み残されているか」の朝のスナップショットが1ページで揃います。
この記事が良かったら
「チップをリクエスト」で著者にチップの受け取り設定をお願いできます
