A 4-Minute Freeze, a Dead PATH, and 3 More: 5 launchd Pitfalls From Mapping 26 Automation Jobs A developer who rebuilt an autonomous Claude Code environment after a layoff has mapped 26 launchd automation jobs on his Mac to track which are alive and which are dead. He created env-map.sh, which generates a daily environment map in Obsidian, showing the PC, Claude, and project layers, and leverages git to record changes over time. The tool, which runs twice daily via launchd, has helped him manage his automation stack that now generates ¥1.2M/month in revenue. Automation doesn't fail loudly. It accumulates until nobody — including the person who wrote it — can say which jobs are still alive. I started with part-time gigs paying ¥100k/month, grew that to ¥600k, got laid off down to zero, and spent six months rebuilding an autonomous Claude Code environment that now brings in ¥1.2M/month in revenue. This post is about the problem that showed up along the way — "too much automation, no idea what's running and what's dead" — and how I solved it. Automation grows slowly. One launchd job at first, then two, and before you know it, 26. My Mac currently runs 26 com.shun. .plist jobs. vault-ingest fires at 4:55 every morning, daily brief runs at 7:30, github-scout cycles on a schedule, and on top of that env-map — the subject of this post — fires twice, at 4:50 and 8:10. That's already close to ten. The rest are Metrics aggregation, an auto-committer, and scheduled social media processing. The problem isn't that the count goes up. It's that you lose track of which ones are alive and which are dead . launchd silently restarts processes when they die. Error logs get dumped into ~/.claude/logs/ , but multiple plists write to the same directory, so things scroll away. Sometimes it takes two days before I notice "huh, the Metrics numbers haven't updated." Detection lags because there's no map. There's an important distinction here. "Looking at work logs" and "reading the environment" are different things. A work log is a record of what you did yesterday. An environment map is a snapshot of what is running, what exists, and what is dead at this exact moment . What I wanted was the latter. Concretely: when I open Obsidian, I wanted this morning's Mac state to be readable on one page . That my Claude environment has 47 plugins, 358 agents, and 112 auto-skills. That there are 11 projects, two of which don't exist on disk not found . That lead-finder has 75 uncommitted changes piled up. I didn't want to type commands every morning to check this — I wanted it to just be there when I opened Obsidian. There's a second mechanism at play. My vault gets a git commit from vault-ingest every morning. environment-map.md is overwritten daily too, so if lead-finder was "clean" yesterday and shows "✎75" today, the git diff shows it. I didn't design this on purpose. It fell out of two existing facts — "the Obsidian vault was already under git" and "environment-map.md gets written into that vault" — combining into a structure that records change automatically. It turned out to be more powerful than I expected: I can go back and trace what the environment looked like on any given day. env-map.sh splits the environment into PC layer, Claude layer, and project layer , generates Mermaid diagrams and tables, and writes them to wiki/meta/environment-map.md in the Obsidian vault. ┌─────────────────────────────────────────────────────────┐ │ launchd スケジューラ │ │ com.shun.env-map.plist │ │ → 04:50 / 08:10 の2回発火 │ │ → LowPriorityIO=true / Nice=10(バックグラウンド優先度) │ └───────────────────┬─────────────────────────────────────┘ │ /bin/bash ~/.claude/scripts/env-map.sh ▼ ┌─────────────────────────────────────────────────────────┐ │ env-map.sh │ │ │ │ ① PC 環境収集 │ │ sw vers / sysctl / df / CLI在否チェック │ │ │ │ ② Claude 環境収集 │ │ ~/.claude/settings.json から plugin数・hook数 │ │ find でskill/agent数カウント │ │ ls ~/Library/LaunchAgents/com.shun. .plist で件数 │ │ │ │ ③ プロジェクト環境収集 │ │ 11 repos × git branch/log/status │ │ │ │ ④ Mermaid図 + 表を $TMP に生成 │ │ → mv $TMP $OUT(アトミック書き込み) │ └───────────────────┬─────────────────────────────────────┘ │ ▼ ~/Documents/claude-obsidian/wiki/meta/environment-map.md │ ▼ vault-ingest(別ジョブ)がgitコミット → 差分 = 前日からの変化記録 set -e env-map.sh 冒頭 set -uo pipefail I use set -uo pipefail instead of set -e . The reason is spelled out in the comment: "run generation to completion no matter what happens individual collection failures degrade to ? ." For a script that runs via launchd in the early morning, MCP connections can be flaky, a project's git repository can be broken, and network-dependent commands can time out. If one failing command kills the entire script, you don't get a map that day. Replace the failed piece with ? and emit the rest normally — that's the degrade design. Here's how the MCP connection count is actually written: 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 Initialize with ? first, attempt with a 12-second timeout if the claude command exists, and overwrite with the connection count if it succeeds. If it can't connect, ? goes into the Mermaid diagram as-is. In today's actual output it reads MCP connected | ? , which tells you this path was taken. launchd's PATH at startup is extremely bare. It's a different thing from the PATH of a terminal launched via the GUI — no nvm-managed node, no Homebrew tools. The plist's EnvironmentVariables: