Show original
Enjoyed this article?
Use "Request tipping" to ask the author to set up tip receiving.

AI translation
--- "Autonomously Improving Claude Code Without Human Intervention" - A continuation from the previous work. This time, we're discussing the mechanism for becoming aware of environmental bloat. \ When skills accumulated underneath, launchd jobs increased, and projects exceeded double digits, I could no longer immediately answer "how many layers the whole system has and what's currently running." Every morning \…
Enjoyed this article?
Use "Request tipping" to ask the author to set up tip receiving.
"Autonomously Improving Claude Code Without Humans" 前作 continued. This time, we're discussing mechanisms for recognizing environmental bloat.
`~/.claude/` As skills accumulated underneath, launchd jobs multiplied, and projects exceeded double digits, I could no longer instantly answer "how many layers does the whole system have and what's running now?" Rather than running `ls` every morning, I aimed for a state where opening Obsidian gives me an overview, so I wrote `~/.claude/scripts/env-map.sh`.
Here's a snapshot of the current state:
Last-update times and git states vary across skills, jobs, and projects, and the cost of checking them accumulates. I wanted a single page that could answer "what did today's autopilot do?" and "what's the branch status of that project?"
The diagram has 3 layers.
Output destination is Obsidian Vault (`~/Documents/claude-obsidian/wiki/meta/environment-map.md`). vault-auto-ingest (4:55) picks it up and auto-commits, so version control comes for free.
The script design philosophy is written in comments.
# 何が起きても生成を完走させる(個別の収集失敗は "?" で degrade)。set -e は使わない。
set -uo pipefailThe key point is not using `set -e`. Even if something partially fails—like MCP connection checks—we fill in `?` and output to completion.
When launched from launchd, PATH is only `/usr/bin:/bin:/usr/sbin:/sbin` or so. `node`, `claude`, and `jq` can't be found. We explicitly construct PATH at the script's beginning.
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"Since nvm changes paths when you upgrade the Node version, we dynamically resolve the latest binary with `sort -V | tail -1`. Hard-coding paths breaks on the next version upgrade.
The minimal launchd PATH trips up "all CLIs installed by users." Stacking fallbacks in order—`~/.local/bin` (uv, claude, etc.) → Homebrew → nvm—ensures you hit something in any environment setup.
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 requires special attention. `claude mcp list` attempts live connections, so startup is slow and failures happen. We cap it with a timeout.
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')"
fiAfter 12 seconds via `timeout 12`, we give up and leave it as `?`. Generation doesn't stop.
We fetch branch, last commit date, and uncommitted change count per repository, reflecting them in node colors.
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
}Projects with uncommitted changes show orange; projects that don't exist on disk show red.
echo ' classDef dirty fill:#3a2a00,stroke:#e8a33d,color:#fff;'
echo ' classDef gone fill:#3a1a1a,stroke:#e06666,color:#fff;'Just opening Obsidian in the morning, you can see at a glance which projects have uncommitted changes.
If hostnames or chip names contain `()` or `[]`, Mermaid's parser breaks. We pass all labels through a sanitization function.
san() { printf '%s' "$1" | tr '"[]()#|<>' ' ' | tr '\n' ' ' | sed 's/ */ /g; s/ *$//'; }Strings like `Apple M4 Pro (14-core)` are output safely.
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 -.->|ローカル開発| PJThe Claude environment breakdown looks like this (measured values):
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"]<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/>It fires at 4:50 to place output before vault-auto-ingest at 4:55. ingest commits it directly to Obsidian, so no git operations needed. 8:10 is a catch-up in case 4:50 is skipped due to overnight sleep. It's idempotent, so multiple fires are harmless. `RunAtLoad: false` suppresses immediate firing on plist load.
Logs are consolidated into a single file.
<key>StandardOutPath</key>
<string>~/.claude/logs/env-map.launchd.log</string>
<key>StandardErrorPath</key>
<string>~/.claude/logs/env-map.launchd.log</string>If `generated (N lines)` appears on the last line, you're good.
[2026-06-20 04:50:03] environment-map.md generated (218 lines)Combining this article with 死活監視(automation-health-check) gives you a single-page morning snapshot of "what's running, what's broken, and what's left undone."
Lily (@bokuwalily) — Individual developer. Building automation infrastructure with Claude Code while mass-producing iOS apps and web services.
◼︎ Apps I've made are summarized at **ポートフォリオ** 📱
◼︎ New releases and development behind-the-scenes shared on X **@bokuwalily** 🐦
◼︎ OSS: **github.com/bokuwalily** 🐙
Your ❤️ and shares are my motivation!
「Claude Codeを無人で自律改善させる」前作との続きで、今回は環境の肥大化を自覚する仕掛けの話です。
`~/.claude/` 配下にスキルが積み重なり、launchdジョブが増え、プロジェクトが二桁を超えたとき、「全体が何層あって今何が動いているか」を即答できなくなりました。毎朝 `ls` を打つのではなく、Obsidianを開けば俯瞰できる状態を目指して、`~/.claude/scripts/env-map.sh` を書きました。
現状のスナップショットはこうです。
スキル・ジョブ・プロジェクトそれぞれの最終更新やgit状態がバラバラで、毎回確認しに行くコストが積み重なります。「今日のautopilotは何をした?」「あのプロジェクトのブランチは?」を1ページで答えられる場所が欲しかった。
図にするのは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は `/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 の順でフォールバックを積んでおくと、どの環境構成でも当たります。
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のパースが壊れます。全ラベルをサニタイズ関数に通します。
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"]<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)本稿と死活監視(automation-health-check)を組み合わせると、「何が動いていて・何が壊れていて・何が積み残されているか」の朝のスナップショットが1ページで揃います。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
◼︎作ったアプリは **ポートフォリオ** にまとめています📱
◼︎新着・開発の裏側は X **@bokuwalily** で発信しています🐦
◼︎OSS: **github.com/bokuwalily**🐙
皆さんの ❤️ やシェアが励みになります!