[object Object]

← back to Checkup

checkup skill: Claude Code environment-hygiene checkup (from bcherny /checkup idea)

6c1f7e2a818249dd49ab22829f066517fb8e0950 · 2026-07-16 18:50:54 -0700 · Steve

Files touched

Diff

commit 6c1f7e2a818249dd49ab22829f066517fb8e0950
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 16 18:50:54 2026 -0700

    checkup skill: Claude Code environment-hygiene checkup (from bcherny /checkup idea)
---
 .gitignore               |   8 +++
 README.md                |  15 +++++
 SKILL.md                 |  83 +++++++++++++++++++++++
 references/actions.md    |  89 ++++++++++++++++++++++++
 scripts/checkup-audit.sh | 172 +++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 367 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6e75e3c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+# checkup
+
+A Claude Code environment-hygiene skill, adapted from Boris Cherny's built-in
+`/checkup` (announced [2026-07-16](https://x.com/bcherny/status/2074997570317779038)),
+tailored to Steve's machine.
+
+Run `/checkup` in a Claude Code session, or directly:
+
+```bash
+bash scripts/checkup-audit.sh          # read-only audit
+bash scripts/checkup-audit.sh --deep   # + cross-check skills vs 30d of session logs
+```
+
+The audit only reports. Every change is confirmed, and `settings.json` changes
+are surfaced for Steve (never auto-applied). See `SKILL.md` + `references/actions.md`.
diff --git a/SKILL.md b/SKILL.md
new file mode 100644
index 0000000..995f52b
--- /dev/null
+++ b/SKILL.md
@@ -0,0 +1,83 @@
+---
+name: checkup
+description: Claude Code environment-hygiene checkup for Steve's machine, adapted from Boris Cherny's /checkup. Audits and (with confirmation) cleans up the Claude setup — archives unused skills/MCPs to save context, dedups local vs checked-in CLAUDE.md, breaks up bloated CLAUDE.md/MEMORY.md, flags slow hooks, reports a Claude Code version update, recommends auto mode, and routes frequently-denied read-only commands to /fewer-permission-prompts. Also verifies the keepawake guard and launchd job health. Use when Steve says "/checkup", "checkup", "clean up my claude setup", "tune up claude code", "my context is bloated", "is my CLAUDE.md too big", "trim MEMORY.md", "which skills am I not using", "why is startup slow", or wants a maintenance pass on the Claude Code environment. READ-ONLY audit first; every change is confirmed, and settings.json changes are surfaced for Steve (never auto-applied).
+---
+
+# checkup
+
+## Overview
+
+`/checkup` gives Steve's Claude Code install a maintenance pass — the same idea
+as Boris Cherny's built-in `/checkup` (announced 2026-07-16), tailored to Steve's
+machine (280+ skills, a large global `CLAUDE.md`, oversized `MEMORY.md` indexes,
+12 MCP servers, many hooks and launchd jobs). It **audits first, changes nothing
+without confirmation**, and respects Steve's hard rule that `settings.json` is
+classifier-blocked for Claude.
+
+## Workflow
+
+Run in three phases: **audit → confirm → apply**.
+
+### 1. Audit (always first, read-only)
+
+```bash
+bash scripts/checkup-audit.sh          # fast
+bash scripts/checkup-audit.sh --deep   # also cross-check skills against 30d of session logs (slower)
+```
+
+The script changes nothing. It reports, tagging each finding `[AUTO ...]`,
+`[GATED ...]`, or a plain FINDING:
+
+1. **Claude Code version** — installed vs latest on npm.
+2. **Root/global CLAUDE.md** bloat (candidates to nest into sub-CLAUDE.md + skills).
+3. **Local vs checked-in CLAUDE.md** duplicate lines (dedup).
+4. **MEMORY.md** files over the ~24KB harness limit + over-long index lines.
+5. **Skills** — count + least-recently-used (and `--deep` never-seen-in-logs) candidates.
+6. **MCP servers** — configured servers (top-level + per-project).
+7. **Hooks** — the hook commands, to eyeball for slow ones.
+8. **Auto mode + permissions** — current mode + allowlist size.
+9. **Steve goodies** — keepawake guard alive?, screensaver idleTime, launchd `com.steve.*` jobs with a non-zero exit.
+
+### 2. Interpret & confirm
+
+Read the audit against `references/actions.md`, which holds the full per-finding
+playbook and — most importantly — the **AUTO vs GATED** classification. Then
+present Steve ONE grouped summary:
+
+- **AUTO (with confirm)** — reversible local edits Claude can make: dedup a local
+  CLAUDE.md, nest a bloated CLAUDE.md, trim a MEMORY.md index (move detail into
+  its topic file, never delete information), archive a specific unused skill (move
+  to `~/.claude/skills-archive/`, never `rm`).
+- **GATED (Steve paste / sanctioned skill)** — anything touching `settings.json`
+  (turn off a hook, enable auto mode) → route through the **`update-config`** skill
+  or surface a paste; permission pre-approval → run **`/fewer-permission-prompts`**;
+  CC version update, MCP removal, and `sudo pmset` repairs → surface for Steve.
+- **CLEAN** — what's already healthy (e.g. keepawake alive).
+
+Ask for a single go/no-go (or a per-item pick). **Do not apply anything before Steve confirms** — this mirrors Boris's `/checkup`, which "confirms with you before making any changes."
+
+### 3. Apply
+
+- Apply only the AUTO items Steve approved. After each cohesive change, commit in
+  the affected repo (`git add -A && git commit`, author `steve@designerwallcoverings.com`).
+- Hand back GATED items as ready-to-paste blocks or by invoking the sanctioned
+  skill. Never hand-edit `settings.json` / `settings.local.json`.
+- End with a `$0 (local)` cost line — the whole checkup is free/local.
+
+## Key rules (do not violate)
+
+- **Audit before acting.** Never propose a fix you didn't see in a fresh audit.
+- **Move, don't summarize.** Nesting a CLAUDE.md or trimming a MEMORY.md index
+  RELOCATES text verbatim into a linked file — it never paraphrases away a rule.
+- **Archive, don't delete.** Unused skills move to `~/.claude/skills-archive/`.
+- **settings.json is off-limits to Claude** — `update-config` / `/fewer-permission-prompts` / Steve-paste only.
+- **Confirm every change.** No silent edits, even AUTO ones.
+
+## Notes
+
+- If Claude Code later ships a native `/checkup`, that built-in takes precedence
+  when Steve types `/checkup`; this skill remains the Steve-tailored superset
+  (keepawake + launchd + MEMORY.md + archive-not-delete goodies) and can be
+  invoked by name.
+- Details, exact procedures, and the full AUTO/GATED matrix live in
+  `references/actions.md` — read it during phase 2.
diff --git a/references/actions.md b/references/actions.md
new file mode 100644
index 0000000..f8f7cd6
--- /dev/null
+++ b/references/actions.md
@@ -0,0 +1,89 @@
+# checkup — action matrix & procedures
+
+The audit only *reports*. This file tells Claude how to turn each finding into a
+change, and — critically — which changes Claude may apply itself (AUTO) versus
+which must be handed to Steve or routed through a sanctioned skill (GATED).
+
+## The gate rule (Steve-specific, non-negotiable)
+
+`settings.json` / `settings.local.json` cannot be edited by Claude directly —
+Steve's classifier blocks autonomous writes to them. Anything that changes those
+files (turn off a hook, enable auto mode, add a permission) is **GATED**:
+- Prefer routing through the **`update-config`** skill (the harness's sanctioned
+  path for settings changes) or **`/fewer-permission-prompts`** (permissions).
+- If neither applies, surface the exact change as a copy-paste block for Steve.
+Never hand-edit the settings files.
+
+Likewise GATED: `sudo pmset` (surface the paste), deleting/archiving a skill,
+purging git history, the CC version update, and enabling auto mode.
+
+## Per-finding playbook
+
+### 1. Unused skills / MCPs / plugins  → save context
+- **Skills.** The audit lists least-recently-modified + (with `--deep`)
+  never-seen-in-logs candidates. mtime is a *weak* signal — Steve has 280+ skills
+  and rarely-fired ones (restore, canaries) are still wanted. **AUTO w/ confirm:**
+  after Steve okays a specific list, "archive" by moving the dir to
+  `~/.claude/skills-archive/<name>` (reversible) — do NOT `rm`. Never archive on
+  mtime alone.
+- **MCPs.** Removing an MCP server edits `~/.claude.json` (not settings.json, so
+  not classifier-blocked) but it's still outward-affecting — **GATED: confirm each
+  removal with Steve**, then remove the block from `.claude.json` (write a `.bak`
+  first, mirroring the secrets-skill pattern).
+
+### 2. Dedup local vs checked-in CLAUDE.md
+- **AUTO w/ confirm.** If `CLAUDE.local.md` repeats lines already in the committed
+  `CLAUDE.md`, delete the duplicated lines from the *local* file only. Show the
+  diff, get a yes, apply, `git add -A && git commit` in that repo.
+
+### 3. Break up a bloated root CLAUDE.md
+- Global `~/.claude/CLAUDE.md` (~18KB) and any project CLAUDE.md over ~12KB.
+- **AUTO w/ confirm.** Extract cohesive sections into either nested
+  `<subdir>/CLAUDE.md` files (loaded only when working in that subtree) or into
+  skills, leaving a one-line pointer behind. Preserve every rule verbatim — this
+  is a *move*, never a *summarize*. Commit after.
+- **MEMORY.md is the same class of problem** and the highest-value target here:
+  the harness warns when `MEMORY.md` exceeds ~24KB and only loads part of it.
+  The `-Users-macstudio3` index is ~45KB / 134 lines with lines up to ~695 chars.
+  Fix = trim each index line to a one-line hook (<200 chars) and move the detail
+  into the linked topic file (`<slug>.md`). Never delete a memory's information —
+  relocate it. Legacy `-Users-stevestudio2*` MEMORY.md files (one is 265KB) belong
+  to an old home dir and are almost certainly dead context — flag them but confirm
+  before touching; they may not even load for the current `-Users-macstudio3` home.
+
+### 4. Turn off slow hooks
+- The audit lists hook commands. Judge "slow": SessionStart hooks that run
+  network calls / big scans / loops. **GATED** (settings.json) — route via
+  `update-config` or surface the paste. Note `~/.claude/hooks-lite` already exists
+  as the streamline off-switch for slow SessionStart hooks; prefer toggling that
+  over editing individual hooks.
+
+### 5. Update Claude Code
+- **GATED.** Surface the command for Steve to run himself (a live update can
+  interrupt running sessions): `claude update` (or `npm i -g @anthropic-ai/claude-code@latest`).
+  Report current→latest from the audit so Steve decides.
+
+### 6. Enable auto mode by default
+- **GATED** (settings.json + a real behavior change). Surface it; do not flip it.
+  Auto mode routes permission prompts to a classifier — that's Steve's call, and
+  many of his rules assume gated actions still stop. Recommend, never enable.
+
+### 7. Pre-approve frequently-DENIED read-only commands
+- **Delegate.** Run the existing **`/fewer-permission-prompts`** skill — it scans
+  session history for safe-but-repeatedly-prompted bash/MCP commands and proposes
+  an allowlist. Don't reimplement that scan here.
+
+## Steve goodies (the "and a few other goodies" bucket)
+- **keepawake guard.** Verify `com.steve.keepawake` is alive + screensaver
+  idleTime 0 (a standing session rule). Repairs are in the global CLAUDE.md; the
+  `sudo pmset` repair is a Steve-paste.
+- **launchd health.** Report `com.steve.*` jobs whose last exit code is non-zero.
+  Exit `-15`/`-9` are just SIGTERM/SIGKILL (a restart/reboot) — usually benign, not
+  a crash. A positive exit (e.g. `1`) is a real failure worth surfacing. Don't
+  auto-restart anything; report and let the relevant owner/agent handle it.
+
+## Output shape Claude should produce
+A grouped report: **AUTO (with confirm)** list, **GATED (Steve paste / skill)**
+list, **CLEAN** list. Then ONE confirmation prompt. Apply only the AUTO items
+Steve okays; commit each in its repo; hand back the GATED paste blocks. Always
+end with a `$0 (local)` cost line — the audit and all fixes are free/local.
diff --git a/scripts/checkup-audit.sh b/scripts/checkup-audit.sh
new file mode 100755
index 0000000..061d92e
--- /dev/null
+++ b/scripts/checkup-audit.sh
@@ -0,0 +1,172 @@
+#!/usr/bin/env bash
+# checkup-audit.sh — READ-ONLY environment audit for Claude Code.
+# Gathers the raw data a /checkup needs, changes NOTHING. All interpretation
+# and any change proposals are done by Claude reading this output against
+# SKILL.md. Safe to run any time.
+#
+# Usage:
+#   bash checkup-audit.sh              # audit ~/.claude + current project
+#   bash checkup-audit.sh --deep       # also scan session logs for skill usage (slower)
+#
+# Output is a plain sectioned report on stdout. Exit code is always 0 unless a
+# path is fundamentally wrong; findings are not errors.
+
+set -uo pipefail
+
+CLAUDE_HOME="${CLAUDE_HOME:-$HOME/.claude}"
+DEEP=0
+[ "${1:-}" = "--deep" ] && DEEP=1
+
+# Thresholds (kept in one place so they're easy to tune)
+ROOT_CLAUDEMD_WARN_BYTES=$((12 * 1024))   # root CLAUDE.md over ~12KB → recommend nesting
+MEMORY_LIMIT_BYTES=$((24 * 1024))          # MEMORY.md soft limit the harness warns at
+IDX_LINE_WARN_CHARS=200                     # MEMORY.md index line too long
+
+hr(){ printf '\n=== %s ===\n' "$1"; }
+kb(){ awk -v b="$1" 'BEGIN{printf "%.1fKB", b/1024}'; }
+have(){ command -v "$1" >/dev/null 2>&1; }
+
+echo "CHECKUP AUDIT  —  $(uname -n)  —  CLAUDE_HOME=$CLAUDE_HOME"
+echo "(read-only; nothing changed)"
+
+# ---------------------------------------------------------------------------
+hr "CLAUDE CODE VERSION"
+CUR="$(claude --version 2>/dev/null | awk '{print $1}')"
+echo "installed: ${CUR:-unknown}"
+if have npm; then
+  LATEST="$(timeout 12 npm view @anthropic-ai/claude-code version 2>/dev/null)"
+  echo "latest(npm): ${LATEST:-lookup-failed}"
+  if [ -n "${LATEST:-}" ] && [ -n "${CUR:-}" ] && [ "$LATEST" != "$CUR" ]; then
+    echo "FINDING: update available ($CUR -> $LATEST)  [GATED: version update]"
+  fi
+else
+  echo "latest(npm): npm not on PATH — skip version check"
+fi
+
+# ---------------------------------------------------------------------------
+hr "ROOT / GLOBAL CLAUDE.md (bloat → nest into sub-CLAUDE.md + skills)"
+for f in "$CLAUDE_HOME/CLAUDE.md" "$PWD/CLAUDE.md" "$PWD/.claude/CLAUDE.md"; do
+  [ -f "$f" ] || continue
+  b=$(wc -c <"$f")
+  flag=""
+  [ "$b" -gt "$ROOT_CLAUDEMD_WARN_BYTES" ] && flag="  <-- FINDING: large, consider nesting  [AUTO w/ confirm: split]"
+  printf '  %-55s %s%s\n' "$f" "$(kb "$b")" "$flag"
+done
+
+# ---------------------------------------------------------------------------
+hr "LOCAL vs CHECKED-IN CLAUDE.md (dedup)"
+# Local overrides live in CLAUDE.local.md (gitignored). Compare against the
+# committed CLAUDE.md so duplicated lines can be pruned from the local copy.
+if [ -f "$PWD/CLAUDE.local.md" ] && [ -f "$PWD/CLAUDE.md" ]; then
+  dup=$(grep -Fxf "$PWD/CLAUDE.md" "$PWD/CLAUDE.local.md" 2>/dev/null | grep -c . )
+  echo "  CLAUDE.local.md shares $dup identical line(s) with CLAUDE.md  [AUTO w/ confirm: prune dups from local]"
+else
+  echo "  no CLAUDE.local.md + CLAUDE.md pair in $PWD — nothing to dedup"
+fi
+
+# ---------------------------------------------------------------------------
+hr "MEMORY.md (auto-memory index) SIZE"
+# Find every MEMORY.md under the projects memory dirs.
+while IFS= read -r m; do
+  [ -f "$m" ] || continue
+  b=$(wc -c <"$m")
+  lines=$(grep -c '^- \[' "$m" 2>/dev/null || echo 0)
+  longest=$(awk '{ if(length>L){L=length} } END{print L+0}' "$m")
+  flag=""
+  [ "$b" -gt "$MEMORY_LIMIT_BYTES" ] && flag="  <-- FINDING: over ${MEMORY_LIMIT_BYTES}B limit  [AUTO w/ confirm: trim index]"
+  printf '  %s  %s  (%s index lines, longest line %s chars)%s\n' "$m" "$(kb "$b")" "$lines" "$longest" "$flag"
+  if [ "$longest" -gt "$IDX_LINE_WARN_CHARS" ]; then
+    echo "    -> index lines exceed ${IDX_LINE_WARN_CHARS} chars; move detail into topic files, keep index one-line."
+  fi
+done < <(find "$CLAUDE_HOME/projects" -maxdepth 3 -name MEMORY.md 2>/dev/null)
+
+# ---------------------------------------------------------------------------
+hr "SKILLS (unused → archive to save context)"
+SK="$CLAUDE_HOME/skills"
+if [ -d "$SK" ]; then
+  total=$(find "$SK" -maxdepth 1 -mindepth 1 -type d | wc -l | tr -d ' ')
+  echo "  $total skill directories in $SK"
+  echo "  10 least-recently-modified skill dirs (staleness proxy):"
+  # Portable mtime sort (macOS stat -f, Linux stat -c fallback)
+  if stat -f '%m %N' "$SK" >/dev/null 2>&1; then
+    find "$SK" -maxdepth 1 -mindepth 1 -type d -exec stat -f '%m %N' {} \; 2>/dev/null \
+      | sort -n | head -10 | while read -r ts path; do
+          printf '    %s  %s\n' "$(date -r "$ts" '+%Y-%m-%d' 2>/dev/null)" "$(basename "$path")"; done
+  else
+    find "$SK" -maxdepth 1 -mindepth 1 -type d -printf '%T@ %p\n' 2>/dev/null \
+      | sort -n | head -10 | while read -r ts path; do
+          printf '    %s  %s\n' "$(date -d "@${ts%.*}" '+%Y-%m-%d' 2>/dev/null)" "$(basename "$path")"; done
+  fi
+  echo "  NOTE: mtime is a weak signal. Confirm with Steve before archiving any skill."
+  if [ "$DEEP" = 1 ]; then
+    echo "  --deep: skill names never seen in session logs (last 30d):"
+    logs=$(find "$CLAUDE_HOME/projects" -name '*.jsonl' -mtime -30 2>/dev/null)
+    if [ -n "$logs" ]; then
+      for d in "$SK"/*/; do
+        n=$(basename "$d")
+        if ! grep -qhl "\"$n\"" $logs 2>/dev/null && ! grep -qh "/$n\b" $logs 2>/dev/null; then
+          echo "    (never-invoked?) $n"
+        fi
+      done | head -40
+      echo "    ...(truncated to 40; treat as CANDIDATES not proof)"
+    else
+      echo "    no recent session logs found"
+    fi
+  else
+    echo "  (run with --deep to cross-check against 30d of session logs)"
+  fi
+else
+  echo "  no skills dir at $SK"
+fi
+
+# ---------------------------------------------------------------------------
+hr "MCP SERVERS (unused → remove to save context/startup)"
+CJSON="$HOME/.claude.json"
+if [ -f "$CJSON" ] && have jq; then
+  echo "  top-level mcpServers:"
+  jq -r '.mcpServers // {} | keys[]' "$CJSON" 2>/dev/null | sed 's/^/    /' || echo "    (none / parse failed)"
+  echo "  per-project mcpServers counts:"
+  jq -r '(.projects // {}) | to_entries[] | "    \(.key): \((.value.mcpServers // {}) | length) server(s)"' "$CJSON" 2>/dev/null | head -20
+else
+  echo "  $CJSON missing or jq not installed — inspect MCP config manually"
+fi
+
+# ---------------------------------------------------------------------------
+hr "HOOKS (slow → disable)  [GATED: settings.json is classifier-blocked for Claude]"
+for s in "$CLAUDE_HOME/settings.json" "$CLAUDE_HOME/settings.local.json"; do
+  [ -f "$s" ] || continue
+  echo "  $s ($(kb "$(wc -c <"$s")")):"
+  if have jq; then
+    jq -r '(.hooks // {}) | to_entries[] | "    \(.key): \(.value | length) matcher-group(s)"' "$s" 2>/dev/null || echo "    (no hooks / parse failed)"
+    echo "    hook commands (eyeball for slow: loops, network, big scans):"
+    jq -r '[.hooks // {} | .. | .command? // empty] | unique[]' "$s" 2>/dev/null | sed 's/^/      /' | head -30
+  else
+    grep -o '"command"[^,]*' "$s" 2>/dev/null | head -20 | sed 's/^/    /'
+  fi
+done
+echo "  NOTE: '~/.claude/hooks-lite' (streamline mode) already exists to suppress slow SessionStart hooks."
+
+# ---------------------------------------------------------------------------
+hr "AUTO MODE + PERMISSIONS"
+if have jq; then
+  am=$(jq -r '(.autoMode // .permissions.defaultMode // "unset") | if type=="object" or type=="array" then "unset" else tostring end' "$CLAUDE_HOME/settings.json" 2>/dev/null)
+  echo "  settings.json autoMode/defaultMode: ${am:-unset}   [GATED: settings.json]"
+  allow=$(jq -r 'try ((.permissions.allow // []) | length)' "$CLAUDE_HOME/settings.json" 2>/dev/null)
+  echo "  permissions.allow entries: ${allow:-0}"
+fi
+echo "  To pre-approve frequently-DENIED read-only commands, run the existing"
+echo "  /fewer-permission-prompts skill — it scans session history and proposes an allowlist."
+
+# ---------------------------------------------------------------------------
+hr "STEVE GOODIES — keepawake + launchd health"
+launchctl list 2>/dev/null | grep -q com.steve.keepawake \
+  && echo "  keepawake guard: ALIVE" || echo "  keepawake guard: MISSING  [FINDING: repair per CLAUDE.md]"
+ss=$(defaults -currentHost read com.apple.screensaver idleTime 2>/dev/null || echo unset)
+echo "  screensaver idleTime: $ss  (expect 0)"
+echo "  launchd com.steve.* jobs with non-zero last exit code:"
+launchctl list 2>/dev/null | awk '$1!="-" && $2!="0" && $3 ~ /^com\.steve\./ {printf "    %s  exit=%s\n",$3,$2}' | head -20
+echo "    (none listed above = all clean)"
+
+hr "END OF AUDIT"
+echo "Next: Claude reads this against SKILL.md, groups findings AUTO vs GATED,"
+echo "confirms with Steve, applies AUTO items (git-committed), surfaces GATED as paste blocks."

(oldest)  ·  back to Checkup  ·  (newest)