30 Minutes Every Morning Lost to Re-Explaining Myself: The 20-Line Auto-Brief That Fixed It A developer built a shell-script-based morning brief system to eliminate the daily 30-minute context loss when restarting Claude Code sessions. The system automatically injects a 20-30 line summary of the environment's current state into each new session, covering live probes, repository status, and API costs, which the developer credits for helping scale solo revenue to ¥1.2M per month. The thing that moved my numbers wasn't a clever idea — it was a shell script that tells me where I stand at 8:00 a.m. every day. I started with a ¥100k/month side hustle in college, stacked gigs up to ¥600k/month, then got laid off and went back to zero. Six months later, after building an autonomous Claude Code environment, I'm at ¥1.2M/month in revenue. The one thing I built before everything else was the morning brief. Claude Code operates per conversation session. The moment you close the browser tab, the context of the previous session is gone. The next time you open it, Claude starts from a blank slate — "nice to meet you." This is quietly fatal. Say yesterday you were halfway through digging into "autolike's Gumroad webhook doesn't go through on staging." You fire up Claude Code the next morning intending to pick up where you left off, but Claude has none of yesterday's context. You end up re-explaining "what is autolike?", "what webhook?", "where did we get stuck?" from scratch. Two to three minutes per round trip, and once that piles up daily, more than 30 minutes a week disappears into "explanation round trips." But the real problem isn't just time. It's that the precision of the explanation drifts every time . The concrete state that yesterday's self had a grip on — "there are 3 files with uncommitted changes," "the production probe is returning 500," "cost consumption is near the weekly limit" — has already faded from today's memory. The coarser the information you can give Claude, the coarser Claude's opening move. The root of this problem is the environment, not the work . To stack up monthly revenue in solo development, you need to be in a state where you can instantly judge "where do I move from today?" every morning. Which state each of 10 repositories is in, whether 4 products survived the night in production, how far this week's API consumption cost has climbed — checking all of that manually every morning burns 30 minutes on its own. And even if you automate it, copy-pasting the results into Claude Code every time is structurally the same problem. The solution is automating context injection. Every time Claude starts a session, a 20–30 line summary describing the current state of the environment gets automatically inserted into the system prompt. Claude starts the session holding "today's state" instead of a blank page. What I built for this is the combination of daily-brief.sh a script that collects the environment's current state and cc-brief.sh a script that injects the collected information into the Claude Code session . The former produces the data; the latter delivers it to Claude. This two-stage structure wiped out the morning "explanation round trip" entirely. Before building the mechanism, what I agonized over was the design question of "what to put in it." Cram in too much and the injected context balloons, increasing the tokens Claude processes and driving up cost. Trim too much and it's meaningless. Looking at the actual script, you can see it's organized into 7 categories . 1. Live probes are the production products alive Hit the product URLs with curl and get the HTTP status. But a single shot can produce false reports. On Vercel deploys with slow cold starts, the first request would time out and a live product would be misdetected as "dead." To avoid this, the script is designed with 3 retries and a 15-second timeout. for attempt in 1 2 3; do code=$ curl -s -o /dev/null -w "%{http code}" --max-time 15 "$url" 2 /dev/null || echo 000 if "$code" = "200" || "${code:0:1}" = "3" ; then break; fi sleep 2 done The products checked here are four: the job-hunting tracker, the job-hunting navigator, the graduation planner, and AETHERIA RPG. On top of that, autolike's license API gets a POST with the dummy key HEALTHCHECK-DUMMY , and the environment-variable configuration state is determined by whether it returns "server configuration error" or "license not found." This distinction matters: the former means the Gumroad connection is broken and the billing flow has stopped. 2. Improvement log how many autonomous improvements were recorded yesterday YESTERDAY=$ date -v-1d +%Y-%m-%d YESTERDAY COUNT=$ grep -cE "^ ${YESTERDAY} " ~/.claude/improvements/log.md 2 /dev/null || echo 0 improvements/log.md is the log of Claude Code autonomously improving the environment. Seeing yesterday's count and the cumulative count every morning lets you confirm at a glance whether the autonomous cycle is turning. 3. Hook latency last 24 hours Check whether Claude Code's hooks are completing within the expected time. When hooks lag, session startup stalls, so the 24-hour delay distribution goes into the brief. 4. API cost last 7 days cost-summary.sh aggregates consumption cost for the last 7 days. If cost is approaching the weekly ceiling, it changes the day's judgment on workload and delegation target Codex or Sonnet . 5. launchd status are the automation tasks running launchctl list | grep com.shun | awk '{printf "- %s last exit=%s \n", $3, $2}' | head -15 Check whether the launchd jobs that run at fixed times each day terminated normally. A non-zero last exit is a sign that something is broken. 6. git status current state of 10 repositories for repo in \ ~/Documents/projects/closet-os \ ~/dev/shukatsu-tracker \ ~/dev/seo-affiliate-site \ ~/fantasy-mmo \ ~/hosei-grad-planner \ ~/Projects/autolike-license-server \ ~/Projects/instagram-auto-liker \ ~/Projects/x-like-demo \ ~/dev/jobform-autofill \ ~/lead-finder; do ... echo "- ${name} ${branch} : ${uncommitted} 未コミット — ${last commit} " done For 10 repositories, it outputs branch name, uncommitted file count, and last commit summary on one line each. "Which repository did work stall in yesterday" becomes visible as a list. 7. auto-skill status how many autonomous skills have been generated Since I run a mechanism where Claude Code generates its own skills, the current count and the last harvest timestamp go into the brief. In fact, this brief does not include a task list . I initially tried to inject the todo list too, but dropped it for two reasons. The first is update cost. Tasks are born, die, and shift priority inside conversations. A task list written into a static file quickly diverges from "today's reality." The second is the question of Claude's role. If you inject "here are today's tasks" in advance, Claude gets pulled toward that list. The room to re-discuss the day's true priorities narrows. The brief is a description of state, not a preemption of instructions. That's the crux of the design. Similarly, it doesn't include the full text of Obsidian notes. Only the first 30 lines of hot.md are read in by cc-brief.sh . The Obsidian vault holds thousands of notes, so injecting the full text would be a waste of tokens. The design is to consolidate just the key points of "what's moving this week" into hot.md, and read only that. Here's how the two scripts work together. launchd │ 毎日 8:00 / 10:30 / ログイン時 ▼ daily-brief.sh ├─ probe url × 4プロダクト(3回リトライ・max-time 15s) ├─ probe autolike × 2(instagram / x) ├─ probe scout ── github-scout-latest.md の mtime + 行数チェック ├─ probe affiliate audit ── audit-YYYY-MM-DD.log 読み取り ├─ automation-health.sh(timeout 60s) ├─ hook-latency-report.sh(直近24h・timeout 60s) ├─ cost-summary.sh(直近7d・timeout 60s) ├─ launchctl list | grep com.shun ├─ git log + status × 10リポジトリ ├─ improvements/log.md(昨日/累計件数) ├─ auto-skills 件数 └─ ディスク使用量(~/.claude・disabled-cache) │ ├─→ ~/.claude/logs/daily-brief-latest.md ←── cc-brief.sh が参照 ├─→ ~/Desktop/Daily Brief/today-brief- .md └─→ vault/wiki/briefs/daily/today-brief- .md (マーカー < -- daily-brief YYYYMMDD -- で二重追記防止) UserPromptSubmit フック │ Claude Code の全セッション・メッセージ送信ごとに自動起動 ▼ cc-brief.sh ├─ ccusage blocks --active --json │ → "5h block: {OUT K}k out / {REMAIN}min left / ≈${COST} API-equiv" ├─ ccusage weekly --json │ → "Week: {WOUT M}M out / ≈${WCOST} API-equiv" ├─ settings.json → plugins / permissions / hooks の件数 ├─ claude mcp list → Connected / Failed / Needs-auth の件数 ├─ improvements/log.md → 直近1エントリ(tail -r で末尾から取得) ├─ MEMORY.md → 末尾12行 └─ hot.md → 先頭30行 │ ▼ Claude Code の system-reminder に注入 → Claudeは白紙でなく「今日の現在地」を持ってセッションを開始する Inside daily-brief.sh , three child scripts get called: automation-health.sh , hook-latency-report.sh , and cost-summary.sh . If any of them hangs for some reason, generation of the entire brief stops. Cases where a socket was dead at dawn, or where things jammed waiting on a connection to an external API, actually happened. To prevent this, the script wraps child process calls in a run to function. TIMEOUT BIN="/opt/homebrew/bin/timeout" -x "$TIMEOUT BIN" || TIMEOUT BIN="" run to { local s=$1; shift; if -n "$TIMEOUT BIN" ; then "$TIMEOUT BIN" --kill-after=15 "$s" "$@"; else "$@"; fi; } Each child script is called in the form run to 60 ... 2 &1 | head -10 . If it doesn't finish within 60 seconds it gets killed, and 15 seconds after that it's force-terminated. Output is truncated with head -10 , so even if a child script emits a flood of output, the injected context doesn't balloon. I run Claude Code on the MAX plan, and the ccusage command can retrieve the token balance for the 5-hour block. ACTIVE=$ "$CCUSAGE" blocks --active --json 2 /dev/null | jq '.blocks | select .isActive ' if -n "$ACTIVE" ; then OUT=$ echo "$ACTIVE" | jq -r '.tokenCounts.outputTokens' REMAIN=$ echo "$ACTIVE" | jq -r '.projection.remainingMinutes' COST=$ echo "$ACTIVE" | jq -r '.costUSD | floor' OUT K=$ OUT / 1000 echo "5h block: ${OUT K}k out / ${REMAIN}min left / ≈\$${COST} API-equiv" fi It divides outputTokens by 1000 to display in k-token units, and shows the remaining minutes in the block via projection.remainingMinutes . Knowing this at session start makes the judgment "can I start a heavy cross-cutting investigation right now?" instantaneous. With 30 minutes left, I narrow to light tasks and push heavy investigation to the next block. The weekly aggregate is retrieved from ccusage weekly --json , and output tokens are rounded to MB units one decimal place for display. WOUT M=$ jq -n --argjson n "$WOUT" '$n / 1000000 | . 10 | round / 10' echo "Week: ${WOUT M}M out / ≈\$${WCOST} API-equiv" The design where cc-brief.sh reads only the last 12 lines from MEMORY.md is intentional trimming. -f "$HOME/.claude/projects/$ echo $HOME | sed 's|/|-|g' /memory/MEMORY.md" && \ cat "$HOME/.claude/projects/$ echo $HOME | sed 's|/|-|g' /memory/MEMORY.md" | tail -12 MEMORY.md is an append-style index, where newer memories get added at the end. In other words, the last 12 lines mean "recently updated memories." Reading all lines would be complete, but tokens balloon. By injecting only the most recent context, Claude starts the session holding only the memories relevant to this week's work. /bin/bash directly As noted in a comment in daily-brief.sh , when executing via a launchd plist, /bin/bash is specified directly rather than going through /bin/zsh . Desktop / ~/Documents vault は TCC 保護領域 → plist は /bin/bash 直起動(FDA付与済み)。 /bin/zsh 経由だと FDA 未付与で書き込みに失敗する。 Apple's Full Disk Access FDA is granted per process. Even if you've granted FDA to /bin/bash , a process launched via /bin/zsh is treated separately and writes to Desktop or Documents get denied. Having fallen into this trap, the plist is designed to list /bin/bash and the absolute path of daily-brief.sh directly in ProgramArguments . Also, the environment launchd runs in has a different PATH than the terminal. To use homebrew commands, the script explicitly supplements PATH at the top. export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH" Forget this and timeout or jq dies with "command not found." This is the classic cause of burning time debugging launchd scripts. daily-brief.sh writes the generated brief to 3 places. ~/.claude/logs/daily-brief-latest.md — the canonical copy that cc-brief.sh reads ~/Desktop/Daily Brief/today-brief-YYYYMMDD.md — the delivery for a human to check in the morning vault/wiki/briefs/daily/today-brief-YYYYMMDD.md — the long-term archive in Obsidian Delivery to Desktop is "for human confirmation." You can read the brief from Finder without opening a terminal. The Obsidian archive is "for looking back a week later," so you can reference past product states and cost trends. To prevent duplicate appends, the marker < -- daily-brief YYYYMMDD -- is embedded in each file, so even if a brief for the same date gets written twice, it's kept as one. php MARK="< -- daily-brief ${DATE} -- " LC ALL=C grep -qF -- "$MARK" "$f" 2 /dev/null && continue LC ALL=C is attached so that even if invalid bytes have crept into the file, grep won't treat it as binary and will reliably detect the marker. Finally, the brief is also posted to the Discord 04 daily-brief channel. -s "$OUT" && /usr/bin/python3 "$HOME/.discord/post brief.py" daily-brief "$OUT" /dev/null 2 &1 || true It's designed so that a failed Discord post doesn't stop the whole brief generation, by ignoring it with || true . The priority is explicit: generating the brief is primary, posting to Discord is a byproduct. cc-brief.sh is a 53-line script, but internally it's divided into 7 sections. Let's dissect the parts other than the "USAGE section" touched on earlier, in order. ENV section SETTINGS="$HOME/.claude/settings.json" echo "Plugins enabled: $ jq -r '.enabledPlugins | length' "$SETTINGS" " echo "Permissions allow: $ jq -r '.permissions.allow | length' "$SETTINGS" " echo "Hooks: $ jq -r '.hooks | keys | length' "$SETTINGS" events" It outputs plugin count, permission count, and hook event count from settings.json , one line each. The point is that they're numbers. If the output "8 plugins / 43 allow / 6 events" is the same every morning, you know immediately that no setting you didn't touch last night has changed. Conversely, an unfamiliar number is a sign that something changed. Detecting "I supposedly added a hook, but events didn't change" also starts from this one line. MCP section MCP=$ "$HOME/.nvm/versions/node/v24.13.0/bin/claude" mcp list 2 &1 || claude mcp list 2 &1 echo "Connected: $ echo "$MCP" | grep -c Connected / \ Failed: $ echo "$MCP" | grep -c 'Failed to connect' / \ Need-auth: $ echo "$MCP" | grep -c 'Needs auth' " It counts MCP server connection states in 3 categories. What's noteworthy is how it's invoked. It first tries the absolute path $HOME/.nvm/versions/node/v24.13.0/bin/claude , and falls back to the claude command on failure. Because cc-brief.sh is called from the UserPromptSubmit hook, there's no guarantee the shell's PATH is fully inherited. Making the absolute path the first candidate ensures the claude binary under nvm gets used. There was a case where Failed: 1 actually showed up and tipped me off. A script that ran in the middle of the night mistakenly overwrote an MCP server config file, and the connection was down as of the next morning's session start. That's a time window I'd never have noticed if I were running claude mcp list manually in the terminal. One second into the session, you know "something is off." RECENT IMPROVEMENTS section -f "$HOME/.claude/improvements/log.md" && \ tail -r "$HOME/.claude/improvements/log.md" 2 /dev/null \ | awk '/^ /{count++; if count 1 exit} {print}' \ | tail -r \ | head -30 This one-liner is the processing that "extracts only the latest entry from improvements/log.md ." The structure is distinctive, so let me explain. improvements/log.md is an append-style log where each entry starts with a YYYY-MM-DD HH:MM header. The end of the file is the latest entry. To extract only the latest entry, you need logic that "reads in reverse from the end and stops when it finds the first section boundary." tail -r is a macOS-specific command that outputs a file in reverse line order tac in the GNU world . After reversing, awk stops output at the point it finds the section boundary a line for the second time. This yields exactly one trailing entry of the file, in reverse. Finally tail -r again restores forward order, and head -30 truncates. "Why not grep -A 30 "the last header" ?" — you'd have to grep the entire file once to identify the header line, and that gets slower as entries grow. Reverse + awk always finishes reading just a few lines from the end, so speed doesn't degrade even past 1000 log entries. MEMORY INDEX section -f "$HOME/.claude/projects/$ echo $HOME | sed 's|/|-|g' /memory/MEMORY.md" && \ cat ... | tail -12 This path construction has a quirk. Claude Code saves per-project memory in ~/.claude/projects/