Auto-Generating a Mermaid Map of My Claude Code Setup Every Morning

작성자

카테고리:

← 피드로
DEV Community · Lily · 2026-07-14 개발(SW)

Lily

Lily

Posted on Jul 14 • Originally published at dev.to

This is a follow-up to my previous post on getting Claude Code to improve itself unattended. This time the topic is a different problem: a mechanism to stay aware of your environment as it balloons.

Skills piled up under ~/.claude/, launchd jobs multiplied, and once my project count crossed into the double digits, I could no longer answer off the top of my head “how many layers is the whole thing, and what’s running right now?” Rather than typing ls every morning, I wanted a state where opening Obsidian gives me the bird’s-eye view. So I wrote ~/.claude/scripts/env-map.sh.

The problem: as the environment grows, you lose the big picture

Here’s the current snapshot.

  • Plugin bundle skills: 1,004
  • Self-generated skills (auto/): 77
  • launchd jobs (com.shun.*.plist): 24
  • Projects I’m tracking: 11

The last-modified times and git state of skills, jobs, and projects are all over the place, and the cost of going to check each one adds up every time. I wanted one place that could answer “what did today’s autopilot do?” and “what branch is that project on?” on a single page.

Design: three-layer Mermaid + output to Obsidian

The diagram covers three layers.

Layer What’s collected 💻 PC environment macOS version, chip, RAM, free disk, key CLIs 🤖 Claude environment skill count, agent count, plugin count, hook event count, MCP connection count, launchd job count 📦 Project environment branch, last commit date, and uncommitted change count for known repos

The output destination is the 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 along for free.

The script’s design intent is written in the comments.

# 何が起きても生成を完走させる(個別の収集失敗は "?" で degrade)。set -e は使わない。
set -uo pipefail

Enter fullscreen mode Exit fullscreen mode

The key is not using set -e. Even if something partially fails, like an MCP connection check, it fills in ? and keeps producing output all the way to the end.

Working around launchd’s minimal PATH

When launched from launchd, PATH is roughly only /usr/bin:/bin:/usr/sbin:/sbin. Neither node, nor claude, nor jq is found. So at the top of the script I explicitly assemble 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"

Enter fullscreen mode Exit fullscreen mode

Because nvm’s path changes when you bump the node version, sort -V | tail -1 resolves the latest version’s bin dynamically. Hardcoding a fixed path breaks on the next version bump.

:::message
Getting stuck on launchd’s minimal PATH affects “any CLI the user installed.” Stacking fallbacks in the order ~/.local/bin (uv, claude, etc.) → Homebrew → nvm hits the mark across any environment configuration.
:::

What to put in the diagram so the overview actually helps

Collecting the Claude environment

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 ' ')"

Enter fullscreen mode Exit fullscreen mode

MCP is the one thing that needs care. claude mcp list attempts a live connection, so it’s slow to start and can fail. I contain 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')"
fi

Enter fullscreen mode Exit fullscreen mode

With timeout 12, it gives up after 12 seconds and leaves ? in place. It doesn’t halt generation.

Project state

For each repo, it fetches the branch, last commit date, and uncommitted change count, and reflects them in the node color.

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
}

Enter fullscreen mode Exit fullscreen mode

Projects with uncommitted changes show up in orange, and projects that don’t exist on disk show up in red.

echo '  classDef dirty fill:#3a2a00,stroke:#e8a33d,color:#fff;'
echo '  classDef gone  fill:#3a1a1a,stroke:#e06666,color:#fff;'

Enter fullscreen mode Exit fullscreen mode

Just by opening Obsidian in the morning, the color tells me “which project has commits I forgot to make.”

Sanitizing Mermaid labels

If a host name or chip name contains () or [], Mermaid’s parsing breaks. I run every label through a sanitize function.

san() { printf '%s' "$1" | tr '"[]()#|<>' '        ' | tr '\n' ' ' | sed 's/  */ /g; s/ *$//'; }

Enter fullscreen mode Exit fullscreen mode

Even a string like Apple M4 Pro (14-core) is output safely.

What the generated diagram looks like

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 -.->|ローカル開発| PJ

Enter fullscreen mode Exit fullscreen mode

The breakdown of the Claude environment 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"]

Enter fullscreen mode Exit fullscreen mode

launchd configuration

<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/>

Enter fullscreen mode Exit fullscreen mode

It fires at 4:50 in order to place the output before the 4:55 vault-auto-ingest. Since ingest commits it straight to Obsidian, no git operations are needed. The 8:10 run is a catch-up for cases where 4:50 was skipped due to overnight sleep. It’s idempotent, so firing multiple times is harmless. RunAtLoad: false suppresses the immediate fire 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>

Enter fullscreen mode Exit fullscreen mode

If the last line shows generated (N lines), you’re good.

[2026-06-20 04:50:03] environment-map.md generated (218 lines)

Enter fullscreen mode Exit fullscreen mode

Pitfalls I hit

  • Adding set -e exits at the MCP timeoutset -uo pipefail only. Continue past MCP or git failures with ? or -
  • Writing a fixed NVM path broke it on a Node update → dynamically resolve the latest version’s bin with sort -V | tail -1
  • A () in a Mermaid label causes a parse error in the diagram → run the san() function on every label. I got bitten by a chip name
  • Falling behind vault-ingest makes the artifact the previous day’s → dual setup: fire early at 4:50, catch up at 8:10
  • Not noticing a project path change → visualize with a (not found) red node. Update the list when PROJECTS changes
  • It fails if the output directory doesn’t exist → put mkdir -p "$(dirname "$OUT")" right before the mv
  • If the comma-joined node IDs are left with spaces, Mermaid breaks → clean them up with sed 's/ /,/g' before passing them to class

Wrap-up

  • Once skills, launchd, and projects cross into the double digits, prepare a single place to survey them with a Mermaid diagram
  • Assemble PATH explicitly at the top of the script for launchd’s minimal PATH at startup. Resolve nvm dynamically, assuming version bumps
  • Wrap slow operations like MCP connection checks in timeout, and degrade failures to ? to prioritize completing the run
  • Placing the output before vault-ingest gets you version control and commits automatically
  • Visualizing projects’ uncommitted changes by node color means just opening Obsidian in the morning tells you the action to take

Combining this with liveness monitoring (automation-health-check) gives you a morning snapshot on a single page of “what’s running, what’s broken, and what’s left undone.”

Written by **Lily* — I ship iOS apps and automate my content stack with Claude Code.
Follow along: Portfolio · X · GitHub*

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다