<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>lem.fyi</title>
        <link>https://lem.fyi</link>
        <description>Personal blog by Lucas McComb</description>
        <lastBuildDate>Sun, 05 Apr 2026 04:19:54 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved 2026, Lucas McComb</copyright>
        <item>
            <title><![CDATA[CCGM: A Modular Configuration System for Claude Code]]></title>
            <link>https://lem.fyi/blog/ccgm/</link>
            <guid isPermaLink="false">https://lem.fyi/blog/ccgm/</guid>
            <pubDate>Sat, 04 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A deep dive into CCGM (Claude Code God Mode) - 35 modules, 30 slash commands, 13 hooks, and a companion deep research pipeline that turns Claude Code into a fully autonomous development system.]]></description>
            <content:encoded><![CDATA[<p>In February I wrote about <a href="/blog/ai-enabled-swe-system/">building a multi-agent AI development system</a> with Claude Code. That post described <code>claude-dotfiles</code> - a single repository containing my global instructions, hooks, slash commands, and workflow documentation. It worked, but it had problems. The entire configuration was a monolith. Everything was tightly coupled to my specific setup. If you wanted to use the git workflow rules but not the multi-agent system, you had to surgically extract them from a 600+ line CLAUDE.md file and hope nothing broke.</p>
<p><a href="https://github.com/lucasmccomb/ccgm" target="_blank" rel="noopener noreferrer">CCGM</a> (Claude Code God Mode) is the modular system that replaced it. Every capability is a self-contained module you can install independently. Pick what you want, skip what you don't. Install with one command. It works across Claude Code CLI, VS Code, Cursor, the macOS Claude app, and any other editor with Claude Code support.</p>
<p>Why modular over monolithic? A monolithic config forces you to fork if you want to diverge, then you're maintaining your own version indefinitely. Conditional config (if statements in one file) mixes concerns and makes dependencies invisible. CCGM's approach: each module is a git-tracked directory with explicit dependencies declared in a manifest. Install only what you need. Read each module's README independently. Update CCGM and your personal additions stay intact.</p>
<hr>
<h2 id="the-problem-with-monolithic-config"><a class="heading-anchor" href="#the-problem-with-monolithic-config">The Problem with Monolithic Config</a></h2>
<p>The original <code>claude-dotfiles</code> repo was a flat dump of everything into <code>~/.claude/</code>. One giant <code>CLAUDE.md</code> with hundreds of lines of instructions. Hooks that assumed specific directory structures. Commands that referenced hardcoded paths. If you wanted to share it with someone, they had to fork the whole thing and strip out anything personal.</p>
<p>The deeper problem was coupling. The multi-agent coordination system depended on session logging, which depended on a specific log repo structure, which assumed you had multiple clones of every repository. A solo developer who just wanted better git workflow rules was pulling in an entire parallel agent infrastructure.</p>
<p>Each capability needed to be self-contained (installable without unrelated dependencies), documented (its own README explaining what it does and why), configurable (template variables for paths, usernames, and preferences), and composable (able to declare dependencies on other modules). CCGM is the result.</p>
<hr>
<h2 id="architecture-and-installation"><a class="heading-anchor" href="#architecture-and-installation">Architecture and Installation</a></h2>
<p>CCGM is a Git repository with 35 modules organized into 5 categories. An interactive installer reads module manifests, resolves dependencies, expands templates, and places files into <code>~/.claude/</code>.</p>
<pre><code>ccgm/
├── start.sh                    # Interactive installer
├── update.sh                   # Pull latest and re-apply
├── uninstall.sh                # Remove only CCGM-installed files
├── presets/                    # Named module collections
├── lib/                        # Installer internals
├── modules/                    # 35 self-contained modules
├── docs/                       # 8 reference documents
└── tests/                      # Validation tests
</code></pre>
<p>The installer handles everything: prerequisite checks (Claude Code, jq, Python 3, gh CLI, gum), module selection via presets or individual checkboxes, dependency resolution via topological sort, configuration prompts for username and preferences, template expansion, file installation (copy or symlink), and settings merge. It records what it installed in a manifest so updates and uninstalls only touch CCGM-managed files.</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">git</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> clone</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> https://github.com/lucasmccomb/ccgm.git</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> &#x26;&#x26; </span><span style="color:#0550AE;--shiki-dark:#79C0FF">cd</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ccgm</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> &#x26;&#x26; </span><span style="color:#953800;--shiki-dark:#FFA657">./start.sh</span></span></code></pre>
<p>Other install modes:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">./start.sh</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --preset</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> standard</span><span style="color:#6E7781;--shiki-dark:#8B949E">      # Skip the selection menu</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">./start.sh</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --scope</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> project</span><span style="color:#6E7781;--shiki-dark:#8B949E">        # Install to .claude/ instead of ~/.claude/</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">./start.sh</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --link</span><span style="color:#6E7781;--shiki-dark:#8B949E">                 # Symlink instead of copy (for CCGM developers)</span></span></code></pre>
<p>For AI agents installing programmatically:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">CCGM_NON_INTERACTIVE</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">1</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> CCGM_USERNAME</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"github-user"</span><span style="color:#953800;--shiki-dark:#FFA657"> ./start.sh</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --preset</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> standard</span></span></code></pre>
<p>CCGM places four types of files into <code>~/.claude/</code>:</p>
<table>
<thead>
<tr>
<th scope="col">Directory</th>
<th scope="col">What</th>
<th scope="col">How Claude Uses It</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>rules/*.md</code></td>
<td>Behavior rules</td>
<td>Loaded automatically at session start</td>
</tr>
<tr>
<td><code>commands/*.md</code></td>
<td>Slash commands</td>
<td>Available as <code>/commit</code>, <code>/pr</code>, etc.</td>
</tr>
<tr>
<td><code>hooks/*.py</code></td>
<td>Workflow hooks</td>
<td>Triggered on Claude Code events</td>
</tr>
<tr>
<td><code>settings.json</code></td>
<td>Permissions</td>
<td>Controls tool access and auto-approval</td>
</tr>
</tbody>
</table>
<p>Rules shape how Claude thinks about code, handles git, debugs problems, and coordinates with other agents. Commands are explicit actions you invoke with <code>/command-name</code>. Hooks fire automatically on Claude Code events and can approve, deny, or modify behavior.</p>
<hr>
<h2 id="presets"><a class="heading-anchor" href="#presets">Presets</a></h2>
<p>Four presets handle different needs. Start here to decide what to install, then read the module details that follow.</p>
<table>
<thead>
<tr>
<th scope="col">Preset</th>
<th scope="col">Modules</th>
<th scope="col">Best For</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>minimal</strong> (3)</td>
<td>global-claude-md, autonomy, git-workflow</td>
<td>Trying CCGM for the first time</td>
</tr>
<tr>
<td><strong>standard</strong> (8)</td>
<td>+ identity, settings, hooks, commands-core, commands-utility</td>
<td>Most individual developers</td>
</tr>
<tr>
<td><strong>team</strong> (10)</td>
<td>+ github-protocols, code-quality, systematic-debugging, verification</td>
<td>Teams with shared repos</td>
</tr>
<tr>
<td><strong>full</strong> (35)</td>
<td>All modules</td>
<td>Power users</td>
</tr>
</tbody>
</table>
<p>The installer resolves dependencies automatically. Select <code>xplan</code> and it pulls in <code>multi-agent</code> and <code>session-logging</code> without you needing to know they're required.</p>
<hr>
<h2 id="the-module-system"><a class="heading-anchor" href="#the-module-system">The Module System</a></h2>
<p>Every module lives in <code>modules/{name}/</code> and contains a <code>module.json</code> manifest, a <code>README.md</code>, and one or more content files. The manifest declares what to install, where to put it, and what configuration to prompt for.</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">{</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">  "name"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"autonomy"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">,</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">  "displayName"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"Autonomy"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">,</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">  "description"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"Claude as a fully autonomous engineer"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">,</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">  "category"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"core"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">,</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">  "scope"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: [</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"global"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">, </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"project"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">],</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">  "dependencies"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: [],</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">  "files"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: {</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">    "rules/autonomy.md"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: {</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">      "target"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"rules/autonomy.md"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">,</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">      "type"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"rule"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">,</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">      "template"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0550AE;--shiki-dark:#79C0FF">false</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">    }</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">  },</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">  "tags"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: [</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"autonomy"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">, </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"core"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">],</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">  "configPrompts"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: []</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">}</span></span></code></pre>
<p>File types: <code>rule</code> (loaded at session start), <code>command</code> (becomes a <code>/slash-command</code>), <code>hook</code> (triggered by events), <code>config</code> (merged into settings.json), <code>doc</code> (reference material, not auto-loaded).</p>
<p>Some modules use template variables (<code>__HOME__</code>, <code>__USERNAME__</code>, <code>__CODE_DIR__</code>) that are expanded during installation. Rule files never use templates because they should work for anyone without substitution.</p>
<hr>
<h2 id="core-modules"><a class="heading-anchor" href="#core-modules">Core Modules</a></h2>
<p>Six core modules form the foundation. Rules tell Claude what to do; hooks (covered later) make sure it actually does it.</p>
<h3 id="global-claude-md"><a class="heading-anchor" href="#global-claude-md">global-claude-md</a></h3>
<p>Installs a slim <code>~/.claude/CLAUDE.md</code> that serves as the root configuration reference. Instead of packing hundreds of lines of instructions into one file, it points to the actual rule files, commands, hooks, and settings where behavior is defined. This prevents the context waste of loading the same instructions twice (once from CLAUDE.md, once from the rule file) and eliminates maintenance drift between duplicated rules.</p>
<h3 id="identity"><a class="heading-anchor" href="#identity">identity</a></h3>
<p>Two foundational context files that give Claude a persistent identity layer surviving across sessions and context resets:</p>
<ul>
<li><strong>soul.md</strong> defines the AI's personality, philosophy, reasoning principles, communication style, and boundaries</li>
<li><strong>human-context.md</strong> defines who you are - your background, goals, domain expertise, working style, and life intentions</li>
</ul>
<p>Together they transform generic AI sessions into a working relationship with a consistent, aligned collaborator. The installer includes an interactive personalization step where you define both files during setup. Design principles: concision beats comprehensiveness (1-3 pages per file), declarative values beat procedural rules ("I value simplicity" works better than "never use complex abstractions"), and stable identity over current tasks (the memory system handles evolving details).</p>
<h3 id="autonomy"><a class="heading-anchor" href="#autonomy">autonomy</a></h3>
<p>The philosophical core. Configures Claude as a fully autonomous Staff-level engineer who executes tasks end-to-end instead of describing steps for you to follow.</p>
<p>The rule establishes one principle: <strong>do it, don't describe it</strong>. If Claude can accomplish something from the command line, it should do it immediately. Run <code>npm install</code> yourself. Fix failing builds yourself. Debug fully yourself.</p>
<p>It also defines clear boundaries for when to ask: credentials Claude doesn't have, third-party dashboard actions requiring a browser session, ambiguous product decisions, and destructive actions on shared systems. Everything else, just do it.</p>
<h3 id="git-workflow"><a class="heading-anchor" href="#git-workflow">git-workflow</a></h3>
<p>Six rules extracted from real mistakes:</p>
<ol>
<li><strong>No AI attribution</strong> - never add <code>Co-Authored-By</code> trailers or "Generated with Claude Code" footers. The human is the author and the one accountable for the code; AI is a tool.</li>
<li><strong>PR template detection</strong> - check the repo root, <code>.github/</code>, and the org's <code>.github</code> repo for PR templates before creating PRs.</li>
<li><strong>Sync before history changes</strong> - always <code>git fetch</code> before rebase, filter-branch, or reset. Running history-altering commands on a stale branch and force-pushing overwrites the remote.</li>
<li><strong>Rebase by default</strong> - use rebase instead of merge for feature branches. Linear history, clean diffs.</li>
<li><strong>Never stash</strong> - commit instead. Stashes are invisible and easy to forget.</li>
<li><strong>Return to main after merge</strong> - checkout main and pull after PRs are merged.</li>
</ol>
<h3 id="settings"><a class="heading-anchor" href="#settings">settings</a></h3>
<p>A base <code>settings.json</code> with 900+ pre-configured tool permission entries. The allow list covers safe operations (git status, npm commands, file operations in safe paths). The deny list blocks dangerous commands (force push to main, <code>rm -rf /</code>, dropping databases). A configurable default mode lets you choose between <code>ask</code> (confirm before risky tools) and <code>dontAsk</code> (auto-approve everything not denied).</p>
<h3 id="hooks"><a class="heading-anchor" href="#hooks">hooks</a></h3>
<p>Ten Python hook scripts that enforce the rules, plus an orphaned process detector. Each hook is covered in <a href="#the-hook-system">The Hook System</a> below.</p>
<hr>
<h2 id="commands-and-skills"><a class="heading-anchor" href="#commands-and-skills">Commands and Skills</a></h2>
<p>CCGM installs up to 30 slash commands and skills across 15 modules. Here are the ones that matter most, grouped by what they do.</p>
<h3 id="daily-workflow-commands-core"><a class="heading-anchor" href="#daily-workflow-commands-core">Daily Workflow (commands-core)</a></h3>
<p>These keep you in the terminal without context-switching to GitHub's UI for routine operations.</p>
<p><strong><code>/commit</code></strong> - Stage, verify (lint, type-check, tests, build), and commit. Issue number extracted from branch name automatically.</p>
<p><strong><code>/pr</code></strong> - Verify, rebase, push, detect PR templates, create PR with <code>Closes #{issue}</code>.</p>
<p><strong><code>/cpm</code></strong> - The full cycle in one command: commit, PR, squash-merge, close issue, return to main.</p>
<p><strong><code>/gs</code></strong> - Git status dashboard: branch, sync state, working directory, open PRs, recommended next action.</p>
<p><strong><code>/ghi</code></strong> - Create a GitHub issue with type labels and structured body.</p>
<h3 id="research-and-ideation"><a class="heading-anchor" href="#research-and-ideation">Research and Ideation</a></h3>
<p>These commands handle the upfront discovery phase before you write code. <code>/research</code> is fast and requires no setup; <code>/deepresearch</code> produces deterministic, reproducible results but requires local infrastructure (Docker, Ollama, ~40GB model). Use <code>/research</code> for quick exploration, <code>/deepresearch</code> when consistency matters, and <code>/ideate</code> when the idea itself isn't clear yet.</p>
<p><strong><code>/research</code></strong> - Zero-dependency research using parallel Claude agents with WebSearch, WebFetch, GitHub CLI, and Reddit. Spawns up to 7 agents that each investigate from a different angle. Works out of the box.</p>
<p><strong><code>/ideate</code></strong> - Structured ideation framework. Takes a half-formed idea and runs a Socratic interview to reach 95% clarity across 7 dimensions. Can delegate to <code>/deepresearch</code> for market validation mid-interview, then hand off to <code>/xplan</code> for execution planning.</p>
<p><strong><code>/deepresearch</code></strong> - Local-first pipeline that replaces parallel subagents with Ollama + SearXNG + a single Sonnet API call. Covered in <a href="#lem-deepresearch-the-companion-pipeline">its own section</a>.</p>
<h3 id="quality-and-review"><a class="heading-anchor" href="#quality-and-review">Quality and Review</a></h3>
<p>These analyze code and writing after the work is done, catching issues before they ship.</p>
<p><strong><code>/audit</code></strong> - Codebase audit across 8 categories (security, dependencies, code quality, architecture, TypeScript/React, testing, documentation, performance) using parallel agents. Supports <code>--fix</code> for auto-remediation.</p>
<p><strong><code>/editorial-critique</code></strong> - 8-pass editorial review of long-form writing: prose craft, AI-tell detection, argument architecture, conciseness, data accuracy, structure and pacing, impact, and grammar. Produces a scored report (80 points max) with a prioritized findings list. <code>--fix</code> applies changes automatically.</p>
<p><strong><code>/design-review</code></strong> - 6-pass visual design review. Takes screenshots at desktop, tablet, and mobile, extracts DOM structure and computed styles, then analyzes spacing, typography, responsive behavior, visual hierarchy, accessibility (WCAG AA), and component consistency. Produces a scored report with exact CSS selectors and property values. <code>--fix</code> applies the CSS changes.</p>
<p><strong><code>/debug</code></strong> - Structured root-cause debugging: reproduce, hypothesize, instrument, diagnose, fix, verify. Runs on Opus. Invoked automatically when you ask Claude to fix a bug.</p>
<h3 id="brand-and-documentation"><a class="heading-anchor" href="#brand-and-documentation">Brand and Documentation</a></h3>
<p><strong><code>/brand</code></strong> - Full naming pipeline: word exploration via 4 parallel agents (Datamuse, ConceptNet, Big Huge Thesaurus, etymological sources), 150-250 candidate generation across 6 categories, verification through domain availability, USPTO/WIPO trademark screening, app store searches, and social handle checks.</p>
<p><strong><code>/brand-check</code></strong> - Deep verification of a single name across 10+ TLDs, trademarks, app stores, and social handles.</p>
<p><strong><code>/docupdate</code></strong> - Documentation audit: README accuracy, TOC vs headings, onboarding flow vs prerequisites, package lists vs installed dependencies. Works in any project type.</p>
<h3 id="orchestration"><a class="heading-anchor" href="#orchestration">Orchestration</a></h3>
<p>These coordinate multi-step and multi-agent work. <code>/startup</code> initializes each session (pulls logs, checks git status, shows the tracking dashboard). <code>/xplan</code> handles the full lifecycle of a new feature or project. <code>/mawf</code> handles ad-hoc multi-agent work without the full planning overhead.</p>
<p><strong><code>/xplan</code></strong> - Interactive planning and execution framework. Built around three human gates: after discovery, you confirm the concept; after research and planning, you approve the technical direction; after peer review by security, architecture, and business logic agents, you launch execution across parallel agent clones. Supports <code>--light</code> to skip the interactive phases.</p>
<p><strong><code>/mawf</code></strong> - Multi-Agent Workflow. Takes unstructured input ("Fix the login bug, add dark mode, update the API docs"), parses into issues, plans dependency waves, spawns parallel agents, monitors progress, merges results.</p>
<p><strong><code>/startup</code></strong> - Session initialization. Derives agent identity, pulls session logs, reads cross-agent activity, checks git status, queries the tracking dashboard, and presents a session dashboard. Runs automatically at session start.</p>
<h3 id="other-commands"><a class="heading-anchor" href="#other-commands">Other Commands</a></h3>
<ul>
<li><strong><code>/pwv</code></strong> - Playwright visual verification</li>
<li><strong><code>/walkthrough</code></strong> - step-by-step guided mode</li>
<li><strong><code>/promote-rule</code></strong> - promote repo rules to global</li>
<li><strong><code>/cws-submit</code></strong> - Chrome Web Store submission walkthrough</li>
<li><strong><code>/ccgm-sync</code></strong> - sync local config changes back to CCGM repo</li>
<li><strong><code>/user-test</code></strong> - browser-based user testing</li>
<li><strong><code>/onremote</code></strong> - run commands on a configured remote server</li>
<li><strong><code>/workspace-setup</code></strong> - create multi-agent workspace directories</li>
<li><strong><code>/reflect</code></strong> - run the self-improving reflection checklist</li>
<li><strong><code>/consolidate</code></strong> - memory maintenance pass (deduplicate, clean stale entries)</li>
<li><strong><code>/xplan-status</code></strong> - check progress on a running xplan</li>
<li><strong><code>/xplan-resume</code></strong> - resume an interrupted xplan</li>
<li><strong><code>/log-init</code></strong> - lightweight session log initialization</li>
</ul>
<hr>
<h2 id="the-hook-system"><a class="heading-anchor" href="#the-hook-system">The Hook System</a></h2>
<p>Rules are instructions in markdown files that shape Claude's thinking. Hooks are Python scripts that intercept events and enforce constraints at runtime: blocking dangerous operations, prompting for required context, or automating approval for safe commands. Together they create guardrails that work even when Claude deviates from the rules.</p>
<p>Claude Code supports four hook types:</p>
<table>
<thead>
<tr>
<th scope="col">Hook Type</th>
<th scope="col">When</th>
<th scope="col">Can Block?</th>
</tr>
</thead>
<tbody>
<tr>
<td>PreToolUse</td>
<td>Before a tool call</td>
<td>Yes</td>
</tr>
<tr>
<td>PostToolUse</td>
<td>After a tool call</td>
<td>No</td>
</tr>
<tr>
<td>UserPromptSubmit</td>
<td>When user submits a message</td>
<td>No (context injection)</td>
</tr>
<tr>
<td>SessionStart</td>
<td>When a session begins</td>
<td>No (context injection)</td>
</tr>
</tbody>
</table>
<p>CCGM installs 13 hooks across 3 modules (10 from hooks, 2 from self-improving, 1 from session-logging):</p>
<h3 id="git-safety"><a class="heading-anchor" href="#git-safety">Git Safety</a></h3>
<p><strong>enforce-git-workflow.py</strong> (PreToolUse:Bash) - The most critical hook. Blocks commits directly to protected branches (main, master, develop, staging, production, and custom branches). Blocks commits without the <code>#N:</code> issue prefix. Blocks pushes to protected branches. Claude will happily commit directly to main if you don't stop it; in a multi-agent setup, an unprotected main branch is a disaster. Escape hatches exist for non-issue commits (<code>sync:</code> prefix) and emergencies (<code>ALLOW_MAIN_COMMIT=1</code>).</p>
<p><strong>check-migration-timestamps.py</strong> (PreToolUse) - Validates Supabase migration file timestamps before commits. Duplicate timestamps break <code>supabase db push</code> because the CLI can't distinguish the files. Catching this before commit prevents hard-to-debug migration state issues.</p>
<h3 id="workflow-adherence"><a class="heading-anchor" href="#workflow-adherence">Workflow Adherence</a></h3>
<p><strong>enforce-issue-workflow.py</strong> (UserPromptSubmit) - Detects implementation requests (keywords like "update", "fix", "add", "create") and injects a reminder: check for an existing issue, create a branch, commit with issue prefix, create a PR.</p>
<p><strong>auto-startup.py</strong> (SessionStart) - Triggers <code>/startup</code> at the beginning of each new session. Only fires on fresh starts, not resume or context compaction.</p>
<h3 id="permissions"><a class="heading-anchor" href="#permissions">Permissions</a></h3>
<p><strong>auto-approve-bash.py</strong> (PreToolUse:Bash) - Enforces Bash command permissions from <code>settings.json</code>. Ensures consistent permission behavior across all Claude Code environments (CLI, VS Code, Cursor).</p>
<p><strong>auto-approve-file-ops.py</strong> (PreToolUse) - Enforces path-based read/edit/write permissions using glob pattern matching.</p>
<h3 id="multi-agent-coordination"><a class="heading-anchor" href="#multi-agent-coordination">Multi-Agent Coordination</a></h3>
<p><strong>agent-tracking-pre.py</strong> (PreToolUse:Bash) - Warns when a branch creation command is about to claim an issue already claimed by another agent.</p>
<p><strong>agent-tracking-post.py</strong> (PostToolUse:Bash) - The engine of multi-agent issue tracking. Records branch creation (claim), first commit (in-progress), PR creation (pr-created), merge (merged), and issue close (closed) in a tracking CSV. Uses git commit + pull --rebase + push for concurrency; different-row edits auto-resolve since each agent modifies only its own rows.</p>
<h3 id="meta-learning"><a class="heading-anchor" href="#meta-learning">Meta-Learning</a></h3>
<p><strong>reflection-trigger.py</strong> (PostToolUse:Bash) - Fires after <code>gh pr merge</code> and <code>gh issue close</code> commands, injecting a reminder to run the self-improving reflection checklist before moving to the next task. This is the integration point between the self-improving module and the development workflow.</p>
<p><strong>precompact-reflection.py</strong> (PreCompact) - Fires before context compaction, prompting Claude to capture any unwritten patterns from the session before context is compressed and observations are lost.</p>
<h3 id="advisory"><a class="heading-anchor" href="#advisory">Advisory</a></h3>
<p><strong>ccgm-update-check.py</strong> (PreToolUse) - Checks once per day whether CCGM has upstream updates.</p>
<p><strong>port-check.py</strong> (PreToolUse:Bash) - Warns about dev server port conflicts. Reads port assignments from <code>.env.clone</code>, checks <code>lsof</code>. Advisory only, never blocks.</p>
<p><strong>orphan-process-check.py</strong> (PreToolUse) - Detects orphaned test worker processes left behind by crashed test runs. Warns before they accumulate and consume resources.</p>
<hr>
<h2 id="workflow-modules"><a class="heading-anchor" href="#workflow-modules">Workflow Modules</a></h2>
<p>Where core modules and commands handle individual tasks, workflow modules coordinate work across sessions, agents, and time.</p>
<h3 id="session-logging"><a class="heading-anchor" href="#session-logging">session-logging</a></h3>
<p>Structured logging system for tracking work across sessions. Each agent gets a unique ID derived from its directory name (e.g., <code>lem-fyi-0</code> becomes <code>agent-0</code>). Logs are markdown files stored in a dedicated git repo, updated at mandatory trigger points: after commits, PR creation, PR merge, issue close, and before context compaction. The <code>/startup</code> command pulls logs, reads other agents' activity for cross-agent awareness, queries the tracking dashboard, and presents a session dashboard. This is what lets an agent pick up where it (or another agent) left off in a previous session.</p>
<h3 id="multi-agent"><a class="heading-anchor" href="#multi-agent">multi-agent</a></h3>
<p>Enables parallel development with multiple Claude Code instances on the same repo. Two organization models: the <strong>workspace model</strong> provides isolated groups of 4 clones with a coordinator agent per workspace, and the <strong>flat clone model</strong> puts all clones as siblings in one directory. The workspace model isolates sets of clones under a workspace parent so each coordinator only sees its own clones; the flat model is simpler but all agents can see all clones.</p>
<p>Port allocation gives each clone unique dev server ports via <code>port-registry.json</code> and <code>.env.clone</code>, preventing collisions when multiple agents run servers simultaneously. The tracking CSV records which agent claimed which issue, with automatic status transitions: <code>claimed</code> -> <code>in-progress</code> -> <code>pr-created</code> -> <code>merged</code> / <code>closed</code>. The tracking hooks handle all CSV writes automatically.</p>
<h3 id="xplan"><a class="heading-anchor" href="#xplan">xplan</a></h3>
<p>The planning and execution framework, bridging "I have an idea" to "I have a running codebase." It enforces three human gates: after discovery you confirm the concept is worth building, after research and planning you approve the technical approach, and after peer review by security, architecture, and business logic agents you launch execution. The full phase list covers discovery interview, deep research, research synthesis, optional naming, tech stack validation, scope negotiation, dependency wave planning, peer review, and parallel execution across clones. Each gate prevents the system from over-investing before you've validated the direction. The <code>--light</code> flag skips the interactive phases for automated execution.</p>
<h3 id="remote-server"><a class="heading-anchor" href="#remote-server">remote-server</a></h3>
<p>SSH access to a configured remote machine. The <code>/onremote</code> command lets Claude run health checks, view logs, restart services, and execute maintenance tasks on the remote server without interactive shell sessions.</p>
<h3 id="self-improving"><a class="heading-anchor" href="#self-improving">self-improving</a></h3>
<p>Meta-learning system with automated triggers. After completing significant tasks, Claude reflects on what went well, what surprised it, and what it would do differently, then writes reusable patterns to memory files that persist across sessions.</p>
<p>Unlike the earlier version (which was just a passive rule), this module is now integrated into the development workflow through hooks and cross-module references. The <code>reflection-trigger.py</code> hook fires after PR merges and issue closes, injecting a reminder to run the reflection checklist. The <code>precompact-reflection.py</code> hook captures unwritten patterns before context compaction. The session-logging module's mandatory triggers include a reflection step after every PR merge. The systematic-debugging module feeds debugging patterns to memory after three-strike situations. And common-mistakes is a living document - the reflection loop can add new anti-patterns when it identifies ones that caused significant wasted time.</p>
<p>Two commands: <code>/reflect</code> runs the full reflection checklist inline, and <code>/consolidate</code> runs a memory maintenance pass (deduplication, contradiction resolution, stale entry cleanup).</p>
<h3 id="subagent-patterns"><a class="heading-anchor" href="#subagent-patterns">subagent-patterns</a></h3>
<p>Methodology for decomposing tasks and delegating to subagents. Covers when to use subagents, how to write specs (objective, context, constraints, deliverable), dispatch patterns, and a two-stage review process (spec compliance, then code quality).</p>
<hr>
<h2 id="pattern-modules"><a class="heading-anchor" href="#pattern-modules">Pattern Modules</a></h2>
<p>Workflow modules coordinate agents. Pattern modules shape how each agent thinks about problems, regardless of tech stack.</p>
<h3 id="code-quality"><a class="heading-anchor" href="#code-quality">code-quality</a></h3>
<p>Covers dependency minimization (prefer built-in, then library, then framework), migration validation (quote PostgreSQL reserved keywords, use idempotent patterns), build verification (pre-push only, not after every change), and living document maintenance (update README.md after merges that change capabilities).</p>
<p>The core principle is <code>change-philosophy.md</code>: when modifying an existing system, don't patch it. Redesign it into the solution that would have existed if the change had been a foundational assumption from the start. This prevents technical debt from accumulating as special cases. The result should look like it was always designed this way.</p>
<h3 id="systematic-debugging"><a class="heading-anchor" href="#systematic-debugging">systematic-debugging</a></h3>
<p>Four-phase root cause investigation: investigate (read the error, reproduce consistently), analyze (find patterns, check recent changes), hypothesize (form testable theories), implement (fix the root cause, not symptoms). Includes a three-strike rule: if three approaches fail, step back and reassess your understanding rather than continuing to guess.</p>
<h3 id="test-driven-development"><a class="heading-anchor" href="#test-driven-development">test-driven-development</a></h3>
<p>Strict red-green-refactor TDD. Write a failing test first, make it pass with the simplest code, then refactor. The module rejects common rationalizations: "This is too simple to test" (simple code has simple tests), "I'll add tests after" (tests written after implementation prove nothing about correctness), "The types guarantee correctness" (types catch type errors, not logic errors).</p>
<h3 id="verification"><a class="heading-anchor" href="#verification">verification</a></h3>
<p>Evidence-before-claims. Never assert that something works without fresh proof. Plan the verification command, execute it fresh, read the full output including exit codes, evaluate whether the output supports the claim, report honestly.</p>
<h3 id="common-mistakes"><a class="heading-anchor" href="#common-mistakes">common-mistakes</a></h3>
<p>Eight anti-patterns extracted from real mistakes: shallow directory exploration in monorepos, dependency blindness (branching without checking open PRs), ESLint Fast Refresh violations, suggesting already-tried solutions, premature solutions without full context, git multi-clone confusion, Cloudflare Pages vs Workers confusion, and CF Pages without Git integration.</p>
<h3 id="frontend-design"><a class="heading-anchor" href="#frontend-design">frontend-design</a></h3>
<p>Principles for distinctive interfaces. Intentional typography (not just Inter by default), cohesive color systems with semantic tokens, consistent spacing scales, purposeful animation. What to avoid: purple-to-blue gradients, overly rounded cards, default framework styles.</p>
<h3 id="browser-automation"><a class="heading-anchor" href="#browser-automation">browser-automation</a></h3>
<p>Tool selection hierarchy: CLI tools first (curl, gh, wrangler), then MCP servers, then API calls, then WebMCP, then browser automation last. Reserve browser automation for things that genuinely require visual verification.</p>
<hr>
<h2 id="tech-specific-modules"><a class="heading-anchor" href="#tech-specific-modules">Tech-Specific Modules</a></h2>
<p>Each of these captures best practices for a common stack, preventing mistakes specific to that ecosystem.</p>
<p><strong>cloudflare</strong> - Pages vs Workers selection, Git integration requirements. Prevents the mistake of creating a Workers project for a static site.</p>
<p><strong>supabase</strong> - Current API key terminology (publishable key, not "anon key"; secret key, not "service_role key"), circuit breaker prevention for the connection pooler, migration workflow.</p>
<p><strong>mcp-development</strong> - Building MCP servers: TypeScript recommended, stdio for local transport, <code>{service}_{action}_{resource}</code> naming, input schema design with Zod, testing with MCP Inspector.</p>
<p><strong>shadcn</strong> - shadcn/ui patterns: composition over custom, semantic theming (<code>bg-primary</code> not <code>bg-blue-500</code>), form architecture, accessibility.</p>
<p><strong>tailwind</strong> - Tailwind CSS v4: CSS-first configuration with <code>@theme</code>, OKLCH color system, CVA for variants, dark mode with <code>@custom-variant</code>. Includes the v4 <code>cursor: pointer</code> gotcha where buttons lose their pointer cursor because v4's preflight dropped the override.</p>
<hr>
<h2 id="the-statusline"><a class="heading-anchor" href="#the-statusline">The Statusline</a></h2>
<p>CCGM includes a statusline script that displays live session metrics at the bottom of your terminal:</p>
<pre><code>🧠 O-4.6 | code main | ctx:8% | 5h:62% ███░░ 2h26m | 7d:79% ████░ 3d8h
</code></pre>
<p>Model with tier emoji (🧠 Opus, 🐢 Sonnet, ⚠️ Haiku) and abbreviation, current directory and git branch, context window usage, 5-hour rate limit with bar and reset countdown, and 7-day rate limit with bar and reset countdown. Color-coded: green under 60%, yellow under 85%, red above 85%.</p>
<hr>
<h2 id="lem-deepresearch-the-companion-pipeline"><a class="heading-anchor" href="#lem-deepresearch-the-companion-pipeline">lem-deepresearch: The Companion Pipeline</a></h2>
<p><a href="https://github.com/lucasmccomb/lem-deepresearch" target="_blank" rel="noopener noreferrer">lem-deepresearch</a> provides the <code>/deepresearch</code> command as a companion to CCGM. It was extracted from CCGM into its own repo because it requires local infrastructure (Docker, Ollama with a ~40GB model, a Python venv) that not every CCGM user will want.</p>
<h3 id="why-a-local-pipeline"><a class="heading-anchor" href="#why-a-local-pipeline">Why a Local Pipeline?</a></h3>
<p>CCGM already includes <code>/research</code>, which spawns parallel Claude subagents and requires zero setup. It's fast and works everywhere. But each agent consumes a full context window, results vary between runs, and agents researching independently sometimes produce conflicting conclusions because they work from different sources with no reconciliation step.</p>
<p>lem-deepresearch trades setup complexity for consistency. It uses a <strong>deterministic local-first pipeline</strong>: local tools handle the cheap, parallelizable work (query generation, web search, fact extraction), and a single Sonnet API call handles the expensive step (synthesis). Same input produces the same output every time. The tradeoff is infrastructure: Docker, Ollama with a 72B parameter model (~40GB), and a Python venv.</p>
<h3 id="the-pipeline"><a class="heading-anchor" href="#the-pipeline">The Pipeline</a></h3>
<pre><code>Topic
  -> Ollama (qwen2.5:72b, local): Generate 3-7 diverse search queries
  -> SearXNG (Docker): Run parallel searches across Google, Bing, DuckDuckGo
  -> Ollama (local): Extract facts from search results
  -> Anthropic Sonnet (single API call): Synthesize into research.md
</code></pre>
<p><strong>Query generation</strong> uses a 72B parameter local model with temperature 0.7 for diversity, with a fallback to simple topic variations if generation fails. <strong>Parallel web search</strong> runs all queries concurrently via <code>httpx.AsyncClient</code>, getting up to 5 results per query from 3 engines. SSRF protection validates every URL against blocked networks (RFC 1918, loopback, link-local) and fails closed on DNS errors. HTML is stripped before any content reaches a model. <strong>Fact extraction</strong> filters by relevance and deduplicates across results. <strong>Synthesis</strong> produces a structured document with sections, citations, and source attribution.</p>
<p>Three depth presets: Standard (5 queries, ~6 min), Full (7 queries, ~8 min), Lite (3 queries, ~4 min).</p>
<h3 id="integration"><a class="heading-anchor" href="#integration">Integration</a></h3>
<p>lem-deepresearch installs to <code>~/.claude/commands/deepresearch.md</code> and <code>~/.claude/bin/deepresearch-cli.py</code>. It integrates with <code>/xplan</code> (xplan delegates its research phase to <code>/deepresearch</code> via <code>--plan-dir</code>) and with <code>/ccgm-sync</code> (edits to the command locally sync back to the lem-deepresearch repo).</p>
<p>Installation:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">git</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> clone</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> https://github.com/lucasmccomb/lem-deepresearch.git</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> &#x26;&#x26; </span><span style="color:#0550AE;--shiki-dark:#79C0FF">cd</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> lem-deepresearch</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> &#x26;&#x26; </span><span style="color:#953800;--shiki-dark:#FFA657">./install.sh</span></span></code></pre>
<hr>
<p>The original problem was a monolithic config that forced you to take everything or nothing. CCGM solves that: 35 modules across 5 categories, each installable independently with explicit dependencies. Start with the minimal preset, add capabilities as you need them, and update without losing your personal additions. The <a href="https://github.com/lucasmccomb/ccgm" target="_blank" rel="noopener noreferrer">README</a> has the full module catalog, and every module has its own README with manual install instructions if you prefer to cherry-pick.</p>
<p>Both CCGM and lem-deepresearch are <a href="https://github.com/lucasmccomb/ccgm/blob/main/LICENSE" target="_blank" rel="noopener noreferrer">MIT licensed</a>.</p>]]></content:encoded>
            <author>Lucas McComb</author>
        </item>
        <item>
            <title><![CDATA[Week of March 16, 2026]]></title>
            <link>https://lem.fyi/blog/2026-W12/</link>
            <guid isPermaLink="false">https://lem.fyi/blog/2026-W12/</guid>
            <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Built and shipped the automated week-in-review pipeline that wrote this post.]]></description>
            <content:encoded><![CDATA[<p>This post wrote itself. Sort of.</p>
<p>I shipped the automation that collects data from GitHub, Spotify, Garmin, YouTube, and Cloudflare, feeds it to Claude, and generates these week-in-review posts. It's scheduled via launchd for Sunday evenings, sends me an email when it's done, and publishes to the site. The irony of writing about the system that's writing this isn't lost on me.</p>
<h2 id="what-i-built"><a class="heading-anchor" href="#what-i-built">What I built</a></h2>
<p><strong>lem-fyi</strong> (#212, #210, #208, #206, #204): The WIR automation went through four iterations Sunday night. First pass: botched .env quoting broke the API tokens. Second: week calculation ran a day early because I forgot launchd executes on system timezone. Third: moved from 6 PM to 7 PM after realizing I'm rarely done working by six. Fourth: fixed code fence stripping in the markdown output - Claude's responses were bleeding through triple backticks. Also added <code>user-library-read</code> scope to the Spotify OAuth helper so the pipeline can actually fetch listening history (#204).</p>
<p>The site itself got a visual refresh. Redesigned the blog index to match the prose content width (#202), fixed Week in Review header spacing that was driving me nuts (#203), and reverted the three-state theme toggle back to a simple light/dark that syncs with system preference (#195, #192). The info icon on the home page now matches the LEM logo color (#190).</p>
<p>Published two posts: one about <a href="/blog/where-your-tax-dollars-actually-go/">where tax dollars actually go</a> during tax season (#188), another about <a href="/blog/shrink-clipboard-screenshots/">shrinking clipboard screenshots by 70%</a> before pasting into AI coding tools (#185).</p>
<p><strong>lem-work</strong> (#526, #524, #503): Updated the 15-minute chat product to $5 and moved it first in the list (#526). Set <code>GOOGLE_CALENDAR_ID</code> for scheduling availability (#524). Restricted coach routes to owner email only (#503) - no reason to expose those endpoints publicly. The rest was dependency bumps: Resend 4.5.2 to 6.9.3, Stripe, Hono, Lucide icons.</p>
<p><strong>Multi-agent work</strong>: Heavy weeks on ccgm, rootstead, sightagent, and supersam. Shipped milestones #1 (Foundation), #2 (Database Schema), and #4 (Search Pipeline) across those projects. Hit usage limits on Wave 3 agents - learned to plan for manual fallback when the token budget runs dry. Ran into pre-push hook failures from pre-existing TypeScript errors in a few repos, had to <code>--no-verify</code> a couple times. PNPM 10's interactive <code>approve-builds</code> doesn't work in non-TTY environments - switched to <code>pnpm.onlyBuiltDependencies</code> in package.json instead.</p>
<h2 id="what-i-consumed"><a class="heading-anchor" href="#what-i-consumed">What I consumed</a></h2>
<p>Only 90 minutes of music this week - unusual for me. <a href="https://open.spotify.com/search/Aleksi%20Per%C3%A4l%C3%A4" target="_blank" rel="noopener noreferrer">Aleksi Perälä</a> for idm/ambient focus, <a href="https://open.spotify.com/search/blink-182" target="_blank" rel="noopener noreferrer">blink-182</a> and <a href="https://open.spotify.com/search/Turnstile" target="_blank" rel="noopener noreferrer">Turnstile</a> when I needed energy, <a href="https://open.spotify.com/search/Matchbox%20Twenty" target="_blank" rel="noopener noreferrer">Matchbox Twenty</a> for post-grunge nostalgia. Daniel Avery's "Illusion Of Time" and Moby's "Go" on repeat for deep work sessions.</p>
<p>One YouTube like: Islestead's <a href="https://www.youtube.com/watch?v=K55zW-McyDs" target="_blank" rel="noopener noreferrer">video on making firewood without fuel or machines</a>. Pure craft.</p>
<div class="spotify-tracks not-prose">
<a href="https://open.spotify.com/track/02svSerh4F5X5Fn3rdnd93" target="_blank" rel="noopener noreferrer" class="spotify-track"><img src="https://i.scdn.co/image/ab67616d000048519ccb197dabd7e63634b20b92" alt="" class="spotify-track-art" loading="lazy" /><div class="spotify-track-info"><span class="spotify-track-name">Illusion Of Time</span><span class="spotify-track-artist">Daniel Avery, Alessandro Cortini</span></div></a>
<a href="https://open.spotify.com/track/0e8C9dPERvvARURkNOFrrC" target="_blank" rel="noopener noreferrer" class="spotify-track"><img src="https://i.scdn.co/image/ab67616d00004851910f382c8547679ccfe3b736" alt="" class="spotify-track-art" loading="lazy" /><div class="spotify-track-info"><span class="spotify-track-name">Go</span><span class="spotify-track-artist">Moby</span></div></a>
<a href="https://open.spotify.com/track/24CA5I411NVDYodjjmKPx3" target="_blank" rel="noopener noreferrer" class="spotify-track"><img src="https://i.scdn.co/image/ab67616d000048513301a0c4e0c8c5e8c331e1d6" alt="" class="spotify-track-art" loading="lazy" /><div class="spotify-track-info"><span class="spotify-track-name">Kick in the Door - 2008 Remaster</span><span class="spotify-track-artist">The Notorious B.I.G.</span></div></a>
<a href="https://open.spotify.com/track/5EMCzyydkJbUgfBybJsDz3" target="_blank" rel="noopener noreferrer" class="spotify-track"><img src="https://i.scdn.co/image/ab67616d0000485182dfc4648957b8e88c1fc497" alt="" class="spotify-track-art" loading="lazy" /><div class="spotify-track-info"><span class="spotify-track-name">When God Comes</span><span class="spotify-track-artist">Craig Mack</span></div></a>
</div>
<div class="media-embeds media-embeds--video">
<iframe src="https://www.youtube.com/embed/K55zW-McyDs" width="100%" height="315" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen loading="lazy" title="How I Make Firewood Efficiently (No Fuel, No Machines)"></iframe>
</div>
<h2 id="how-i-moved"><a class="heading-anchor" href="#how-i-moved">How I moved</a></h2>
<p>Three workouts Thursday: 16 minutes of miscellaneous movement, 31 minutes on the elliptical (average HR 131), 31 minutes of strength training. Sleep was solid - 7.7 hours average, resting heart rate at 54 bpm. Only 22,759 steps for the week, though. Need to walk more.</p>
<h2 id="reach"><a class="heading-anchor" href="#reach">Reach</a></h2>
<p>10,318 page views across all sites. lem.fyi saw decent traffic to the AI-enabled SWE system post (78 views) and the Claude setup guide (41 views). The tax dollars post engagement page got 27 views - people were commenting. lem.work and lem.photo both got hammered with WordPress admin login attempts (808 and 446 views respectively on <code>/wp-admin/index.php</code>). Those sites don't run WordPress. Nice try.</p>
<h2 id="closing"><a class="heading-anchor" href="#closing">Closing</a></h2>
<p>The meta-loop of building the system that documents the building is satisfying in a way I didn't expect. I've tried weekly reviews before and always quit after a few entries - too much friction. Automating the collection and drafting removes 90% of that. I still edit the output, but starting from Claude's draft instead of a blank page changes everything. The system works because it lowers the activation energy enough that I'll actually keep doing it.</p>]]></content:encoded>
            <author>Lucas McComb</author>
        </item>
        <item>
            <title><![CDATA[The Federal Safety Net: Cost and Context]]></title>
            <link>https://lem.fyi/blog/the-federal-safety-net-cost-and-context/</link>
            <guid isPermaLink="false">https://lem.fyi/blog/the-federal-safety-net-cost-and-context/</guid>
            <pubDate>Sat, 21 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[The federal safety net costs 3.3% of the budget. The government loses more than that to fraud, uncollected taxes, and waste every year. Here are the numbers.]]></description>
            <content:encoded><![CDATA[<p>In fiscal year 2024, the federal safety net, or "welfare" as some call it, (SNAP, housing vouchers, school lunch, heating aid, and four other programs) cost $221 billion, or about 3.3% of the federal budget.
Many Americans believe these programs are bloated, their budgets should be cut, and that their beneficiaries should be able to lift themselves out of poverty without taxpayer support.</p>
<p>The evidence tells a different story. The federal government loses more to uncollected taxes, documented fraud, and systemic waste than the entire safety net costs, every single year. These programs cost a fraction of the budget and are a lifeline for historically disenfranchised communities.</p>
<p>The analysis below contextualizes the cost of these programs using data from <a href="https://www.cbo.gov/publication/60843/html" target="_blank" rel="noopener noreferrer">CBO</a>, <a href="https://www.gao.gov/high-risk-list" target="_blank" rel="noopener noreferrer">GAO</a>, <a href="https://www.irs.gov/newsroom/irs-releases-2022-tax-gap-projections-voluntary-compliance-rate-among-taxpayers-remains-steady" target="_blank" rel="noopener noreferrer">IRS</a>, <a href="https://www.cms.gov/newsroom/fact-sheets/fiscal-year-2024-improper-payments-fact-sheet" target="_blank" rel="noopener noreferrer">CMS</a>, <a href="https://www.ers.usda.gov/topics/food-nutrition-assistance/supplemental-nutrition-assistance-program-snap" target="_blank" rel="noopener noreferrer">USDA</a>, and other federal sources. All figures are FY2024 unless otherwise noted.</p>
<hr>
<h2 id="the-full-budget-at-a-glance"><a class="heading-anchor" href="#the-full-budget-at-a-glance">The full budget at a glance</a></h2>
<p>The federal government spent <a href="https://www.cbo.gov/publication/60843/html" target="_blank" rel="noopener noreferrer">$6.75 trillion</a> in FY2024:</p>
<div class="budget-chart" role="img" aria-label="Federal budget breakdown: Social Security 22%, Everything Else 21%, Medicare 14%, Net Interest 13%, National Defense 13%, Medicaid and CHIP 9%, Veterans 5%, Safety Net Programs 3.3%">
  <div class="chart-row">
    <span class="chart-label">Social Security</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-blue" style="width: 66%"></div>
      <span class="chart-value">$1.5T (22%)</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">Everything Else</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-sky" style="width: 62%"></div>
      <span class="chart-value">$1.4T (21%)</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">Medicare</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-teal" style="width: 40%"></div>
      <span class="chart-value">$912B (14%)</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">Net Interest</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-gray" style="width: 39%"></div>
      <span class="chart-value">$882B (13%)</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">National Defense</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-red" style="width: 39%"></div>
      <span class="chart-value">$886B (13%)</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">Medicaid & CHIP</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-green" style="width: 28%"></div>
      <span class="chart-value">$626B (9%)</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">Veterans</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-purple" style="width: 14%"></div>
      <span class="chart-value">$310B (5%)</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">Safety Net Programs</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-amber" style="width: 10%"></div>
      <span class="chart-value">$221B (3.3%)</span>
    </div>
  </div>
  <p class="chart-note">Sources: CBO Monthly Budget Review FY2024; CBO Mandatory & Discretionary Spending Infographics FY2024. "Everything Else" is the remainder: other mandatory programs, non-defense discretionary spending, and offsetting receipts.</p>
</div>
<hr>
<h2 id="what-safety-net-programs-actually-cost"><a class="heading-anchor" href="#what-safety-net-programs-actually-cost">What safety net programs actually cost</a></h2>
<table>
<thead>
<tr>
<th scope="col">Program</th>
<th scope="col">FY2024 Cost</th>
<th scope="col">People Served</th>
<th scope="col">Avg. Benefit</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://www.ers.usda.gov/topics/food-nutrition-assistance/supplemental-nutrition-assistance-program-snap" target="_blank" rel="noopener noreferrer">SNAP</a> (food stamps)</td>
<td>$99.8B</td>
<td>41.7M/month</td>
<td><a href="https://fns-prod.azureedge.us/sites/default/files/resource-files/snap-FY23-Characteristics-Report.pdf" target="_blank" rel="noopener noreferrer">$187/month</a>*</td>
</tr>
<tr>
<td><a href="https://www.hud.gov/topics/housing_choice_voucher_program_section_8" target="_blank" rel="noopener noreferrer">Section 8</a> (housing vouchers)</td>
<td>$32.4B</td>
<td>~2.3M households</td>
<td>~$1,174/month</td>
</tr>
<tr>
<td><a href="https://www.congress.gov/crs-product/R45418" target="_blank" rel="noopener noreferrer">Pell Grants</a></td>
<td>$31.5B</td>
<td>6.3M students</td>
<td>$5,000/year</td>
</tr>
<tr>
<td><a href="https://www.ers.usda.gov/topics/food-nutrition-assistance/child-nutrition-programs/national-school-lunch-program" target="_blank" rel="noopener noreferrer">School lunch</a></td>
<td>$17.7B</td>
<td>~30M children/day</td>
<td>~$3.28/lunch</td>
</tr>
<tr>
<td><a href="https://www.acf.hhs.gov/ofa/programs/tanf" target="_blank" rel="noopener noreferrer">TANF</a> (cash assistance)</td>
<td>$16.5B</td>
<td>~2.1M families</td>
<td>~$655/month</td>
</tr>
<tr>
<td><a href="https://headstart.gov/program-data/article/head-start-program-facts-fiscal-year-2024" target="_blank" rel="noopener noreferrer">Head Start</a></td>
<td>$12.3B</td>
<td>~833K children</td>
<td>~$14,800/year</td>
</tr>
<tr>
<td><a href="https://www.ers.usda.gov/topics/food-nutrition-assistance/wic-program" target="_blank" rel="noopener noreferrer">WIC</a> (nutrition for mothers/infants)</td>
<td>$7.2B</td>
<td>~6.3M/month</td>
<td>~$95/month</td>
</tr>
<tr>
<td><a href="https://www.acf.hhs.gov/ocs/programs/liheap" target="_blank" rel="noopener noreferrer">LIHEAP</a> (heating/cooling)</td>
<td>$4.1B</td>
<td>~5.3M households</td>
<td>~$774/year</td>
</tr>
</tbody>
</table>
<p class="chart-note">\*SNAP per-person figure reflects average monthly benefit per participant, not total program cost (which includes administrative expenses) divided by participants.</p>
<p>That's $221 billion combined. TANF's federal block grant has been frozen at $16.5 billion since 1996 with no inflation adjustment, which equates to a <a href="https://www.cbpp.org/research/family-income-support/tanf-at-25-overly-restrictive-funding-structure-and-state-flexibility" target="_blank" rel="noopener noreferrer">40% cut in purchasing power</a> by CBPP's estimate as of 2021.</p>
<div class="budget-chart" role="img" aria-label="Safety net program costs: SNAP $99.8B, Section 8 $32.4B, Pell Grants $31.5B, School Lunch $17.7B, TANF $16.5B, Head Start $12.3B, WIC $7.2B, LIHEAP $4.1B">
  <div class="chart-row">
    <span class="chart-label">SNAP</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-amber" style="width: 45%"></div>
      <span class="chart-value">$99.8B</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">Section 8</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-amber" style="width: 15%"></div>
      <span class="chart-value">$32.4B</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">Pell Grants</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-amber" style="width: 14%"></div>
      <span class="chart-value">$31.5B</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">School Lunch</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-amber" style="width: 8%"></div>
      <span class="chart-value">$17.7B</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">TANF</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-amber" style="width: 7.5%"></div>
      <span class="chart-value">$16.5B</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">Head Start</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-amber" style="width: 5.6%"></div>
      <span class="chart-value">$12.3B</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">WIC</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-amber" style="width: 3.3%"></div>
      <span class="chart-value">$7.2B</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">LIHEAP</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-amber" style="width: 1.9%"></div>
      <span class="chart-value">$4.1B</span>
    </div>
  </div>
  <p class="chart-note">Sources: USDA ERS, HUD, ACF/HHS, CRS, Head Start program data. All FY2024.</p>
</div>
<hr>
<h2 id="where-the-money-actually-disappears"><a class="heading-anchor" href="#where-the-money-actually-disappears">Where the money actually disappears</a></h2>
<p>Here's where the real gaps are. Every item below is documented by federal auditors, inspectors general, or official government data.</p>
<h3 id="the-tax-gap-606-billion-per-year"><a class="heading-anchor" href="#the-tax-gap-606-billion-per-year">The tax gap: $606 billion per year</a></h3>
<p>The IRS estimates a net <a href="https://www.irs.gov/newsroom/irs-releases-2022-tax-gap-projections-voluntary-compliance-rate-among-taxpayers-remains-steady" target="_blank" rel="noopener noreferrer">$606 billion gap</a> between taxes owed and taxes actually collected (tax year 2022), after accounting for late payments and enforcement recoveries. The gross gap before those recoveries is $696 billion. Both figures have nearly <a href="https://www.congress.gov/crs-product/R47858" target="_blank" rel="noopener noreferrer">doubled since 2001</a>.</p>
<p><a href="https://www.gao.gov/products/gao-24-106449" target="_blank" rel="noopener noreferrer">77% is underreported income</a>, concentrated in business income lacking third-party reporting: sole proprietors, pass-throughs, and partnerships. The GAO and CRS have flagged this as a persistent, structural problem on the <a href="https://www.gao.gov/products/gao-25-107743" target="_blank" rel="noopener noreferrer">GAO High-Risk List</a> for years.</p>
<p>The tax gap is uncollected revenue, not direct spending, so the remedies differ. But the net tax gap is nearly 3x the entire safety net, every single year.</p>
<h3 id="government-wide-fraud-and-improper-payments"><a class="heading-anchor" href="#government-wide-fraud-and-improper-payments">Government-wide fraud and improper payments</a></h3>
<p>In 2024, the GAO published its <a href="https://www.gao.gov/products/gao-24-105833" target="_blank" rel="noopener noreferrer">first-ever government-wide fraud estimate</a>: the federal government loses between $233 billion and $521 billion annually to fraud, or 3-7% of average annual obligations. Even the low-end estimate exceeds the entire safety net. This excludes tax fraud (that's the tax gap above) and state-level fraud.</p>
<p>Separately, the GAO <a href="https://www.gao.gov/products/gao-25-107753" target="_blank" rel="noopener noreferrer">reported $162 billion</a> in improper payments in FY2024. The largest components:</p>
<table>
<thead>
<tr>
<th scope="col">Program</th>
<th scope="col">Improper Payments (FY2024)</th>
<th scope="col">Error Rate</th>
<th scope="col">Source</th>
</tr>
</thead>
<tbody>
<tr>
<td>Medicare (all parts)</td>
<td>$54.3B</td>
<td>5.6-7.7%</td>
<td><a href="https://www.cms.gov/newsroom/fact-sheets/fiscal-year-2024-improper-payments-fact-sheet" target="_blank" rel="noopener noreferrer">CMS</a></td>
</tr>
<tr>
<td>Medicaid</td>
<td>$31.1B</td>
<td>5.1%</td>
<td><a href="https://www.cms.gov/newsroom/fact-sheets/fiscal-year-2024-improper-payments-fact-sheet" target="_blank" rel="noopener noreferrer">CMS</a></td>
</tr>
<tr>
<td>Earned Income Tax Credit</td>
<td>$21.9B</td>
<td>33.9%</td>
<td><a href="https://www.congress.gov/crs-product/R48296" target="_blank" rel="noopener noreferrer">IRS / CRS</a></td>
</tr>
</tbody>
</table>
<p>Most improper payments aren't fraud. <a href="https://www.gao.gov/products/gao-24-107487" target="_blank" rel="noopener noreferrer">68% of Medicare errors</a> and 74% of Medicaid errors involve missing documentation: systemic process failures, not criminal activity. But the dollars are real.</p>
<h3 id="defense-spending-12-trillion-and-no-accountability"><a class="heading-anchor" href="#defense-spending-12-trillion-and-no-accountability">Defense spending: $1.2 trillion and no accountability</a></h3>
<p>The <a href="https://www.congress.gov/crs-product/R47582" target="_blank" rel="noopener noreferrer">FY2024 NDAA</a> authorized $886 billion for national defense. The actual national security footprint is larger:</p>
<div class="budget-chart" role="img" aria-label="National security spending at approximately $1.2 trillion compared to all 8 safety net programs at $221 billion">
  <div class="chart-row">
    <span class="chart-label">National Security</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-red" style="width: 85%"></div>
      <span class="chart-value">~$1.2 trillion</span>
    </div>
  </div>
  <div class="chart-row">
    <span class="chart-label">Safety Net (all 8)</span>
    <div class="chart-bar-wrap">
      <div class="chart-bar chart-bar-amber" style="width: 15.5%"></div>
      <span class="chart-value">$221 billion</span>
    </div>
  </div>
</div>
<table>
<thead>
<tr>
<th scope="col">Component</th>
<th scope="col">FY2024 Cost</th>
<th scope="col">Source</th>
</tr>
</thead>
<tbody>
<tr>
<td>DOD military activities</td>
<td>$832B</td>
<td><a href="https://www.congress.gov/crs-product/R47582" target="_blank" rel="noopener noreferrer">CRS R47582</a></td>
</tr>
<tr>
<td>DOE nuclear weapons programs</td>
<td>$35.1B</td>
<td><a href="https://www.congress.gov/crs-product/R47582" target="_blank" rel="noopener noreferrer">CRS R47582</a></td>
</tr>
<tr>
<td>Other defense (FBI CI, CISA, etc.)</td>
<td>$12.1B</td>
<td><a href="https://www.congress.gov/crs-product/R47582" target="_blank" rel="noopener noreferrer">CRS R47582</a></td>
</tr>
<tr>
<td>Veterans Affairs</td>
<td>$310B</td>
<td><a href="https://www.congress.gov/crs-product/R48056" target="_blank" rel="noopener noreferrer">CRS R48056</a></td>
</tr>
<tr>
<td><strong>Total national security</strong></td>
<td><strong>~$1.2T</strong></td>
<td>Combined</td>
</tr>
</tbody>
</table>
<p class="chart-note">All figures FY2024. National security includes DOD, DOE nuclear, intelligence, and VA. Safety net is SNAP, TANF, WIC, Section 8, LIHEAP, Pell Grants, Head Start, and school lunch.</p>
<p>The DOD's annual budget was 3.8x more than all eight safety net programs combined. The full national security footprint reaches 5.4x the safety net cost.</p>
<p>The Department of Defense is the only federal agency that has <a href="https://www.gao.gov/products/gao-25-108191" target="_blank" rel="noopener noreferrer">never passed a financial audit</a>. Required to be audit-ready since 1990, the DOD wasn't audited until FY2018. It has failed every one since.</p>
<div class="audit-grid">
  <div class="audit-year audit-fail">
    <span>FAIL</span>
    <span class="audit-year-label">FY2018</span>
  </div>
  <div class="audit-year audit-fail">
    <span>FAIL</span>
    <span class="audit-year-label">FY2019</span>
  </div>
  <div class="audit-year audit-fail">
    <span>FAIL</span>
    <span class="audit-year-label">FY2020</span>
  </div>
  <div class="audit-year audit-fail">
    <span>FAIL</span>
    <span class="audit-year-label">FY2021</span>
  </div>
  <div class="audit-year audit-fail">
    <span>FAIL</span>
    <span class="audit-year-label">FY2022</span>
  </div>
  <div class="audit-year audit-fail">
    <span>FAIL</span>
    <span class="audit-year-label">FY2023</span>
  </div>
  <div class="audit-year audit-fail">
    <span>FAIL</span>
    <span class="audit-year-label">FY2024</span>
  </div>
</div>
<p>In the most recent audit (November 2024), the DOD <a href="https://econofact.org/factbrief/has-the-pentagon-failed-its-7th-audit-in-a-row" target="_blank" rel="noopener noreferrer">could not adequately document 63% of its $3.8 trillion in assets</a>. This doesn't mean $2.4 trillion is missing or stolen; it means accounting systems are so poor that auditors cannot verify whether these assets exist, are properly maintained, or are allocated efficiently. The GAO found <a href="https://www.gao.gov/products/gao-25-108191" target="_blank" rel="noopener noreferrer">$10.8 billion in confirmed fraud</a> between FY2017 and FY2024. DOD financial management has been on the <a href="https://www.gao.gov/products/gao-25-108191" target="_blank" rel="noopener noreferrer">GAO High-Risk List</a> every year since 1995.</p>
<p>The F-35 Joint Strike Fighter will exceed <a href="https://www.gao.gov/blog/f-35-will-now-exceed-2-trillion-military-plans-fly-it-less" target="_blank" rel="noopener noreferrer">$2 trillion in lifetime costs</a> according to the GAO, with sustainment costs alone up 44% since 2018. Over a decade behind schedule, the military now plans to fly the jets <em>less</em> than originally intended.</p>
<p>In FY2024, the DOD awarded <a href="https://www.congress.gov/crs-product/IF10600" target="_blank" rel="noopener noreferrer">$456.2 billion in contracts</a>. Lockheed Martin alone received $50.7 billion, more than TANF, Head Start, WIC, and LIHEAP combined.</p>
<p>Boeing paid <a href="https://www.justice.gov/archive/opa/pr/2006/June/06_civ_412.html" target="_blank" rel="noopener noreferrer">$615 million</a> for misusing competitor data; KBR paid <a href="https://www.justice.gov/archive/opa/pr/2009/April/09-civ-330.html" target="_blank" rel="noopener noreferrer">$579 million</a> for kickbacks on military contracts. In FY2023, the DOJ recovered <a href="https://www.justice.gov/civil/pages/attachments/2024/02/22/fraud_statistics_-_fca_-_2023.pdf" target="_blank" rel="noopener noreferrer">$2.68 billion</a> in False Claims Act settlements. This is just what gets caught.</p>
<h3 id="systemic-waste"><a class="heading-anchor" href="#systemic-waste">Systemic waste</a></h3>
<p><strong>Legacy IT: $83 billion per year on maintenance.</strong> The federal government spends approximately <a href="https://www.gao.gov/products/gao-25-107795" target="_blank" rel="noopener noreferrer">$105 billion annually on IT</a>. Of that, $83 billion (79%) maintains existing systems, not modernization. The Treasury Department still runs systems <a href="https://www.gao.gov/products/gao-23-106821" target="_blank" rel="noopener noreferrer">dating to the late 1960s</a>. GAO identified 11 legacy systems most in need of modernization: 8 use outdated languages like COBOL, 4 run on unsupported hardware, and 7 have known cybersecurity vulnerabilities.</p>
<p><a href="https://www.gao.gov/products/gao-25-107852" target="_blank" rel="noopener noreferrer">93.6% of federal IT projects</a> costing more than $10 million have experienced cost overruns, schedule delays, or both. Since 2010, GAO has made over 1,800 IT management recommendations. 463 remain unimplemented. IT management has been on the GAO High-Risk List since 2015.</p>
<p><strong>77,000 empty federal buildings.</strong> Across 24 surveyed agencies, federal buildings average an 80% vacancy rate. The federal government owns or leases <a href="https://www.gao.gov/products/gao-25-108400" target="_blank" rel="noopener noreferrer">77,000 buildings</a> that are empty or underutilized. The deferred maintenance backlog has doubled from $171 billion in FY2017 to <a href="https://www.gao.gov/products/gao-25-108400" target="_blank" rel="noopener noreferrer">$370 billion in FY2024</a>. Annual operating costs: approximately <a href="https://www.gao.gov/products/gao-25-108159" target="_blank" rel="noopener noreferrer">$7 billion</a>.</p>
<p>Federal real property management has been on the <a href="https://www.gao.gov/products/gao-25-108159" target="_blank" rel="noopener noreferrer">GAO High-Risk List since 2003</a>. GSA identified 45 properties for disposal saving $106 million annually in operations and $3 billion in deferred maintenance. That's 45 out of 77,000.</p>
<p><strong>Year-end spending sprees.</strong> Federal agencies operate under "use it or lose it" budgeting: unspent funds at the end of the fiscal year get returned, creating a predictable spending surge every September. Government-wide, agencies <a href="https://www.ntu.org/foundation/tax-page/use-it-or-lose-it-spending-analysis" target="_blank" rel="noopener noreferrer">spend approximately 16.5% of annual budgets in the final month</a> (vs. the expected 8.3%), with 8.7% in the final week alone.</p>
<p>Quality suffers: contracts initiated during the last week of the fiscal year are <a href="https://www.mercatus.org/research/federal-testimonies/curbing-wasteful-year-end-federal-government-spending-reforming-use-it" target="_blank" rel="noopener noreferrer">at least 2.3x more likely</a> to receive lower quality scores. The GAO first flagged this pattern in 1978. Nearly half a century later, nothing has changed.</p>
<p><strong>Duplicative programs.</strong> Every year since 2011, the GAO publishes a <a href="https://www.gao.gov/products/gao-25-107604" target="_blank" rel="noopener noreferrer">report on duplication, overlap, and fragmentation</a> across federal programs. The 2025 report identified <a href="https://www.gao.gov/duplication-cost-savings" target="_blank" rel="noopener noreferrer">$100 billion or more</a> in potential savings from unimplemented recommendations.</p>
<p>Of <a href="https://www.gao.gov/products/gao-12-491" target="_blank" rel="noopener noreferrer">26 federal homelessness programs</a> across multiple agencies, only 2 had conducted a program evaluation in the prior 5 years. The government also operates <a href="https://www.gao.gov/products/gao-18-290" target="_blank" rel="noopener noreferrer">163 STEM education programs</a> across 13 agencies ($3 billion/year) and <a href="https://www.gao.gov/products/gao-11-92" target="_blank" rel="noopener noreferrer">47 employment and training programs</a> across 9 agencies, with the GAO finding substantial overlap in populations served and services offered.</p>
<p>Since 2011, implemented GAO recommendations have saved <a href="https://www.gao.gov/duplication-cost-savings" target="_blank" rel="noopener noreferrer">$725 billion</a> in cumulative financial benefits. Hundreds of recommendations remain open.</p>
<hr>
<h2 id="who-needs-these-programs-and-why"><a class="heading-anchor" href="#who-needs-these-programs-and-why">Who needs these programs, and why</a></h2>
<p>The budget math is clear: waste dwarfs the safety net. But there's a second problem with the "cut benefits" argument. It assumes these programs are charity. The evidence shows they're not.</p>
<p>Some people think that safety net recipients just need to work harder. <a href="https://fns-prod.azureedge.us/sites/default/files/resource-files/snap-FY23-Characteristics-Report.pdf" target="_blank" rel="noopener noreferrer">55% of SNAP households with children</a> already work. Their jobs don't pay enough to cover food. As for the claim that benefits discourage work: SNAP already requires able-bodied adults without dependents to work or participate in job training to maintain eligibility.</p>
<p>Federal policy created these conditions, and they have compounded across generations.</p>
<h3 id="the-wealth-gap-is-a-policy-outcome"><a class="heading-anchor" href="#the-wealth-gap-is-a-policy-outcome">The wealth gap is a policy outcome</a></h3>
<p>The <a href="https://www.federalreserve.gov/econres/notes/feds-notes/greater-wealth-greater-uncertainty-changes-in-racial-inequality-in-the-survey-of-consumer-finances-20231018.html" target="_blank" rel="noopener noreferrer">Federal Reserve's 2022 Survey of Consumer Finances</a> (the most recent available) found median household wealth of $284,310 for white families and $44,100 for Black families. For every $100 in wealth a white family holds, a Black family holds $15.</p>
<p>Federal housing policies enacted between the New Deal and the Fair Housing Act built this gap. From 1934 to 1968, the FHA required racial segregation in federally insured housing. The Home Owners' Loan Corporation <a href="https://ncrc.org/decades-of-disinvestment/" target="_blank" rel="noopener noreferrer">graded neighborhoods by race</a>, marking Black neighborhoods as "hazardous" in red, the origin of the term "redlining." Of the <a href="https://massbudget.org/2021/08/06/a-history-of-racist-federal-housing-policies/" target="_blank" rel="noopener noreferrer">$120 billion in federal housing subsidies</a> between 1934 and 1962, only 2% reached nonwhite families. After WWII, <a href="https://consumerfed.org/wp-content/uploads/2022/03/Housing-African-American-VA-Home-Loan-Benefits-Report.pdf" target="_blank" rel="noopener noreferrer">only 0.7% of Black veterans</a> obtained home loans under the GI Bill, compared to millions of white veterans. <a href="https://heller.brandeis.edu/iere/pdfs/racial-wealth-equity/racial-wealth-gap/gi-bill-final-report.pdf" target="_blank" rel="noopener noreferrer">Banks refused mortgages</a> in Black neighborhoods. Suburban developments like Levittown were restricted to white buyers by deed covenant.</p>
<p>These exclusions didn't end with the Fair Housing Act. Predatory lending in the 1990s and 2000s disproportionately targeted Black and Hispanic neighborhoods, and disparities in post-2008 foreclosure recovery widened the gap further.</p>
<p><a href="https://fred.stlouisfed.org/series/NHWAHORUSQ156N" target="_blank" rel="noopener noreferrer">Homeownership rates today</a>: 75.1% for white families, 44.2% for Black families. That 31-point gap stems directly from decades of federal subsidies for white homeownership and systematic exclusion of everyone else.</p>
<h3 id="poverty-rates-reflect-compounding-disadvantage"><a class="heading-anchor" href="#poverty-rates-reflect-compounding-disadvantage">Poverty rates reflect compounding disadvantage</a></h3>
<p><a href="https://www.census.gov/library/publications/2025/demo/p60-287.html" target="_blank" rel="noopener noreferrer">The Census Bureau reports</a> poverty rates of 18.4% for Black Americans and 7.7% for white Americans. Wage gaps relative to white men tell the same story: <a href="https://www.bls.gov/charts/usual-weekly-earnings/usual-weekly-earnings-current-quarter-by-race-and-sex.htm" target="_blank" rel="noopener noreferrer">Black men earn 84.6 cents</a> per dollar; Hispanic men earn 75.8 cents. The gap compounds for women: <a href="https://www.bls.gov/charts/usual-weekly-earnings/usual-weekly-earnings-current-quarter-by-race-and-sex.htm" target="_blank" rel="noopener noreferrer">Black women earn 74 cents, Hispanic women earn 64 cents</a>.</p>
<p>These gaps persist across every metric the "bootstrap" narrative cites. <a href="https://www.aeaweb.org/articles?id=10.1257/0002828042002561" target="_blank" rel="noopener noreferrer">Bertrand and Mullainathan (2004)</a> found that a white-sounding name on a resume generated 50% more callbacks, equivalent to roughly 8 additional years of experience. <a href="https://academic.oup.com/qje/article-abstract/137/4/1963/6605934" target="_blank" rel="noopener noreferrer">Kline, Rose, and Walters (2022)</a> confirmed the pattern persists: across 83,000 applications to Fortune 500 firms, identical resumes with Black-sounding names received 2.1% fewer callbacks.</p>
<h3 id="mobility-is-not-equal"><a class="heading-anchor" href="#mobility-is-not-equal">Mobility is not equal</a></h3>
<p>Even Black boys raised in the top 1% of household income earn less in adulthood than white boys raised at the median. Economist Raj Chetty's <a href="https://opportunityinsights.org/paper/race/" target="_blank" rel="noopener noreferrer">research on intergenerational mobility</a> found that Black children's income falls approximately 13 percentiles below white children at <em>every</em> parental income level.</p>
<p>This gap appears in 99% of Census tracts, even after controlling for parental education, marital status, and wealth. The structural barriers persist today, not as relics of the past.</p>
<h3 id="so-who-uses-safety-net-programs"><a class="heading-anchor" href="#so-who-uses-safety-net-programs">So who uses safety net programs?</a></h3>
<p>Black Americans, roughly 13% of the U.S. population, account for <a href="https://fns-prod.azureedge.us/sites/default/files/resource-files/snap-FY23-Characteristics-Report.pdf" target="_blank" rel="noopener noreferrer">25.7% of SNAP recipients</a>, <a href="https://www.hud.gov/helping-americans/public-indian-housing-hcv-dashboard" target="_blank" rel="noopener noreferrer">45% of Housing Choice Voucher holders</a>, <a href="https://acf.gov/ofa/data/characteristics-and-financial-circumstances-tanf-recipients-fiscal-year-2020" target="_blank" rel="noopener noreferrer">approximately 30% of TANF recipients</a>, and <a href="https://headstart.gov/program-data/article/head-start-program-facts-fiscal-year-2024" target="_blank" rel="noopener noreferrer">29% of Head Start enrollees</a>. <a href="https://nces.ed.gov/programs/raceindicators/indicator_rec.asp" target="_blank" rel="noopener noreferrer">72% of Black college students receive Pell Grants</a>, compared to 34% of white students.</p>
<p>Federal policy locked these communities out of mortgages, hiring, education, and wealth building for decades. These programs exist to begin addressing the damage. The cost of that entire effort, all eight programs serving tens of millions of people, is 3.3% of the federal budget.</p>
<hr>
<h2 id="safety-net-spending-generates-economic-returns"><a class="heading-anchor" href="#safety-net-spending-generates-economic-returns">Safety net spending generates economic returns</a></h2>
<p>Low-income recipients spend benefits immediately at local stores, and that money flows to distributors, farmers, and their employees. The USDA's Economic Research Service <a href="https://www.ers.usda.gov/amber-waves/2019/july/quantifying-the-impact-of-snap-benefits-on-the-u-s-economy-and-jobs" target="_blank" rel="noopener noreferrer">found that every $1 in SNAP benefits generates approximately $1.54 in GDP</a> during economic downturns.</p>
<p>The CBO has published <a href="https://www.cbo.gov/sites/default/files/114th-congress-2015-2016/workingpaper/49925-FiscalMultiplier_1.pdf" target="_blank" rel="noopener noreferrer">fiscal multiplier analyses</a> showing where different types of spending land:</p>
<table>
<thead>
<tr>
<th scope="col">Policy Type</th>
<th scope="col">Fiscal Multiplier</th>
<th scope="col">Source</th>
</tr>
</thead>
<tbody>
<tr>
<td>Government purchases (goods &#x26; services)</td>
<td>0.5 - 2.5x</td>
<td>CBO</td>
</tr>
<tr>
<td>Transfers to individuals (SNAP, etc.)</td>
<td>0.4 - 2.1x</td>
<td>CBO</td>
</tr>
<tr>
<td>Tax cuts (lower/middle income)</td>
<td>0.3 - 1.5x</td>
<td>CBO</td>
</tr>
<tr>
<td>Tax cuts (higher income)</td>
<td>0.1 - 0.6x</td>
<td>CBO</td>
</tr>
<tr>
<td>Corporate tax provisions</td>
<td>0.0 - 0.4x</td>
<td>CBO</td>
</tr>
</tbody>
</table>
<p>These multipliers measure GDP impact, not direct federal revenue returned. But the pattern is clear: providing financial assistance to people who will immediately spend it generates more economic activity than tax cuts for those likely to save. A dollar of SNAP benefits does more for GDP than a dollar of corporate tax relief.</p>
<hr>
<h2 id="the-math-doesnt-add-up"><a class="heading-anchor" href="#the-math-doesnt-add-up">The math doesn't add up</a></h2>
<p>Eliminating every safety net program would mean 42 million people losing food assistance, 2.3 million families losing housing vouchers, 30 million children losing school lunch, and 6.3 million students losing Pell Grants. The savings: 3.3% of the federal budget.</p>
<p>The net tax gap alone is nearly 3x that. The government's own fraud estimate equals the entire safety net. The Pentagon cannot account for 63% of its assets.</p>
<p>Some argue that waste is harder to eliminate than cutting benefits. That's partly true: tax enforcement requires IRS funding (which has historically been defunded), IT modernization requires upfront capital, and reducing contractor fraud requires litigation. It seems more logical to focus on recouping some of the massive fraud, waste, and overspending rampant throughout the federal budget than to try to save a few percent of the total budget by defunding programs that help Americans in need.</p>
<p>"Pull yourself up by your bootstraps" requires boots. When the federal government systematically excluded communities from mortgages, education, and wealth building for decades, these programs became the response. Cutting them doesn't fix the budget. It abandons the people our policies already failed.</p>
<details class="sources-section">
<summary><h2 id="sources">Sources</h2></summary>
<ol class="sources-list">
  <li>Congressional Budget Office. <a href="https://www.cbo.gov/publication/60843/html">"The Budget and Economic Outlook: 2025 to 2035."</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/high-risk-list">"High-Risk List."</a></li>
  <li>Internal Revenue Service. <a href="https://www.irs.gov/newsroom/irs-releases-2022-tax-gap-projections-voluntary-compliance-rate-among-taxpayers-remains-steady">"IRS Releases 2022 Tax Gap Projections."</a></li>
  <li>Centers for Medicare &amp; Medicaid Services. <a href="https://www.cms.gov/newsroom/fact-sheets/fiscal-year-2024-improper-payments-fact-sheet">"Fiscal Year 2024 Improper Payments Fact Sheet."</a></li>
  <li>USDA Economic Research Service. <a href="https://www.ers.usda.gov/topics/food-nutrition-assistance/supplemental-nutrition-assistance-program-snap">"Supplemental Nutrition Assistance Program (SNAP)."</a></li>
  <li>USDA Food and Nutrition Service. <a href="https://fns-prod.azureedge.us/sites/default/files/resource-files/snap-FY23-Characteristics-Report.pdf">"SNAP Characteristics Report, FY2023."</a></li>
  <li>Department of Housing and Urban Development. <a href="https://www.hud.gov/topics/housing_choice_voucher_program_section_8">"Housing Choice Voucher Program (Section 8)."</a></li>
  <li>Congressional Research Service. <a href="https://www.congress.gov/crs-product/R45418">"Federal Pell Grant Program: Overview" (R45418).</a></li>
  <li>USDA Economic Research Service. <a href="https://www.ers.usda.gov/topics/food-nutrition-assistance/child-nutrition-programs/national-school-lunch-program">"National School Lunch Program."</a></li>
  <li>Administration for Children and Families. <a href="https://www.acf.hhs.gov/ofa/programs/tanf">"Temporary Assistance for Needy Families (TANF)."</a></li>
  <li>Head Start. <a href="https://headstart.gov/program-data/article/head-start-program-facts-fiscal-year-2024">"Program Facts, Fiscal Year 2024."</a></li>
  <li>USDA Economic Research Service. <a href="https://www.ers.usda.gov/topics/food-nutrition-assistance/wic-program">"WIC Program."</a></li>
  <li>Administration for Children and Families. <a href="https://www.acf.hhs.gov/ocs/programs/liheap">"Low Income Home Energy Assistance Program (LIHEAP)."</a></li>
  <li>Center on Budget and Policy Priorities. <a href="https://www.cbpp.org/research/family-income-support/tanf-at-25-overly-restrictive-funding-structure-and-state-flexibility">"TANF at 25: Overly Restrictive Funding Structure and State Flexibility."</a></li>
  <li>Congressional Research Service. <a href="https://www.congress.gov/crs-product/R47858">"The Tax Gap" (R47858).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-24-106449">"Tax Gap: Underreported Income" (GAO-24-106449).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-25-107743">"Tax Compliance: High-Risk Area" (GAO-25-107743).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-24-105833">"Government-Wide Fraud Estimate" (GAO-24-105833).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-25-107753">"Improper Payments, FY2024" (GAO-25-107753).</a></li>
  <li>Congressional Research Service. <a href="https://www.congress.gov/crs-product/R48296">"Earned Income Tax Credit" (R48296).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-24-107487">"Medicare Improper Payments" (GAO-24-107487).</a></li>
  <li>Congressional Research Service. <a href="https://www.congress.gov/crs-product/R47582">"FY2024 National Defense Authorization Act" (R47582).</a></li>
  <li>Congressional Research Service. <a href="https://www.congress.gov/crs-product/R48056">"Veterans Affairs Appropriations" (R48056).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-25-108191">"DOD Financial Management" (GAO-25-108191).</a></li>
  <li>EconoFact. <a href="https://econofact.org/factbrief/has-the-pentagon-failed-its-7th-audit-in-a-row">"Has the Pentagon Failed Its 7th Audit in a Row?"</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/blog/f-35-will-now-exceed-2-trillion-military-plans-fly-it-less">"F-35 Will Now Exceed $2 Trillion."</a></li>
  <li>Congressional Research Service. <a href="https://www.congress.gov/crs-product/IF10600">"Defense Acquisitions: DOD Contract Spending" (IF10600).</a></li>
  <li>Department of Justice. <a href="https://www.justice.gov/archive/opa/pr/2006/June/06_civ_412.html">"Boeing Settlement."</a></li>
  <li>Department of Justice. <a href="https://www.justice.gov/archive/opa/pr/2009/April/09-civ-330.html">"KBR/Halliburton Settlement."</a></li>
  <li>Department of Justice. <a href="https://www.justice.gov/civil/pages/attachments/2024/02/22/fraud_statistics_-_fca_-_2023.pdf">"False Claims Act Statistics, FY2023."</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-25-107795">"Federal IT Spending" (GAO-25-107795).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-23-106821">"Legacy IT Systems" (GAO-23-106821).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-25-107852">"IT Project Outcomes" (GAO-25-107852).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-25-108400">"Federal Real Property" (GAO-25-108400).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-25-108159">"Federal Buildings: High-Risk Area" (GAO-25-108159).</a></li>
  <li>National Taxpayers Union Foundation. <a href="https://www.ntu.org/foundation/tax-page/use-it-or-lose-it-spending-analysis">"Use It or Lose It Spending Analysis."</a></li>
  <li>Mercatus Center, George Mason University. <a href="https://www.mercatus.org/research/federal-testimonies/curbing-wasteful-year-end-federal-government-spending-reforming-use-it">"Curbing Wasteful Year-End Federal Government Spending."</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-25-107604">"Duplication, Overlap, and Fragmentation" (GAO-25-107604).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/duplication-cost-savings">"Duplication Cost Savings."</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-12-491">"Federal Homelessness Programs" (GAO-12-491).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-18-290">"STEM Education Programs" (GAO-18-290).</a></li>
  <li>Government Accountability Office. <a href="https://www.gao.gov/products/gao-11-92">"Employment and Training Programs" (GAO-11-92).</a></li>
  <li>Federal Reserve Board. <a href="https://www.federalreserve.gov/econres/notes/feds-notes/greater-wealth-greater-uncertainty-changes-in-racial-inequality-in-the-survey-of-consumer-finances-20231018.html">"Greater Wealth, Greater Uncertainty: Changes in Racial Inequality in the Survey of Consumer Finances."</a></li>
  <li>National Community Reinvestment Coalition. <a href="https://ncrc.org/decades-of-disinvestment/">"Decades of Disinvestment."</a></li>
  <li>Massachusetts Budget and Policy Center. <a href="https://massbudget.org/2021/08/06/a-history-of-racist-federal-housing-policies/">"A History of Racist Federal Housing Policies."</a></li>
  <li>Consumer Federation of America. <a href="https://consumerfed.org/wp-content/uploads/2022/03/Housing-African-American-VA-Home-Loan-Benefits-Report.pdf">"Housing and the African American VA Home Loan Benefits Report."</a></li>
  <li>Brandeis University, Heller School. <a href="https://heller.brandeis.edu/iere/pdfs/racial-wealth-equity/racial-wealth-gap/gi-bill-final-report.pdf">"GI Bill and Racial Wealth Gap Report."</a></li>
  <li>Federal Reserve Bank of St. Louis (FRED). <a href="https://fred.stlouisfed.org/series/NHWAHORUSQ156N">"Homeownership Rate by Race."</a></li>
  <li>U.S. Census Bureau. <a href="https://www.census.gov/library/publications/2025/demo/p60-287.html">"Income and Poverty in the United States: 2024" (P60-287).</a></li>
  <li>Bureau of Labor Statistics. <a href="https://www.bls.gov/charts/usual-weekly-earnings/usual-weekly-earnings-current-quarter-by-race-and-sex.htm">"Usual Weekly Earnings by Race and Sex."</a></li>
  <li>Bertrand, Marianne, and Sendhil Mullainathan. <a href="https://www.aeaweb.org/articles?id=10.1257/0002828042002561">"Are Emily and Greg More Employable Than Lakisha and Jamal?"</a> <em>American Economic Review</em> 94, no. 4 (2004).</li>
  <li>Kline, Patrick, Evan K. Rose, and Christopher R. Walters. <a href="https://academic.oup.com/qje/article-abstract/137/4/1963/6605934">"Systemic Discrimination Among Large U.S. Employers."</a> <em>Quarterly Journal of Economics</em> 137, no. 4 (2022).</li>
  <li>Opportunity Insights. <a href="https://opportunityinsights.org/paper/race/">"Race and Economic Opportunity in the United States."</a></li>
  <li>Department of Housing and Urban Development. <a href="https://www.hud.gov/helping-americans/public-indian-housing-hcv-dashboard">"Housing Choice Voucher Dashboard."</a></li>
  <li>Administration for Children and Families. <a href="https://acf.gov/ofa/data/characteristics-and-financial-circumstances-tanf-recipients-fiscal-year-2020">"TANF Recipient Characteristics, FY2020."</a></li>
  <li>National Center for Education Statistics. <a href="https://nces.ed.gov/programs/raceindicators/indicator_rec.asp">"Race and Ethnicity Indicators: Financial Aid."</a></li>
  <li>USDA Economic Research Service. <a href="https://www.ers.usda.gov/amber-waves/2019/july/quantifying-the-impact-of-snap-benefits-on-the-u-s-economy-and-jobs">"Quantifying the Impact of SNAP Benefits on the U.S. Economy and Jobs."</a></li>
  <li>Congressional Budget Office. <a href="https://www.cbo.gov/sites/default/files/114th-congress-2015-2016/workingpaper/49925-FiscalMultiplier_1.pdf">"The Macroeconomic Effects of Federal Fiscal Policy: A Review."</a></li>
</ol>
</details>]]></content:encoded>
            <author>Lucas McComb</author>
        </item>
        <item>
            <title><![CDATA[Shrink clipboard screenshots by 70% before pasting into AI tools]]></title>
            <link>https://lem.fyi/blog/clipboard-screenshot-optimization/</link>
            <guid isPermaLink="false">https://lem.fyi/blog/clipboard-screenshot-optimization/</guid>
            <pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A macOS utility that automatically compresses clipboard screenshots using pngquant, reducing token consumption when pasting into AI coding assistants like Claude Code.]]></description>
            <content:encoded><![CDATA[<p>I paste a lot of screenshots into <a href="https://docs.anthropic.com/en/docs/claude-code/overview" target="_blank" rel="noopener noreferrer">Claude Code</a>. UI bugs, browser state, error messages, design references. Every screenshot gets base64-encoded and sent to the API, consuming tokens. A single Retina screenshot can easily be 200-400KB as a PNG. Across a coding session with 10-20 screenshots, that adds up fast.</p>
<p>The fix: intercept the clipboard and compress the PNG before you paste it. I built a small macOS utility that does this automatically, reducing most screenshots by 50-80% with no visible quality loss.</p>
<hr>
<h2 id="the-problem"><a class="heading-anchor" href="#the-problem">The problem</a></h2>
<p>macOS screenshots are saved as lossless PNGs. This is great for archival quality but wasteful for AI context windows. When you paste a screenshot into Claude Code (or any tool that sends images to an API), the full uncompressed PNG gets transmitted.</p>
<p>A typical macOS screenshot of a code editor or browser window is 150-400KB. After lossy PNG compression, that same image is 40-100KB, and you can't tell the difference.</p>
<h2 id="why-pngquant-not-jpeg"><a class="heading-anchor" href="#why-pngquant-not-jpeg">Why pngquant, not JPEG</a></h2>
<p>The obvious thought is "just convert to JPEG." But JPEG is a bad format for screenshots. It uses DCT-based compression designed for photographs with smooth gradients. Sharp edges (text, UI borders, icons, code) get smudgy compression artifacts. To keep text crisp at JPEG quality 95+, you barely save any space.</p>
<p><a href="https://pngquant.org/" target="_blank" rel="noopener noreferrer">pngquant</a> takes a different approach. It reduces the color palette of a PNG from millions of colors to an optimally chosen subset (up to 256 colors per channel). This works perfectly for screenshots because they're mostly flat UI colors, text, and simple gradients. The output stays as PNG, so edges remain pixel-perfect.</p>
<table>
<thead>
<tr>
<th scope="col">Method</th>
<th scope="col">Typical reduction</th>
<th scope="col">Text quality</th>
</tr>
</thead>
<tbody>
<tr>
<td>JPEG 85</td>
<td>70-80% smaller</td>
<td>Visible artifacts around text</td>
</tr>
<tr>
<td>JPEG 95</td>
<td>40-50% smaller</td>
<td>Mostly fine, some ringing</td>
</tr>
<tr>
<td>pngquant 85-100</td>
<td>50-80% smaller</td>
<td>Pixel-perfect</td>
</tr>
</tbody>
</table>
<p>pngquant gives you JPEG-level file size reduction while keeping PNG-level sharpness. For screenshots of code, terminals, and UIs, it's the right tool.</p>
<h2 id="the-solution"><a class="heading-anchor" href="#the-solution">The solution</a></h2>
<p>I wrote a bash script that runs as a background daemon on macOS. It watches the clipboard for new images, compresses them with pngquant, and replaces the clipboard content with the optimized version. The whole thing is invisible once running.</p>
<h3 id="prerequisites"><a class="heading-anchor" href="#prerequisites">Prerequisites</a></h3>
<p>Install two Homebrew packages:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">brew</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> install</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> pngpaste</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> pngquant</span></span></code></pre>
<ul>
<li><strong>pngpaste</strong> reads image data from the macOS clipboard and saves it as a PNG file</li>
<li><strong>pngquant</strong> performs lossy PNG compression with configurable quality</li>
</ul>
<h3 id="the-script"><a class="heading-anchor" href="#the-script">The script</a></h3>
<p>Create <code>~/bin/clipboard-optimize</code> (or wherever you keep personal scripts):</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E">#!/bin/bash</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">set</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -euo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> pipefail</span></span>
<span class="line"></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">CONFIG_DIR</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$HOME</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">/.config/clipboard-optimize"</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">ENABLED_FILE</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$CONFIG_DIR</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">/enabled"</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">PID_FILE</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$CONFIG_DIR</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">/daemon.pid"</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">LOG_FILE</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$CONFIG_DIR</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">/clipboard-optimize.log"</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">TEMP_DIR</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"${</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">TMPDIR</span><span style="color:#CF222E;--shiki-dark:#FF7B72">:-/</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">tmp</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">}/clipboard-optimize"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">mkdir</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -p</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$CONFIG_DIR</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$TEMP_DIR</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#8250DF;--shiki-dark:#D2A8FF">log</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">() {</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">  echo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "[$(</span><span style="color:#953800;--shiki-dark:#FFA657">date</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> '+%H:%M:%S')] </span><span style="color:#0550AE;--shiki-dark:#79C0FF">$*</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> >></span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$LOG_FILE</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">}</span></span></code></pre>
<p>The script uses <code>~/.config/clipboard-optimize/</code> for state: a toggle file, PID file, and log.</p>
<h3 id="clipboard-change-detection"><a class="heading-anchor" href="#clipboard-change-detection">Clipboard change detection</a></h3>
<p>The first challenge is knowing when the clipboard changes. My initial approach was hashing the clipboard contents with <code>md5</code>, but macOS doesn't round-trip PNGs faithfully. Writing a 61KB optimized PNG to the clipboard and reading it back with <code>pngpaste</code> produces an 82KB file with a different hash, creating an infinite re-optimization loop.</p>
<p>The fix is <code>NSPasteboard.changeCount</code>, which macOS increments every time the clipboard changes. You can access it from bash via <code>osascript</code>:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#8250DF;--shiki-dark:#D2A8FF">get_change_count</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">() {</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">  osascript</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -e</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> 'use framework "AppKit"'</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> \</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">    -e</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "return (current application's NSPasteboard's generalPasteboard()'s changeCount()) as integer"</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> 2></span><span style="color:#0A3069;--shiki-dark:#A5D6FF">/dev/null</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">}</span></span></code></pre>
<p>This returns an integer that only changes when something actually modifies the clipboard. After we write back the optimized image, we capture the new count, so the daemon knows to skip it.</p>
<h3 id="the-optimization-function"><a class="heading-anchor" href="#the-optimization-function">The optimization function</a></h3>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#8250DF;--shiki-dark:#D2A8FF">optimize_clipboard</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">() {</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  local</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> raw</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$TEMP_DIR</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">/raw.png"</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  local</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> opt</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$TEMP_DIR</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">/opt.png"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E">  # Extract image from clipboard (fails if no image present)</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  if</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> !</span><span style="color:#953800;--shiki-dark:#FFA657"> pngpaste</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$raw</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> 2></span><span style="color:#0A3069;--shiki-dark:#A5D6FF">/dev/null</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">; </span><span style="color:#CF222E;--shiki-dark:#FF7B72">then</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">    return</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 1</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  fi</span></span>
<span class="line"></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  local</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> original_size</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">  original_size</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(</span><span style="color:#0550AE;--shiki-dark:#79C0FF">stat</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -f%z</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$raw</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E">  # Skip tiny images (&#x3C; 10KB) — not worth optimizing</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  if</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> [[ $original_size </span><span style="color:#CF222E;--shiki-dark:#FF7B72">-lt</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 10240</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ]]; </span><span style="color:#CF222E;--shiki-dark:#FF7B72">then</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">    rm</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -f</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$raw</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">    return</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 1</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  fi</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E">  # Lossy compression: quality 85-100, max effort, strip metadata</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  if</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> !</span><span style="color:#953800;--shiki-dark:#FFA657"> pngquant</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --quality=85-100</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --speed</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 1</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --strip</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --force</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> \</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">       --output</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$opt</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$raw</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> 2></span><span style="color:#0A3069;--shiki-dark:#A5D6FF">/dev/null</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">; </span><span style="color:#CF222E;--shiki-dark:#FF7B72">then</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">    rm</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -f</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$raw</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$opt</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">    return</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 1</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  fi</span></span>
<span class="line"></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  local</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> opt_size</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">  opt_size</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(</span><span style="color:#0550AE;--shiki-dark:#79C0FF">stat</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -f%z</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$opt</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E">  # Only replace if we saved more than 5%</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  local</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> threshold</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(( </span><span style="color:#953800;--shiki-dark:#FFA657">original_size</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> *</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 95</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> /</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 100</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ))</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  if</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> [[ $opt_size </span><span style="color:#CF222E;--shiki-dark:#FF7B72">-ge</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> $threshold ]]; </span><span style="color:#CF222E;--shiki-dark:#FF7B72">then</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">    rm</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -f</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$raw</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$opt</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">    return</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 1</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  fi</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E">  # Write optimized PNG back to clipboard</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">  osascript</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -e</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "set the clipboard to (read (POSIX file </span><span style="color:#CF222E;--shiki-dark:#FF7B72">\"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$opt</span><span style="color:#CF222E;--shiki-dark:#FF7B72">\"</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">) as «class PNGf»)"</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> 2></span><span style="color:#0A3069;--shiki-dark:#A5D6FF">/dev/null</span></span>
<span class="line"></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  local</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> saved_kb</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(( (</span><span style="color:#953800;--shiki-dark:#FFA657">original_size</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> -</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> opt_size</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">) </span><span style="color:#953800;--shiki-dark:#FFA657">/</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 1024</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ))</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  local</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> orig_kb</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(( </span><span style="color:#953800;--shiki-dark:#FFA657">original_size</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> /</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 1024</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ))</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  local</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> opt_kb</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(( </span><span style="color:#953800;--shiki-dark:#FFA657">opt_size</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> /</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 1024</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ))</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  local</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> pct</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(( (</span><span style="color:#953800;--shiki-dark:#FFA657">original_size</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> -</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> opt_size</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">) </span><span style="color:#953800;--shiki-dark:#FFA657">*</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 100</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> /</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> original_size</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ))</span></span>
<span class="line"></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">  log</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "Optimized: ${</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">orig_kb</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">}KB -> ${</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">opt_kb</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">}KB (saved ${</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">saved_kb</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">}KB, ${</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">pct</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">}%)"</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">  echo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "Optimized: ${</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">orig_kb</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">}KB -> ${</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">opt_kb</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">}KB (-${</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">pct</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">}%)"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">  rm</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -f</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$raw</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$opt</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  return</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 0</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">}</span></span></code></pre>
<p>Key decisions in the flags:</p>
<ul>
<li><code>--quality=85-100</code> keeps quality high. pngquant will skip the optimization entirely if it can't meet the minimum quality of 85, so you never get a bad-looking result.</li>
<li><code>--speed 1</code> uses maximum compression effort (slower but smaller output). Since we're only compressing one image at a time, the extra milliseconds don't matter.</li>
<li><code>--strip</code> removes PNG metadata (color profiles, timestamps, etc.) for additional savings.</li>
<li>The 5% threshold prevents replacing the clipboard when pngquant can barely improve on the original.</li>
</ul>
<h3 id="the-daemon-loop"><a class="heading-anchor" href="#the-daemon-loop">The daemon loop</a></h3>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#8250DF;--shiki-dark:#D2A8FF">daemon_loop</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">() {</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">  log</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "Daemon started (PID </span><span style="color:#0550AE;--shiki-dark:#79C0FF">$$</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">)"</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">  echo</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> $$</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> ></span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$PID_FILE</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  local</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> last_count</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">  last_count</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(</span><span style="color:#953800;--shiki-dark:#FFA657">get_change_count</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">  trap</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> 'log "Daemon stopped"; rm -f "$PID_FILE"; exit 0'</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> SIGTERM</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> SIGINT</span></span>
<span class="line"></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  while</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> true</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">; </span><span style="color:#CF222E;--shiki-dark:#FF7B72">do</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">    if</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> [[ </span><span style="color:#CF222E;--shiki-dark:#FF7B72">-f</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$ENABLED_FILE</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ]]; </span><span style="color:#CF222E;--shiki-dark:#FF7B72">then</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">      local</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> current_count</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">      current_count</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(</span><span style="color:#953800;--shiki-dark:#FFA657">get_change_count</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">      if</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> [[ </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$current_count</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> !=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$last_count</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ]]; </span><span style="color:#CF222E;--shiki-dark:#FF7B72">then</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">        if</span><span style="color:#953800;--shiki-dark:#FFA657"> optimize_clipboard</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">; </span><span style="color:#CF222E;--shiki-dark:#FF7B72">then</span></span>
<span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E">          # Our write incremented changeCount - capture the new value</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">          last_count</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(</span><span style="color:#953800;--shiki-dark:#FFA657">get_change_count</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">)</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">        else</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">          last_count</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$current_count</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">        fi</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">      fi</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">    fi</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">    sleep</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 1</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  done</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">}</span></span></code></pre>
<p>The daemon polls every second. It only attempts optimization when the clipboard <code>changeCount</code> has changed and the <code>enabled</code> toggle file exists. After a successful optimization, it captures the new <code>changeCount</code> (which was incremented by our osascript clipboard write) so it doesn't try to re-optimize the same image.</p>
<h3 id="command-interface"><a class="heading-anchor" href="#command-interface">Command interface</a></h3>
<p>The script supports four modes via the first argument:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">case</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#0550AE;--shiki-dark:#79C0FF">${1</span><span style="color:#CF222E;--shiki-dark:#FF7B72">:-</span><span style="color:#0550AE;--shiki-dark:#79C0FF">}</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> in</span></span>
<span class="line"><span style="color:#0A3069;--shiki-dark:#A5D6FF">  daemon</span><span style="color:#CF222E;--shiki-dark:#FF7B72">)</span><span style="color:#953800;--shiki-dark:#FFA657"> cmd_daemon</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ;;    </span><span style="color:#6E7781;--shiki-dark:#8B949E"># Start background daemon</span></span>
<span class="line"><span style="color:#0A3069;--shiki-dark:#A5D6FF">  stop</span><span style="color:#CF222E;--shiki-dark:#FF7B72">)</span><span style="color:#953800;--shiki-dark:#FFA657">   cmd_stop</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ;;      </span><span style="color:#6E7781;--shiki-dark:#8B949E"># Stop running daemon</span></span>
<span class="line"><span style="color:#0A3069;--shiki-dark:#A5D6FF">  status</span><span style="color:#CF222E;--shiki-dark:#FF7B72">)</span><span style="color:#953800;--shiki-dark:#FFA657"> cmd_status</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ;;    </span><span style="color:#6E7781;--shiki-dark:#8B949E"># Show status and recent optimizations</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">  *)</span><span style="color:#953800;--shiki-dark:#FFA657">      cmd_oneshot</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ;;   </span><span style="color:#6E7781;--shiki-dark:#8B949E"># Optimize current clipboard image (default)</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">esac</span></span></code></pre>
<p>The <code>cmd_daemon</code> function starts the loop in the background with <code>disown</code>, and <code>cmd_stop</code> sends <code>SIGTERM</code> to the PID stored in the PID file.</p>
<h3 id="shell-aliases-for-toggling"><a class="heading-anchor" href="#shell-aliases-for-toggling">Shell aliases for toggling</a></h3>
<p>Add these to your <code>~/.zshrc</code> (or <code>~/.bashrc</code>):</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E"># Clipboard screenshot optimizer</span></span>
<span class="line"><span style="color:#8250DF;--shiki-dark:#D2A8FF">clipopt</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">() { </span><span style="color:#953800;--shiki-dark:#FFA657">clipboard-optimize</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">; }</span></span>
<span class="line"><span style="color:#8250DF;--shiki-dark:#D2A8FF">clipopt-on</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">() {</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">  touch</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ~/.config/clipboard-optimize/enabled</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">  clipboard-optimize</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> daemon</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">}</span></span>
<span class="line"><span style="color:#8250DF;--shiki-dark:#D2A8FF">clipopt-off</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">() {</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">  rm</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -f</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ~/.config/clipboard-optimize/enabled</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">  clipboard-optimize</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> stop</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">}</span></span>
<span class="line"><span style="color:#8250DF;--shiki-dark:#D2A8FF">clipopt-status</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">() { </span><span style="color:#953800;--shiki-dark:#FFA657">clipboard-optimize</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> status</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">; }</span></span></code></pre>
<p>Make sure <code>~/bin</code> is in your PATH:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">export</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> PATH</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$HOME</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">/bin:</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$PATH</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span></span></code></pre>
<h2 id="usage"><a class="heading-anchor" href="#usage">Usage</a></h2>
<p><strong>One-shot mode</strong> (optimize whatever's on the clipboard right now):</p>
<pre><code>$ clipopt
Optimized: 201KB -> 61KB (-69%)
</code></pre>
<p><strong>Daemon mode</strong> (auto-optimize every screenshot you copy):</p>
<pre><code>$ clipopt-on
Starting clipboard-optimize daemon...
Daemon running (PID 79214)
</code></pre>
<p><strong>Check status:</strong></p>
<pre><code>$ clipopt-status
Optimization: ENABLED
Daemon: RUNNING (PID 79214)
Total optimizations: 47

Recent:
[14:23:01] Optimized: 312KB -> 89KB (saved 223KB, 71%)
[14:25:44] Optimized: 187KB -> 52KB (saved 135KB, 72%)
[14:31:12] Optimized: 95KB -> 41KB (saved 54KB, 56%)
</code></pre>
<p><strong>Turn it off:</strong></p>
<pre><code>$ clipopt-off
Daemon stopped
</code></pre>
<h2 id="results"><a class="heading-anchor" href="#results">Results</a></h2>
<p>In my first test, the script compressed a 201KB screenshot down to 61KB, a 69% reduction. Across a typical coding session, here's what I've seen:</p>
<table>
<thead>
<tr>
<th scope="col">Screenshot type</th>
<th scope="col">Before</th>
<th scope="col">After</th>
<th scope="col">Reduction</th>
</tr>
</thead>
<tbody>
<tr>
<td>Code editor (VS Code)</td>
<td>201KB</td>
<td>61KB</td>
<td>69%</td>
</tr>
<tr>
<td>Browser page</td>
<td>312KB</td>
<td>89KB</td>
<td>71%</td>
</tr>
<tr>
<td>Terminal output</td>
<td>95KB</td>
<td>41KB</td>
<td>56%</td>
</tr>
<tr>
<td>Figma design</td>
<td>187KB</td>
<td>52KB</td>
<td>72%</td>
</tr>
</tbody>
</table>
<p>The visual quality is indistinguishable from the originals. Text remains crisp, colors are accurate, and UI elements look identical. The compression works by reducing the color palette intelligently, not by blurring or introducing artifacts.</p>
<hr>
<h2 id="gotchas-i-ran-into"><a class="heading-anchor" href="#gotchas-i-ran-into">Gotchas I ran into</a></h2>
<p><strong>macOS clipboard doesn't round-trip PNGs.</strong> When you write a PNG to the clipboard via <code>osascript</code> and read it back with <code>pngpaste</code>, you get a different binary representation. The image looks the same, but the file size and hash are different. This broke my initial hash-based change detection and caused an infinite loop where the daemon kept re-optimizing the same image every second. The <code>NSPasteboard.changeCount</code> approach solved this cleanly.</p>
<p><strong>pngquant exits non-zero when quality can't be met.</strong> If the input image can't be compressed to meet the <code>--quality</code> minimum, pngquant returns exit code 99. The script handles this gracefully by checking the return code and skipping the replacement.</p>
<p><strong>The daemon needs <code>disown</code>.</strong> Starting the loop with <code>&#x26;</code> alone isn't enough because the background process is still attached to the shell session. <code>disown</code> detaches it so it survives terminal closure.</p>
<h2 id="next-steps"><a class="heading-anchor" href="#next-steps">Next steps</a></h2>
<p>A few improvements I'm considering:</p>
<ul>
<li><strong>launchd integration</strong> to start the daemon at login instead of manually running <code>clipopt-on</code></li>
<li><strong>Notification Center alerts</strong> for each optimization (currently logs only)</li>
<li><strong>Configurable quality</strong> via environment variable or config file</li>
<li><strong>Stats tracking</strong> for total bytes saved across sessions</li>
</ul>
<p>If you work with AI coding assistants and paste a lot of screenshots, this is an easy win. The whole setup takes about five minutes and runs invisibly in the background.</p>]]></content:encoded>
            <author>Lucas McComb</author>
        </item>
        <item>
            <title><![CDATA[Week of February 23, 2026]]></title>
            <link>https://lem.fyi/blog/2026-W09/</link>
            <guid isPermaLink="false">https://lem.fyi/blog/2026-W09/</guid>
            <pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Built a full engagement system for lem.fyi, migrated from Preact to React, launched a shared design system, and pushed Darkly Suite through CWS rejection fixes.]]></description>
            <content:encoded><![CDATA[<p>The week started with a Chrome Web Store rejection email and ended with a shared design system powering three sites. In between, lem.fyi got a full social layer.</p>
<h2 id="what-i-built"><a class="heading-anchor" href="#what-i-built">What I built</a></h2>
<p><strong>lem-fyi</strong> had its biggest week yet - 54 commits across 32 merged PRs. The headline was issue #63: a complete engagement system with likes, comments, Google OAuth, Cloudflare Turnstile bot prevention, Claude API content moderation, and Resend email notifications. Comments support editing, deleting, likes, and single-level replies (#132). The moderation defaults to <code>review</code> on API failure rather than <code>reject</code> - false negatives beat false positives on a personal blog.</p>
<p>The Preact-to-React migration (#128) was mechanical but tricky. React requires <code>charSet</code> (camelCase) and object-style <code>style</code> props where Preact accepted strings. The admin SPA stays on Preact with its own tsconfig to avoid type conflicts.</p>
<p>That migration enabled the real goal: a shared design system. Created <code>@lem/ui</code> (#127) as a standalone package with CSS custom properties, Tailwind v4 preset, and shared React components (Logo, SideNav, BottomNav, ThemeToggle). Wired it into both lem.fyi (#134) and lem-work (#307), cutting ~200 lines from each site. One gotcha: esbuild doesn't respect <code>@jsxImportSource</code> pragmas for files in symlinked packages, so the shared components use classic <code>import React from 'react'</code>.</p>
<p>Post page styling got a full pass (#129) - tighter letter-spacing on headings, rounded code blocks and callouts, responsive font sizes. The TOC went through three iterations (#70, #75, #79) before landing on a flex-based layout visible down to 1024px with a mobile collapsible fallback. Nav styling matched lem.work pixel-for-pixel (#91) after I discovered the root font size difference (18px vs 16px) was inflating all rem-based values by 12.5%.</p>
<p>Also shipped: Stripe donations (#61), floating action buttons (#95), email notifications for likes and comments (#94), Spotify and YouTube embed support for WIR posts (#121, #122), and a blog post on managing multiple GitHub accounts with direnv (#50).</p>
<p><strong>darkly-suite</strong> spent the first half recovering from a Chrome Web Store rejection. Red Potassium flagged the listing for missing subscription disclosure - all features require a paid plan but the description didn't say so (#554). What followed was a compliance marathon: 14 PRs fixing descriptions (#583, #585, #587, #589), trimming test instructions to the 500-char CWS limit (#593), and applying the same fixes to Sheets (#601). Gmail Darkly bumped to 1.0.1 for resubmission (#580).</p>
<p>The second half pivoted to building a screenshot harness for CWS listing images. Six issues (#609-#614) delivered a mock framework with realistic Gmail, Sheets, and Docs page replicas, a Playwright + Sharp pipeline for automated capture and compositing, and per-extension YAML configs for screenshot generation. The mock pages render real-looking UIs with toolbars, sidebars, and content areas that show dark mode applied.</p>
<p>Subscription UX improved: warning banners for canceled and past-due subscriptions (#577, #582), a restore purchase flow for license recovery after reinstall (#564), and a redesigned side panel with collapsible sections (#572).</p>
<p><strong>lem-work</strong> got a consulting booking feature (#301, #304) with Stripe payments and Google Calendar integration, renamed from "Consulting" to "Meet" with a General Inquiry meeting type. The CareConnect provider portal got its showcase page (#287) with deployment to Cloudflare (#290) and live demo links. Portfolio pages shipped for lem.work itself (#264) and eluketronic (#257).</p>
<p>The headshot got an easter egg: hover reveals a silly photo (#275), mousedown shows a no-glasses variant (#277), with mobile touch support (#281) and a question mark cursor hint (#279). The About page was redesigned (#185) with Spotify recently-played integration (#189).</p>
<h2 id="what-i-consumed"><a class="heading-anchor" href="#what-i-consumed">What I consumed</a></h2>
<p>No Spotify or YouTube data collected this week - the polling system wasn't wired up yet. That changes next week.</p>
<h2 id="closing"><a class="heading-anchor" href="#closing">Closing</a></h2>
<p>181 commits, 92 PRs, three repos. The engagement system and shared design system were the big wins. The CWS rejection felt like a setback but forced a proper compliance review that'll prevent future rejections. The screenshot harness means never hand-crafting store listing images again. Next week: get the journal automation actually running (it wasn't), and tackle the about page.</p>]]></content:encoded>
            <author>Lucas McComb</author>
        </item>
        <item>
            <title><![CDATA[Translating a Claude Code Workflow to GitHub Copilot]]></title>
            <link>https://lem.fyi/blog/translating-claude-code-to-copilot/</link>
            <guid isPermaLink="false">https://lem.fyi/blog/translating-claude-code-to-copilot/</guid>
            <pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How I translated my entire Claude Code development infrastructure - commands, hooks, session logging, multi-agent coordination - into GitHub Copilot configs for a Windows VM at work.]]></description>
            <content:encoded><![CDATA[<p>I wrote previously about <a href="/posts/ai-enabled-swe-system">building a multi-agent AI development system with Claude Code</a> - a comprehensive setup with slash commands, enforcement hooks, session logging, multi-agent orchestration, and a reproducible config system that deploys to a new machine in one command.</p>
<p>That system runs on my personal Mac. Then I needed to use GitHub Copilot on a Windows VM for work, and the question became: how much of this workflow translates?</p>
<p>The answer is most of it. Not all of it, and not one-to-one, but the core ideas carry over surprisingly well. This post walks through the translation and what I learned along the way.</p>
<hr>
<h2 id="why-not-just-use-claude-code-on-both"><a class="heading-anchor" href="#why-not-just-use-claude-code-on-both">Why not just use Claude Code on both?</a></h2>
<p>My employer provides a Windows VM with GitHub Copilot Enterprise. The work happens there. Claude Code is a terminal-native tool that runs best on macOS/Linux. Even if I could run it on the VM, Copilot is what the team uses, and there's value in working within the same tool ecosystem as your coworkers.</p>
<p>But the workflow patterns I've built, the commit conventions, the session logging, the quality gates, those aren't Claude-specific ideas. They're development workflow ideas that happen to be implemented in Claude Code. Translating them to Copilot was about carrying the workflow forward, not the tool.</p>
<hr>
<h2 id="the-mapping"><a class="heading-anchor" href="#the-mapping">The mapping</a></h2>
<p>Here's the high-level translation table. Each Claude Code concept has a Copilot equivalent, though the implementation details differ.</p>
<table>
<thead>
<tr>
<th scope="col">Claude Code</th>
<th scope="col">Copilot Equivalent</th>
<th scope="col">Location</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>~/.claude/CLAUDE.md</code> (global instructions)</td>
<td>VS Code user settings + <code>copilot-instructions.md</code></td>
<td><code>.vscode/settings.json</code> + <code>.github/copilot-instructions.md</code></td>
</tr>
<tr>
<td><code>~/.claude/commands/*.md</code> (slash commands)</td>
<td>Prompt files</td>
<td><code>.github/prompts/*.prompt.md</code></td>
</tr>
<tr>
<td><code>~/.claude/hooks/*.py</code> (enforcement hooks)</td>
<td>Husky git hooks</td>
<td><code>.husky/pre-commit</code>, <code>.husky/commit-msg</code></td>
</tr>
<tr>
<td><code>.claudeignore</code></td>
<td><code>.copilotignore</code></td>
<td>Root of each repo</td>
</tr>
<tr>
<td><code>~/.claude/log-system.md</code></td>
<td>Reference doc</td>
<td><code>docs/log-system.md</code></td>
</tr>
<tr>
<td><code>~/.claude/multi-agent-system.md</code></td>
<td>Reference doc</td>
<td><code>docs/multi-agent-system.md</code></td>
</tr>
<tr>
<td>Per-project <code>CLAUDE.md</code></td>
<td>Per-project <code>copilot-instructions.md</code></td>
<td><code>.github/copilot-instructions.md</code></td>
</tr>
<tr>
<td>N/A (Claude loads context hierarchy)</td>
<td>Conditional instruction files</td>
<td><code>.github/instructions/*.instructions.md</code></td>
</tr>
</tbody>
</table>
<hr>
<h2 id="global-instructions-claudemd-to-copilot-instructionsmd"><a class="heading-anchor" href="#global-instructions-claudemd-to-copilot-instructionsmd">Global instructions: CLAUDE.md to copilot-instructions.md</a></h2>
<p>In Claude Code, <code>~/.claude/CLAUDE.md</code> is a single markdown file that gets loaded into every conversation. It contains everything: commit conventions, code standards, error handling patterns, common mistakes to avoid, security rules. One file, always present.</p>
<p>My initial translation split this across two mechanisms: inline JSON instruction arrays in VS Code user settings, and a <code>copilot-instructions.md</code> file per repo. The settings approach looked like this:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">{</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">  "github.copilot.chat.codeGeneration.instructions"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: [</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">    { </span><span style="color:#116329;--shiki-dark:#7EE787">"text"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"Never add AI attribution to git commits or PR descriptions."</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> },</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">    { </span><span style="color:#116329;--shiki-dark:#7EE787">"text"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"Use functional React components with TypeScript interfaces for props."</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> }</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">  ],</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">  "github.copilot.chat.commitMessageGeneration.instructions"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: [</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">    { </span><span style="color:#116329;--shiki-dark:#7EE787">"text"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"Format: {issue_number}: {brief description}. Never include AI attribution."</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> }</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">  ]</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">}</span></span></code></pre>
<p>This seemed nicer than Claude Code's approach - instructions scoped to specific Copilot behaviors (code generation, commit messages, reviews, PR descriptions) instead of one big file where the model has to figure out what applies.</p>
<p><strong>Then I actually set it up on the VM.</strong> VS Code immediately flagged every one of these with deprecation warnings: "Use instructions files instead." The <code>github.copilot.chat.*.instructions</code> inline settings are deprecated in favor of the instruction file system. All those carefully structured JSON arrays? Unnecessary. The same rules belong in <code>.github/copilot-instructions.md</code> and <code>.github/instructions/*.instructions.md</code>.</p>
<p>This was a good lesson in the gap between documentation and reality. The settings API still technically works, but VS Code actively warns against it. The instruction files are the correct path forward.</p>
<p><strong>copilot-instructions.md</strong> (<code>.github/copilot-instructions.md</code>) is where all the rules actually live. Git workflow documentation, session logging protocols, code standards with examples, commit conventions, the full "common mistakes to avoid" section. This file lives in each repo and is loaded automatically when Copilot operates in that workspace. It's the direct equivalent of <code>CLAUDE.md</code>.</p>
<hr>
<h2 id="slash-commands-commands-to-prompts"><a class="heading-anchor" href="#slash-commands-commands-to-prompts">Slash commands: commands/ to prompts/</a></h2>
<p>Claude Code's slash commands live in <code>~/.claude/commands/</code> as markdown files with YAML frontmatter. They use <code>!</code> prefix syntax to execute shell commands inline when the command is invoked, which means the command can pre-load git state, issue lists, and other context before Claude even starts thinking.</p>
<p>Copilot's equivalent is <code>.prompt.md</code> files in <code>.github/prompts/</code>. These appear in the <code>/</code> autocomplete menu in Copilot Chat when you're in Agent mode. The frontmatter is slightly different:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">---</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">agent</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">'agent'</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">tools</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: [</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">'#runInTerminal'</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">, </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">'#readFile'</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">, </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">'#editFiles'</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">]</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">description</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">'Stage all changes and commit with conventional format'</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">---</span></span></code></pre>
<p>The <code>tools</code> array explicitly declares which capabilities the prompt needs. <code>#runInTerminal</code> lets it execute shell commands, <code>#readFile</code> lets it read files, <code>#editFiles</code> lets it make changes, and <code>#codebase</code> lets it search across the project.</p>
<p>Here's how the nine Claude Code commands mapped:</p>
<table>
<thead>
<tr>
<th scope="col">Claude Code</th>
<th scope="col">Copilot</th>
<th scope="col">Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/startup</code></td>
<td><code>/startup</code></td>
<td>Same concept. Copilot version instructs agent to run commands as steps instead of pre-loading context with <code>!</code> syntax</td>
</tr>
<tr>
<td><code>/commit</code></td>
<td><code>/commit</code></td>
<td>Direct translation. Uses <code>${input:commitMessage}</code> for user input</td>
</tr>
<tr>
<td><code>/pr</code></td>
<td><code>/pr</code></td>
<td>Direct translation. Includes PR template check</td>
</tr>
<tr>
<td><code>/sync</code></td>
<td><code>/sync</code></td>
<td>Direct translation</td>
</tr>
<tr>
<td><code>/new-issue</code></td>
<td><code>/new-issue</code></td>
<td>Direct translation</td>
</tr>
<tr>
<td><code>/gs</code></td>
<td><code>/gs</code></td>
<td>Direct translation</td>
</tr>
<tr>
<td><code>/walkthrough</code></td>
<td><code>/walkthrough</code></td>
<td>Direct translation</td>
</tr>
<tr>
<td><code>/audit</code></td>
<td><code>/audit</code></td>
<td>Simplified from 7-phase parallel to sequential single-agent</td>
</tr>
<tr>
<td><code>/dotsync</code></td>
<td>Not migrated</td>
<td>Claude-specific dotfiles sync</td>
</tr>
<tr>
<td><code>/promote-rule</code></td>
<td>Not migrated</td>
<td>Claude-specific instruction management</td>
</tr>
<tr>
<td><code>/cpm</code></td>
<td><code>/cpm</code> (new)</td>
<td>Commit + PR + Merge one-shot workflow</td>
</tr>
</tbody>
</table>
<p>The biggest behavioral difference is how context gets loaded. In Claude Code, the <code>/startup</code> command runs shell commands with <code>!</code> prefix and injects the output directly into the conversation context before Claude processes anything. The model sees the git status, open issues, and log files as pre-loaded data.</p>
<p>In Copilot, the prompt file is just instructions. It tells the agent "run <code>git status</code>", "run <code>gh pr list</code>", "run <code>gh issue list</code>". The agent then executes these as sequential steps, which means it takes longer and the model processes each result individually rather than seeing everything at once.</p>
<p>For most commands this difference doesn't matter. For <code>/startup</code>, which gathers context from five or six different sources, it's noticeably slower. But the end result is the same: the agent has a full picture of the project state.</p>
<hr>
<h2 id="vs-code-settings-what-actually-goes-in-settingsjson"><a class="heading-anchor" href="#vs-code-settings-what-actually-goes-in-settingsjson">VS Code settings: what actually goes in settings.json</a></h2>
<p>With the inline instruction arrays removed, <code>settings.json</code> becomes much cleaner. It handles the behavioral configuration that applies globally across all workspaces - things that aren't project-specific rules, but rather how VS Code and Copilot should behave.</p>
<p>Here's what survived the setup process:</p>
<p><strong>Copilot behavior settings:</strong></p>
<table>
<thead>
<tr>
<th scope="col">Setting</th>
<th scope="col">What It Does</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>chat.useClaudeMdFile: true</code></td>
<td>Copilot reads <code>CLAUDE.md</code> files in your repos. If you have repos with existing Claude Code configs, Copilot respects those instructions too.</td>
</tr>
<tr>
<td><code>chat.agent.enabled: true</code></td>
<td>Enables Agent Mode - the closest equivalent to Claude Code's default behavior. Without this, chat is ask-only.</td>
</tr>
<tr>
<td><code>github.copilot.nextEditSuggestions.enabled: true</code></td>
<td>After you make an edit, Copilot predicts where you'll edit next. Rename a parameter, and it suggests updating all usages.</td>
</tr>
</tbody>
</table>
<p><strong>Editor settings that affect the Copilot workflow:</strong></p>
<table>
<thead>
<tr>
<th scope="col">Setting</th>
<th scope="col">What It Does</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>editor.formatOnSave: true</code></td>
<td>Auto-formats on <code>Ctrl+S</code>. Prevents style inconsistencies without thinking about it.</td>
</tr>
<tr>
<td><code>editor.codeActionsOnSave</code></td>
<td>Auto-fixes ESLint errors and organizes imports on save. <code>"explicit"</code> means manual save only, not auto-save.</td>
</tr>
<tr>
<td><code>git.postCommitCommand: "none"</code></td>
<td>After committing, VS Code does nothing (no auto-push). Intentional for the issue-first workflow where you review before pushing.</td>
</tr>
</tbody>
</table>
<p><strong>Context exclusion settings:</strong></p>
<table>
<thead>
<tr>
<th scope="col">Setting</th>
<th scope="col">Why It Matters for Copilot</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>files.exclude</code> (node_modules, dist, build)</td>
<td>Hides generated directories from file explorer and <code>Ctrl+P</code>.</td>
</tr>
<tr>
<td><code>search.exclude</code> (same + lock files)</td>
<td>Excludes from <code>Ctrl+Shift+F</code> search. Also reduces noise when Copilot searches your codebase via <code>#codebase</code>.</td>
</tr>
</tbody>
</table>
<p>A few things I learned during setup:</p>
<ul>
<li><strong><code>chat.instructionsFilesLocations</code></strong> was supposed to point at <code>.github/instructions/</code>, but VS Code rejected the schema format. Turns out <code>.github/instructions/</code> is auto-scanned by default - the setting is unnecessary.</li>
<li><strong><code>editor.defaultFormatter: "esbenp.prettier-vscode"</code></strong> fails validation if the Prettier extension isn't installed. Comment it out until you install it, or install it first.</li>
<li><strong>Extension dependencies are real.</strong> Four extensions are needed for full functionality: GitHub Copilot, GitHub Copilot Chat, Prettier, and ESLint. The settings file should document these prerequisites (ours now does).</li>
</ul>
<p>The full settings reference with every setting explained is in the <a href="https://github.com/lucasmccomb/copilot-configs#settings-reference" target="_blank" rel="noopener noreferrer">copilot-configs README</a>.</p>
<hr>
<h2 id="conditional-instructions-something-claude-code-doesnt-have"><a class="heading-anchor" href="#conditional-instructions-something-claude-code-doesnt-have">Conditional instructions: something Claude Code doesn't have</a></h2>
<p>Copilot has a feature that Claude Code doesn't: <code>.instructions.md</code> files in <code>.github/instructions/</code> that apply conditionally based on file glob patterns.</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">---</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">name</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">'React TypeScript Standards'</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">applyTo</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">'**/*.{tsx,ts}'</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">---</span></span>
<span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E">## Component Patterns</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">- </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">Always use functional components with TypeScript interfaces</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">- </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">Never export components and non-components from the same file</span></span></code></pre>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">---</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">name</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">'SQL Migration Standards'</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">applyTo</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">'**/migrations/**/*.sql'</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">---</span></span>
<span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E">## Reserved Keywords</span></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">Always double-quote</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">position, order, user, offset, limit, key, value, type...</span></span></code></pre>
<p>This is genuinely useful. In Claude Code, the React conventions, the SQL migration rules, and the Go patterns all live in the same CLAUDE.md file. The model gets all of them regardless of what file it's working on. With Copilot's instruction files, the React rules only load when you're editing <code>.tsx</code> files, and the SQL rules only load when you're in a migration directory.</p>
<p>I created two instruction files to start: <code>react-typescript.instructions.md</code> for TypeScript/React conventions, and <code>sql-migrations.instructions.md</code> for Supabase migration patterns. Adding more is straightforward.</p>
<hr>
<h2 id="enforcement-hooks-python-hooks-to-husky"><a class="heading-anchor" href="#enforcement-hooks-python-hooks-to-husky">Enforcement hooks: Python hooks to Husky</a></h2>
<p>This is where the translation gets lossy.</p>
<p>Claude Code's enforcement hooks are Python scripts that intercept tool calls at the <code>PreToolUse</code> and <code>UserPromptSubmit</code> layers. They run before Claude executes any action, and they can block it, modify it, or inject context. Four hooks in my setup:</p>
<ul>
<li><code>enforce-git-workflow.py</code> - blocks commits on main, enforces issue-number prefixes in commit messages, blocks pushes to main</li>
<li><code>enforce-issue-workflow.py</code> - detects work-request prompts and injects a "create an issue first" reminder</li>
<li><code>auto-approve-bash.py</code> - reimplements the permission allow/deny list at the hook layer (workaround for a Claude Code bug)</li>
<li><code>auto-approve-file-ops.py</code> - same for file read/write/edit operations</li>
</ul>
<p>Copilot doesn't have a hook/interception system. There's no way to run code before Copilot executes a command, and there's no way to programmatically block actions based on content inspection.</p>
<p>The workaround is Husky git hooks, which operate at the git level rather than the AI level:</p>
<p><strong>Pre-commit</strong> (<code>.husky/pre-commit</code>):</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E">#!/bin/sh</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">BRANCH</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(</span><span style="color:#953800;--shiki-dark:#FFA657">git</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> branch</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --show-current</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">)</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">if</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> [ </span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$BRANCH</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> =</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "main"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> ]; </span><span style="color:#CF222E;--shiki-dark:#FF7B72">then</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">  echo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "ERROR: Cannot commit on main. Create a feature branch first."</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">  exit</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 1</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">fi</span></span></code></pre>
<p><strong>Commit-msg</strong> (<code>.husky/commit-msg</code>):</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E">#!/bin/sh</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">MSG</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(</span><span style="color:#953800;--shiki-dark:#FFA657">cat</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#0550AE;--shiki-dark:#79C0FF">$1</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">)</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">if</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> !</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> echo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$MSG</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> |</span><span style="color:#953800;--shiki-dark:#FFA657"> grep</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -qE</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "^[0-9]+:"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">; </span><span style="color:#CF222E;--shiki-dark:#FF7B72">then</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">  echo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "ERROR: Commit message must start with issue number."</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">  echo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "  Format: {issue_number}: {description}"</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">  exit</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 1</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">fi</span></span></code></pre>
<p>These cover the two most important enforcement rules (no commits on main, issue-number prefix in messages). But they miss the subtler ones. The <code>enforce-issue-workflow.py</code> hook that detects work-request verbs and reminds Claude to check for issues first? That has no Copilot equivalent. The permission deny list that blocks <code>rm</code> and <code>git reset --hard</code>? That's in the instructions as soft guidance, not hard enforcement.</p>
<p>The tradeoff is clear: Husky hooks enforce rules at the git boundary, where they catch mistakes regardless of whether a human or AI made them. Claude Code hooks enforce rules at the AI boundary, which catches mistakes earlier but only applies to AI-initiated actions.</p>
<hr>
<h2 id="session-logging-and-multi-agent-coordination"><a class="heading-anchor" href="#session-logging-and-multi-agent-coordination">Session logging and multi-agent coordination</a></h2>
<p>The session logging system and multi-agent coordination protocols are workflow documentation, not tool-specific features. They translate as reference docs that Copilot reads when instructed to.</p>
<p>In Claude Code, <code>log-system.md</code> and <code>multi-agent-system.md</code> live in <code>~/.claude/</code> and are referenced by the slash commands. The <code>/startup</code> command reads agent logs, checks cross-agent state, and creates new log entries. The logging triggers (after every commit, after PR creation, after merge) are enforced by instructions in CLAUDE.md.</p>
<p>In Copilot, these same documents live in <code>docs/</code> and are referenced by the prompt files. The <code>/startup</code> prompt instructs the agent to pull logs, read today's log, check other agents' logs, and create a new entry if needed. The logging triggers are documented in <code>copilot-instructions.md</code>.</p>
<p>The implementation is softer. Claude Code's hooks can't force the model to update logs, but the tight integration between commands and instructions makes it reliable in practice. With Copilot, it's entirely instruction-based, so compliance depends on the model following the prompt. In my experience, both tools follow logging instructions consistently once the prompts are well-written.</p>
<p>One adaptation for the Windows environment: all paths in the docs reference both Unix-style (<code>~/code/agent-logs/</code>) and Windows-style (<code>C:\code\agent-logs\</code>) conventions, since Copilot on Windows might use either depending on whether it's running in PowerShell or Git Bash.</p>
<hr>
<h2 id="the-setupsh-approach"><a class="heading-anchor" href="#the-setupsh-approach">The setup.sh approach</a></h2>
<p>The original Claude Code setup uses a <code>setup.sh</code> script that symlinks config files and generates <code>settings.json</code> with path expansion. The copilot-configs repo takes a different approach: placeholder replacement.</p>
<p>All config files use <code>{{PLACEHOLDER}}</code> tokens for values that vary per user:</p>
<ul>
<li><code>{{GITHUB_USERNAME}}</code> - your GitHub handle</li>
<li><code>{{FULL_NAME}}</code> - your name for attribution</li>
<li><code>{{LOG_REPO_NAME}}</code> - the name of your agent logs repo</li>
</ul>
<p>The setup script prompts for these values (auto-detecting from <code>gh</code> CLI and <code>git config</code> where possible), then does a <code>find</code>/<code>sed</code> across all <code>.md</code> and <code>.json</code> files to replace every placeholder. It can also create the agent logs repo on GitHub.</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">git</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> clone</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> https://github.com/lucasmccomb/copilot-configs.git</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">cd</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> copilot-configs</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">./setup.sh</span></span></code></pre>
<p>Or non-interactively:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">./setup.sh</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --github-user</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> octocat</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --full-name</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "Octo Cat"</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --log-repo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> my-agent-logs</span></span></code></pre>
<p>This makes the configs genuinely portable. Someone can fork the repo, run setup, and have a complete Copilot workflow configured in under a minute. The placeholder approach is simpler than the symlink/template system I use for Claude Code, but it works well for a repo that's meant to be forked and customized rather than centrally managed.</p>
<hr>
<h2 id="what-didnt-translate"><a class="heading-anchor" href="#what-didnt-translate">What didn't translate</a></h2>
<p>Some things are Claude Code-specific and have no meaningful Copilot equivalent:</p>
<table>
<thead>
<tr>
<th scope="col">Feature</th>
<th scope="col">Why it didn't translate</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>hooks/*.py</code> (tool-call interception)</td>
<td>Copilot has no pre-execution hook system</td>
</tr>
<tr>
<td><code>settings.json</code> permission deny list</td>
<td>No way to block specific commands in Copilot</td>
</tr>
<tr>
<td>MCP servers</td>
<td>Copilot has <code>.vscode/mcp.json</code> support, but the server ecosystem is different</td>
</tr>
<tr>
<td><code>/dotsync</code> command</td>
<td>Specific to the Claude dotfiles reverse-sync workflow</td>
</tr>
<tr>
<td><code>/promote-rule</code> command</td>
<td>Specific to the CLAUDE.md instruction hierarchy</td>
</tr>
<tr>
<td>Plugin ecosystem</td>
<td>Copilot extensions exist but operate differently than Claude Code plugins</td>
</tr>
<tr>
<td>Inline context pre-loading (<code>!</code> syntax)</td>
<td>Copilot prompts instruct; they don't pre-execute</td>
</tr>
</tbody>
</table>
<p>The permission system is the most significant loss. Claude Code's deny list blocks destructive operations (<code>rm</code>, <code>git reset --hard</code>, <code>DROP TABLE</code>) at the tool level. In Copilot, these are documented as "don't do this" in the instructions. The model generally respects them, but there's no hard gate.</p>
<hr>
<h2 id="key-differences-in-practice"><a class="heading-anchor" href="#key-differences-in-practice">Key differences in practice</a></h2>
<p>After using both systems side by side, a few practical differences stand out:</p>
<p><strong>Context loading is explicit in Copilot.</strong> Claude Code automatically reads referenced files, loads the CLAUDE.md hierarchy, and ingests MCP server data. Copilot requires you to explicitly reference context with <code>#file</code>, <code>#codebase</code>, or <code>#changes</code>. This means you need to be more intentional about what the model sees, which is both a feature (less noise) and a limitation (more manual work).</p>
<p><strong>Agent mode is opt-in.</strong> In Claude Code, the model always has access to tools. In Copilot, you need to open Agent mode specifically (<code>Ctrl+Shift+I</code> instead of <code>Ctrl+I</code>) and the <code>/</code> commands only appear there. Default chat is ask-only with no tool access.</p>
<p><strong>Model switching is per-chat.</strong> Claude Code switches models with a <code>--model</code> flag. Copilot uses <code>Ctrl+Alt+.</code> to cycle through available models within a chat. Different models are better at different tasks, and Copilot makes it easy to switch mid-conversation.</p>
<p><strong>The instruction hierarchy is different.</strong> Claude Code loads CLAUDE.md files from root to working directory, creating a three-tier inheritance (global, workspace, project). Copilot has user settings (global), <code>copilot-instructions.md</code> (per-repo), and <code>.instructions.md</code> files (per-file-type). The layering is flatter but more granular at the file level.</p>
<hr>
<h2 id="the-result"><a class="heading-anchor" href="#the-result">The result</a></h2>
<p>The <a href="https://github.com/lucasmccomb/copilot-configs" target="_blank" rel="noopener noreferrer">copilot-configs</a> repo is public. It contains:</p>
<ul>
<li><code>.github/copilot-instructions.md</code> - global rules translated from CLAUDE.md</li>
<li><code>.github/prompts/</code> - 9 prompt files (commit, pr, gs, startup, sync, new-issue, walkthrough, cpm, audit)</li>
<li><code>.github/instructions/</code> - 2 conditional instruction files (React/TypeScript, SQL migrations)</li>
<li><code>.vscode/settings.json</code> - user-level Copilot behavior settings (with a full reference guide in the README)</li>
<li><code>.copilotignore</code> - context exclusion rules</li>
<li><code>docs/</code> - log system, multi-agent system, and GitHub repo protocols</li>
<li><code>setup.sh</code> - one-command personalization</li>
</ul>
<p>The workflow I've built over months with Claude Code now has a Copilot equivalent that captures maybe 80% of the functionality. The missing 20% is mostly the enforcement layer (hooks, permissions, tool-call interception) and the deeper MCP integration. For day-to-day development work, the Copilot version covers what matters: consistent commit conventions, session continuity, quality gates, and a standardized workflow that any new project can adopt immediately.</p>
<p>The translation process itself was instructive. Several assumptions I made from reading Copilot documentation didn't survive contact with reality - deprecated settings, schema mismatches, extension dependencies. The best way to validate any AI tool configuration is to actually set it up and watch what VS Code complains about. Documentation lags behind the tool.</p>
<p>If you're using Claude Code and need to work with Copilot (or vice versa), the concepts transfer. The specific files and syntax are different, but the underlying ideas (global instructions, scoped commands, enforcement at boundaries, session logging for continuity) are tool-agnostic. Build the workflow first, then implement it in whatever tool your environment requires.</p>]]></content:encoded>
            <author>Lucas McComb</author>
        </item>
        <item>
            <title><![CDATA[Getting up and running with the Claude desktop app]]></title>
            <link>https://lem.fyi/blog/claude-setup-guide/</link>
            <guid isPermaLink="false">https://lem.fyi/blog/claude-setup-guide/</guid>
            <pubDate>Sat, 28 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A step-by-step guide to downloading the Claude desktop app on macOS, signing in with Google, and getting started with AI-powered research.]]></description>
            <content:encoded><![CDATA[<figure class="post-figure img-md post-video">
  <video autoplay loop muted playsinline>
    <source src="/videos/claude-setup-demo.mp4" type="video/mp4" />
  </video>
</figure>
<h2 id="introduction"><a class="heading-anchor" href="#introduction">Introduction</a></h2>
<p>I use <a href="https://claude.com/product/overview" target="_blank" rel="noopener noreferrer">Claude</a> for 99% of my research and general questions these days and it's become incredibly useful to me. This is a guide that covers:</p>
<ul>
<li>how to install the Claude desktop app on an Apple computer</li>
<li>how to create a Claude account using your existing Google account</li>
<li>how to enter a prompt</li>
<li>tips on how to write prompts</li>
<li>tips on how to use the Claude desktop app</li>
</ul>
<h3 id="prerequisites"><a class="heading-anchor" href="#prerequisites">Prerequisites</a></h3>
<p>This guide requires you to have:</p>
<ul>
<li>an Apple computer running macOS (ideally the latest version)</li>
<li>a Google account (ideally you are logged in to said Google account in your browser)</li>
<li>a web browser (ideally Google Chrome)</li>
<li>10-20 minutes of available time (depending on how tech savvy you are)</li>
</ul>
<div class="warning-callout">
  <div class="warning-callout-header"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg> Disclaimer</div>
<p>While Claude is an incredibly useful tool, there are some important things to keep in mind:</p>
<ul>
<li><strong>Never share sensitive personal information</strong> such as Social Security numbers, bank account numbers, credit card numbers, passwords, or medical record numbers. Claude does not need this information to help you and sharing it creates unnecessary risk.</li>
<li><strong>Be cautious with private details.</strong> Avoid sharing your home address, phone number, or other identifying information unless absolutely necessary for the task at hand.</li>
<li><strong>Claude can be wrong.</strong> AI models are not perfect. They can produce inaccurate, outdated, or misleading information. Always verify important facts, especially for medical, legal, or financial decisions, with qualified professionals and trusted sources.</li>
<li><strong>Claude is not a substitute for professional advice.</strong> Do not rely on Claude for medical diagnoses, legal counsel, financial planning, or other areas where professional expertise is required.</li>
<li><strong>Be mindful of what you upload.</strong> If you share files or images with Claude, make sure they don't contain sensitive information you wouldn't want processed by a third-party service.</li>
<li><strong>Review Anthropic's privacy policy.</strong> To understand how your data is handled, review <a href="https://www.anthropic.com/privacy" target="_blank" rel="noopener noreferrer">Anthropic's privacy policy</a> and <a href="https://www.anthropic.com/terms" target="_blank" rel="noopener noreferrer">terms of service</a>.</li>
</ul>
</div>
<hr>
<h2 id="instructions"><a class="heading-anchor" href="#instructions">Instructions</a></h2>
<h3 id="step-1-download-the-desktop-app-from-claudeai"><a class="heading-anchor" href="#step-1-download-the-desktop-app-from-claudeai">Step 1: Download the Desktop app from claude.ai</a></h3>
<p>Go to <a href="https://claude.ai/login" target="_blank" rel="noopener noreferrer">claude.ai/login</a>. You can either download the Desktop app or continue on web. I recommend downloading the Desktop app.</p>
<p><strong>Click the "Download desktop app" button</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/01-login.png" alt="Claude login page" class="img-md"><figcaption>Claude login page</figcaption></figure>
<h3 id="step-2-open-the-installer"><a class="heading-anchor" href="#step-2-open-the-installer">Step 2: Open the installer</a></h3>
<p>You're then going to install the Desktop app like you would any other app. The Claude.dmg install program will likely be saved in your Downloads folder or on your Desktop. It might even appear in the upper right of Chrome where you can double click it to open it.</p>
<p><strong>Find where the downloaded <code>Claude.dmg</code> file was saved and double-click it to open it</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/02-dmg-download.png" alt="Claude DMG in Downloads" class="img-md"><figcaption>Claude DMG in Downloads</figcaption></figure>
<h3 id="step-3-drag-claude-to-applications"><a class="heading-anchor" href="#step-3-drag-claude-to-applications">Step 3: Drag Claude to Applications</a></h3>
<p>Once you find the Claude.dmg installer program, double click on it and you should see a Finder window pop up that looks like this:</p>
<figure class="post-figure"><img src="/images/claude-setup/03-installer-window.png" alt="Claude installer Finder window" class="img-md"><figcaption>Claude installer Finder window</figcaption></figure>
<p><strong>Click and drag the Claude program into your Applications folder</strong>. It will take 30-60 seconds to add it to your Applications. You can then close that window.</p>
<h3 id="step-4-eject-claude-installer-optional-but-recommended"><a class="heading-anchor" href="#step-4-eject-claude-installer-optional-but-recommended">Step 4: Eject Claude installer (optional but recommended)</a></h3>
<p>Eject the installer application from your Desktop by right clicking on it with your mouse (or two finger tap/click with trackpad or <code>"control"</code> + click) and selecting "Eject Claude" or by dragging it to the Trash in the Dock.</p>
<figure class="post-figure"><img src="/images/claude-setup/04-eject-installer.png" alt="Eject Claude installer" class="img-md"><figcaption>Eject Claude installer</figcaption></figure>
<h3 id="step-5-open-claude-from-applications"><a class="heading-anchor" href="#step-5-open-claude-from-applications">Step 5: Open Claude from Applications</a></h3>
<p>You now have Claude installed as an application on your computer. Go into Applications using a Finder window, scroll to Claude and double click on it to open it.</p>
<figure class="post-figure"><img src="/images/claude-setup/05-applications.png" alt="Claude in Applications folder" class="img-lg"><figcaption>Claude in Applications folder</figcaption></figure>
<div class="tip-callout">
  <div class="tip-callout-header"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg> Pro tip</div>
  <p>You can also use macOS Spotlight to open apps (and <a href="https://support.apple.com/guide/mac-help/search-with-spotlight-mchlp1008/mac">much more</a>). To search with Spotlight, press <code>Command + Space</code> which will open a Spotlight search input. Type in "Claude" and then press enter once it shows up.</p>
</div>
<h3 id="step-6-allow-claude-to-open"><a class="heading-anchor" href="#step-6-allow-claude-to-open">Step 6: Allow Claude to open</a></h3>
<p>You will likely see a popup like the one below. <strong>This is safe</strong>. This program will not have control of your computer. The only thing Claude will have access to is the information you put into it unless you manually give it access to other applications.</p>
<p><strong>Click "Open"</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/06-open-confirmation.png" alt="macOS security popup" class="img-sm"><figcaption>macOS security popup</figcaption></figure>
<h3 id="step-7-get-started"><a class="heading-anchor" href="#step-7-get-started">Step 7: Get Started</a></h3>
<p>You're then going to see the Get Started window.</p>
<p><strong>Click "Get Started"</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/07-get-started.png" alt="Get Started screen" class="img-md"><figcaption>Get Started screen</figcaption></figure>
<h3 id="step-8-sign-in-with-google"><a class="heading-anchor" href="#step-8-sign-in-with-google">Step 8: Sign in with Google</a></h3>
<p>From there you're going to be prompted to sign in. I recommend using Google. Google sign-in is one of the most secure authentication methods and prevents having to create new passwords.</p>
<p><strong>Click "Continue with Google"</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/08-sign-in.png" alt="Sign in screen" class="img-md"><figcaption>Sign in screen</figcaption></figure>
<h3 id="step-9-continue-with-google-in-chrome"><a class="heading-anchor" href="#step-9-continue-with-google-in-chrome">Step 9: Continue with Google in Chrome</a></h3>
<p>A new Chrome window will open that looks like the one below.</p>
<p><strong>Click "Continue with Google"</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/09-chrome-google-signin.png" alt="Chrome Google sign in" class="img-md"><figcaption>Chrome Google sign in</figcaption></figure>
<h3 id="step-10-select-your-google-account"><a class="heading-anchor" href="#step-10-select-your-google-account">Step 10: Select your Google account</a></h3>
<p>Another Google Authentication window will appear that looks like the one below.</p>
<p><strong>Select the account you want to sign in with</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/10-select-account.png" alt="Google account selection" class="img-md"><figcaption>Google account selection</figcaption></figure>
<h3 id="step-11-authorize-claude"><a class="heading-anchor" href="#step-11-authorize-claude">Step 11: Authorize Claude</a></h3>
<p>You'll then see a "Sign in to Claude" window appear. <strong>Again, this is safe.</strong> You're only giving Claude access to your basic Google profile information, not your emails, contacts, or any of your private data. This is simply using your Google account to authenticate for login and it is safe.</p>
<p><strong>Click "Continue"</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/11-authorize.png" alt="Claude authorization screen" class="img-md"><figcaption>Claude authorization screen</figcaption></figure>
<h3 id="step-12-accept-the-terms"><a class="heading-anchor" href="#step-12-accept-the-terms">Step 12: Accept the terms</a></h3>
<p>You'll then be returned to the Claude desktop app interface where you'll see the following screen.</p>
<p><strong>Check the "I agree..." checkbox and click "Continue"</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/12-terms.png" alt="Terms of service screen" class="img-md"><figcaption>Terms of service screen</figcaption></figure>
<h3 id="step-13-choose-a-plan"><a class="heading-anchor" href="#step-13-choose-a-plan">Step 13: Choose a plan</a></h3>
<p>You'll then see the "Choose a plan" screen appear. I recommend starting with a Free plan and if you end up wanting more features you can always upgrade.</p>
<p><strong>Click "Use Claude for free"</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/13-choose-plan.png" alt="Choose a plan screen" class="img-md"><figcaption>Choose a plan screen</figcaption></figure>
<h3 id="step-14-welcome-screen"><a class="heading-anchor" href="#step-14-welcome-screen">Step 14: Welcome screen</a></h3>
<p>You'll then see a welcome screen explaining a few things.</p>
<p><strong>Toggle "Help Improve Claude" off at the bottom</strong>
<strong>Click "I understand"</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/14-welcome.png" alt="Welcome screen" class="img-md"><figcaption>Welcome screen</figcaption></figure>
<figure class="post-figure"><img src="/images/claude-setup/14b-toggle-off.png" alt="Help Improve Claude toggle in off state" class="img-md"><figcaption>Help Improve Claude toggle in off state</figcaption></figure>
<h3 id="step-15-enter-your-name"><a class="heading-anchor" href="#step-15-enter-your-name">Step 15: Enter your name</a></h3>
<p>Enter your name in the following screen.</p>
<figure class="post-figure"><img src="/images/claude-setup/15-enter-name.png" alt="Enter your name screen" class="img-md"><figcaption>Enter your name screen</figcaption></figure>
<h3 id="step-16-select-your-interests"><a class="heading-anchor" href="#step-16-select-your-interests">Step 16: Select your interests</a></h3>
<p>On the "What are you into?" screen select which options that you think fit your needs most. For you, I would recommend: Life stuff and Design &#x26; creativity.</p>
<p><strong>Once you have the options selected, click "Let's go"</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/16-interests.png" alt="Interest selection screen" class="img-md"><figcaption>Interest selection screen</figcaption></figure>
<h3 id="step-17-start-your-first-conversation"><a class="heading-anchor" href="#step-17-start-your-first-conversation">Step 17: Start your first conversation</a></h3>
<p>You'll then see a "Where should we start?" screen.</p>
<p><strong>Click "I have my own topic" on the bottom right</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/17-start-topic.png" alt="Where should we start screen" class="img-md"><figcaption>Where should we start screen</figcaption></figure>
<h3 id="step-18-select-quick-chat-options"><a class="heading-anchor" href="#step-18-select-quick-chat-options">Step 18: Select Quick Chat options</a></h3>
<p>I would keep the two default options in this screen selected. I use the menu bar shortcut often and am working on getting used to the double-option shortcut.</p>
<p><strong>Select the options you want to use and click "Continue"</strong></p>
<figure class="post-figure"><img src="/images/claude-setup/18-quick-chat.png" alt="Quick Chat options screen" class="img-md"><figcaption>Quick Chat options screen</figcaption></figure>
<h3 id="step-19-start-your-first-chat-with-claude"><a class="heading-anchor" href="#step-19-start-your-first-chat-with-claude">Step 19: Start your first chat with Claude!</a></h3>
<p>We've reached the finish line for getting up and running with Claude as a desktop app. Ask Claude about whatever is on your mind. I use Claude for all of my questions, research, guidance, advice and even just processing my thoughts.</p>
<figure class="post-figure"><img src="/images/claude-setup/19-chat-home.png" alt="Claude chat home screen" class="img-md"><figcaption>Claude chat home screen</figcaption></figure>
<p>If you're not sure the best ways to use Claude, ask Claude. AI agents like Claude are the most powerful tools that humans have created for the purpose of acquiring general knowledge. It has access to almost all of documented human knowledge and it can explain things for any level of understanding. Ask it anything!</p>
<p>Here's an example use case that might be helpful. A family member of mine recently messaged me with the following question:</p>
<blockquote>
<p>I have a bunch of family photos that I want to digitize. They're all in different print sizes up to 11x14. I'm thinking about ordering a scanner but not sure which one to get. I also don't know what the best way to digitally store the scanned photos would be. Would you be able to recommend a good scanner that can do batch scanning for lots of photos and what the best storage option would be? I have an iCloud account that I backup my iPhone photos to and have also used Google Photos. Batch upload would be helpful. I also want to be able to create photo books with the digitized images.</p>
</blockquote>
<p>Here's exactly what I would do to answer that question:</p>
<ol>
<li>Open Claude</li>
<li>Copy the text of that message and paste it into the Claude prompt</li>
<li>Add a couple notes to the end of the prompt that I think would be helpful:
<ul>
<li>"Give me a high level overview of how I should think about configuring this process. There has to be a lot of documentation available online explaining how people have done this. What are the major things that I want to pay attention to? What are the scanner specifications that I should be concerned with? What are the image storage options that I should be concerned with? Explain everything thoroughly so that someone without much technological know-how can understand."</li>
<li>"The scanner and storage option should both be relatively easy to use for someone who is not necessarily technologically savvy. Both processes should not be overly complex."</li>
<li>"I want the scans of the images to be high quality but I also want to be able to do batch scanning and not have to place every image into a scanner individually."</li>
<li>"I don't want to spend more than $1000 on the scanner and more than $20/month for image storage. Give me a range of options within these price points and the pros and cons of each."</li>
<li>"Again, make sure to explain things thoroughly and do in-depth research to find the best options and systems that make the most sense."</li>
<li>"I will be using a MacBook Pro laptop with an external monitor as my main machine for this process."</li>
</ul>
</li>
</ol>
<p>When I'm writing prompts into AI agents I try to write as clearly as possible and identify key areas that I want the agent to research and think about. I ask the agent to explain things to me in plain terms. If I don't know anything about something that I'm researching I start from that point of view and ask the agent to explain things to me from a high level, break down the main concepts so that I can understand them and use that foundation to guide my thinking and the path of the conversation. If you don't understand something that Claude told you, ask Claude to explain it in simpler terms ("Explain this to me like I'm 10 years old"). Curiosity is your friend here. Claude will not judge you for not knowing something. Claude's purpose is to help you figure anything out. Use it!</p>
<p>Once you've finished entering the prompt into the text area, hit the return key or press the arrow button on the right side of the text area. Claude will begin researching.</p>
<figure class="post-figure"><img src="/images/claude-setup/19b-research-example.png" alt="Claude researching scanner options" class="img-md"><figcaption>Claude researching scanner options</figcaption></figure>
<h3 id="step-20-utilizing-claudes-output"><a class="heading-anchor" href="#step-20-utilizing-claudes-output">Step 20: Utilizing Claude's output</a></h3>
<p>After entering that prompt regarding photo print digitizing, Claude spent about 3 minutes researching options for scanners and photo storage and then created a <code>.docx</code> document for me with all of the information.</p>
<figure class="post-figure"><img src="/images/claude-setup/20-docx-output.png" alt="Claude generated a Family Photo Digitization Guide document" class="img-md"><figcaption>Claude generated a Family Photo Digitization Guide document</figcaption></figure>
<p>I can now download that file or add it to my Google Drive to share it using the buttons in the upper right corner of the window.</p>
<figure class="post-figure"><img src="/images/claude-setup/20b-download-buttons.png" alt="Download and Google Drive buttons" class="img-md"><figcaption>Download and Google Drive buttons</figcaption></figure>
<p>Here's the document that Claude created for me: <a href="/docs/Family_Photo_Digitization_Guide.docx" download>Family Photo Digitization Guide <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:middle;margin-left:4px"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></a></p>
<p>Let's say that I review the document and want Claude to explain something more or I want to clarify a feature that I want the scanner to have or I realized a constraint with the scanning process that I forgot to mention in the original prompt. I would simply type into the prompt:</p>
<blockquote>
<p>I realized I forgot to mention X, does that change your recommendation at all?</p>
</blockquote>
<hr>
<div class="tip-callout">
  <h2 id="tips" class="tip-callout-header"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg> Tips</h2>
<ul>
<li>Use "Return + Shift" to add a line break in the prompt area without sending the prompt.</li>
<li>If you accidentally sent a prompt and want to stop Claude from answering it, hit the stop button in the bottom right of the prompt area or hit the "Esc" key.</li>
</ul>
<figure class="post-figure"><img src="/images/claude-setup/20-stop-button.png" alt="Stop Claude response button" class="img-md"><figcaption>Stop Claude response button</figcaption></figure>
<ul>
<li>Click the "Open sidebar" button at the top left of the screen to view your recent chats, create a new chat or use other features of Claude.</li>
</ul>
<figure class="post-figure"><img src="/images/claude-setup/20-sidebar-button.png" alt="Open sidebar button" class="img-md"><figcaption>Open sidebar button</figcaption></figure>
<ul>
<li>Quickly create a new chat by pressing "Command + Shift + O".</li>
<li>If you upgrade to a paid plan, you can use better models for more complex tasks. Unless you're asking Claude to process complex data or research a very complex process of some kind, the latest model that's provided with the Free plan should be sufficient for your needs. If you find that it's not performing to your desired ability I would try upgrading to see if that improves Claude's responses.</li>
</ul>
<figure class="post-figure"><img src="/images/claude-setup/tip-models.png" alt="Claude model selection showing available models" class="img-md"><figcaption>Claude model selection showing available models</figcaption></figure>
<ul>
<li>Claude is multi-modal, meaning that it can consume and analyze images and files in addition to plain text. Use the "+" button on the bottom left of the prompt input to upload images or files in order to provide additional context with your prompt. Claude can summarize documents and analyze screenshots. <a href="https://support.apple.com/en-us/102646">Here's a guide for how to take screenshots</a>.</li>
</ul>
<div class="image-row">
  <img src="/images/claude-setup/tip-add-button.png" alt="The + button to add files" />
  <img src="/images/claude-setup/tip-add-menu.png" alt="Add files menu showing options" />
  <img src="/images/claude-setup/tip-image-attached.png" alt="Screenshot attached to prompt" />
</div>
<ul>
<li>Ask Claude to save research to files so you can share the files with people and for yourself to easily reference later on. Save them to a "Claude docs" directory somewhere on your computer where you can easily find them.</li>
</ul>
</div>
<h2 id="conclusion"><a class="heading-anchor" href="#conclusion">Conclusion</a></h2>
<p>You now have all recorded human knowledge just a prompt away. For more information on how to use Claude, I recommend checking out this course created by Anthropic, the creators of Claude: <a href="https://anthropic.skilljar.com/claude-101" target="_blank" rel="noopener noreferrer">Claude 101</a>. Claude is an incredibly powerful tool and is an excellent introduction to using AI for everyday problems.</p>
<p><strong>Use it!</strong></p>
<h3 id="questions-comments-concerns"><a class="heading-anchor" href="#questions-comments-concerns">Questions, comments, concerns?</a></h3>
<p>Reach out to me here: <a href="mailto:luke@lem.fyi">luke@lem.fyi</a></p>
<p>Thanks for reading and hopefully this is helpful!</p>]]></content:encoded>
            <author>Lucas McComb</author>
        </item>
        <item>
            <title><![CDATA[Multiple GitHub Accounts with direnv]]></title>
            <link>https://lem.fyi/blog/multiple-github-accounts-with-direnv/</link>
            <guid isPermaLink="false">https://lem.fyi/blog/multiple-github-accounts-with-direnv/</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How I set up seamless per-directory GitHub account switching using direnv, SSH host aliases, and the gh CLI — no conditional git config needed.]]></description>
            <content:encoded><![CDATA[<p>If you use more than one GitHub account — say, a personal account and a work account — you've probably hit the friction of switching between them. Wrong SSH key. Wrong <code>gh</code> auth token. A commit pushed to the wrong account. The usual advice involves git's <code>includeIf</code> conditional config or manually running <code>gh auth switch</code> every time you change context.</p>
<p>I wanted something that just works: <code>cd</code> into a directory and have the right GitHub identity loaded automatically. Here's how I set that up with direnv.</p>
<hr>
<h2 id="the-problem"><a class="heading-anchor" href="#the-problem">The Problem</a></h2>
<p>I have two GitHub accounts: <code>lucasmccomb</code> (personal) and a work account. Each has its own SSH key, its own <code>gh</code> auth token, and its own set of repos. The challenge is making sure that when I'm working in <code>~/code/work/</code>, everything - git pushes, <code>gh</code> CLI commands, API calls - routes through the correct account without me thinking about it.</p>
<p>Git's built-in <code>includeIf</code> can handle some of this, but it only covers git config values (name, email, signing key). It doesn't touch the <code>gh</code> CLI, which uses its own auth system. I needed a solution that sets the entire shell environment per directory.</p>
<hr>
<h2 id="the-pieces"><a class="heading-anchor" href="#the-pieces">The Pieces</a></h2>
<p>The setup has three layers:</p>
<ol>
<li><strong>SSH host aliases</strong> — route <code>git push</code> to the correct SSH key</li>
<li><strong>direnv <code>.envrc</code> files</strong> — set <code>GH_TOKEN</code> and other env vars per directory</li>
<li><strong><code>gh auth</code> with multiple accounts</strong> — provide the tokens that direnv exports</li>
</ol>
<h3 id="ssh-config"><a class="heading-anchor" href="#ssh-config">SSH Config</a></h3>
<p>SSH doesn't natively support "use this key for this directory." But it does support host aliases. By defining a custom hostname that maps to <code>github.com</code> with a specific key, you can control which identity git uses based on the remote URL.</p>
<pre><code># ~/.ssh/config

# Personal (lucasmccomb)
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519

# Work
Host github-work
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_work
  IdentitiesOnly yes
</code></pre>
<p>The <code>IdentitiesOnly yes</code> line is important — it tells SSH to only offer the specified key, not every key in the agent. Without it, SSH might try your personal key first and succeed (or fail confusingly) before reaching the work key.</p>
<p>With this config, a repo cloned as <code>git@github-work:org/repo.git</code> will always use the work key, while <code>git@github.com:lucasmccomb/repo.git</code> uses the personal key. The hostname in the remote URL is what determines the identity.</p>
<h3 id="direnv"><a class="heading-anchor" href="#direnv">direnv</a></h3>
<p><a href="https://direnv.net/" target="_blank" rel="noopener noreferrer">direnv</a> is a shell extension that loads and unloads environment variables based on <code>.envrc</code> files as you navigate directories. It's the glue that makes all of this automatic.</p>
<p>At the workspace root (<code>~/code/</code>), I have an <code>.envrc</code> that sets the default (personal) account:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E"># ~/code/.envrc</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">export</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> GH_TOKEN</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"$(</span><span style="color:#953800;--shiki-dark:#FFA657">gh</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> auth token </span><span style="color:#0550AE;--shiki-dark:#79C0FF">--user</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> lucasmccomb)"</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">export</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> GH_ACCOUNT</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"lucasmccomb"</span></span></code></pre>
<p>In the work directory, a second <code>.envrc</code> overrides it:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E"># ~/code/work/.envrc</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">export</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> GH_TOKEN</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"$(</span><span style="color:#953800;--shiki-dark:#FFA657">gh</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> auth token </span><span style="color:#0550AE;--shiki-dark:#79C0FF">--user</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> work-username)"</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">export</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> GH_ACCOUNT</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"work-username"</span></span></code></pre>
<p>direnv inherits from parent directories, so subdirectories of <code>~/code/work/</code> get the work token, while everything else under <code>~/code/</code> gets the personal token.</p>
<p>The <code>GH_TOKEN</code> environment variable is what the <code>gh</code> CLI and GitHub API clients check first — before any stored auth. By exporting it, every <code>gh pr create</code>, <code>gh issue list</code>, or API call in that shell session uses the correct account.</p>
<p><code>GH_ACCOUNT</code> is a custom variable I set for my own tooling. Some of my scripts and hooks reference it to know which account context they're running in.</p>
<h3 id="gh-cli-multi-account-auth"><a class="heading-anchor" href="#gh-cli-multi-account-auth">gh CLI Multi-Account Auth</a></h3>
<p>The <code>gh</code> CLI supports multiple authenticated accounts. You register each one:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">gh</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> auth</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> login</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --hostname</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> github.com</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --user</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> lucasmccomb</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">gh</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> auth</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> login</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --hostname</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> github.com</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --user</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> work-username</span></span></code></pre>
<p>Once both are registered, <code>gh auth token --user &#x3C;account></code> returns the token for that specific account - which is exactly what the <code>.envrc</code> files call.</p>
<hr>
<h2 id="how-it-comes-together"><a class="heading-anchor" href="#how-it-comes-together">How It Comes Together</a></h2>
<p>Here's what happens when I open a terminal and navigate:</p>
<pre><code>~/ $ cd code/work/my-project
direnv: loading ~/code/work/.envrc
direnv: export +GH_ACCOUNT +GH_TOKEN

~/code/work/my-project $ gh api user --jq .login
work-username

~/code/work/my-project $ cd ~/code/lem-fyi-repos/lem-fyi-0
direnv: loading ~/code/.envrc
direnv: export ~GH_ACCOUNT ~GH_TOKEN

~/code/lem-fyi-repos/lem-fyi-0 $ gh api user --jq .login
lucasmccomb
</code></pre>
<p>No manual switching. No remembering which account is active. The directory determines the identity.</p>
<p>For git operations, the SSH host alias in the remote URL handles key selection independently. So <code>git push</code> in a work repo uses the work SSH key, and <code>gh pr create</code> in the same repo uses the work token - both automatically.</p>
<hr>
<h2 id="setup-steps"><a class="heading-anchor" href="#setup-steps">Setup Steps</a></h2>
<p>If you want to replicate this:</p>
<p><strong>1. Generate a separate SSH key for each account:</strong></p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">ssh-keygen</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -t</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ed25519</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -C</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "personal@example.com"</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -f</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ~/.ssh/id_ed25519</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">ssh-keygen</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -t</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ed25519</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -C</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "work@example.com"</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -f</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ~/.ssh/id_ed25519_work</span></span></code></pre>
<p><strong>2. Add each public key to the corresponding GitHub account</strong> (Settings > SSH Keys).</p>
<p><strong>3. Configure SSH host aliases</strong> in <code>~/.ssh/config</code> (as shown above).</p>
<p><strong>4. Authenticate both accounts with <code>gh</code>:</strong></p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">gh</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> auth</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> login</span><span style="color:#6E7781;--shiki-dark:#8B949E">  # First account</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">gh</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> auth</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> login</span><span style="color:#6E7781;--shiki-dark:#8B949E">  # Second account (select "Login with a different account")</span></span></code></pre>
<p><strong>5. Install direnv</strong> and add the shell hook:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">brew</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> install</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> direnv</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E"># Add to ~/.zshrc (or ~/.bashrc):</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">eval</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "$(</span><span style="color:#953800;--shiki-dark:#FFA657">direnv</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> hook zsh)"</span></span></code></pre>
<p><strong>6. Create <code>.envrc</code> files</strong> at the appropriate directory levels:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E"># Default account at workspace root</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">echo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> 'export GH_TOKEN="$(gh auth token --user personal-username)"'</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> ></span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ~/code/.envrc</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">echo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> 'export GH_ACCOUNT="personal-username"'</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> >></span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ~/code/.envrc</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6E7781;--shiki-dark:#8B949E"># Override for work directory</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">echo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> 'export GH_TOKEN="$(gh auth token --user work-username)"'</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> ></span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ~/code/work/.envrc</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">echo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> 'export GH_ACCOUNT="work-username"'</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> >></span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ~/code/work/.envrc</span></span></code></pre>
<p><strong>7. Allow the <code>.envrc</code> files:</strong></p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">direnv</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> allow</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ~/code/.envrc</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">direnv</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> allow</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ~/code/work/.envrc</span></span></code></pre>
<p><strong>8. Clone work repos using the SSH alias:</strong></p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">git</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> clone</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> git@github-work:org/repo.git</span></span></code></pre>
<hr>
<h2 id="why-not-includeif"><a class="heading-anchor" href="#why-not-includeif">Why Not <code>includeIf</code>?</a></h2>
<p>Git's conditional includes (<code>includeIf</code>) are the commonly recommended approach. You add something like this to <code>~/.gitconfig</code>:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">[includeIf "gitdir:~/code/work/"]</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">    path</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> = ~/.gitconfig-work</span></span></code></pre>
<p>This works well for git-specific config (user.name, user.email, signing key). But it has limitations:</p>
<ul>
<li>It only affects git config values — not shell environment variables</li>
<li>The <code>gh</code> CLI ignores it entirely</li>
<li>GitHub API clients, CI scripts, and other tooling that read <code>GH_TOKEN</code> aren't covered</li>
<li>It doesn't compose well when you need the same directory to affect multiple tools</li>
</ul>
<p>direnv solves this at a lower level. By setting environment variables, it works with any tool that reads from the environment — which is most of them.</p>
<p>That said, you can combine both approaches. Use direnv for <code>GH_TOKEN</code> and <code>includeIf</code> for git author identity if you want per-directory commit emails without setting them per-repo.</p>
<hr>
<h2 id="gotchas"><a class="heading-anchor" href="#gotchas">Gotchas</a></h2>
<p>A few things I ran into:</p>
<ul>
<li><strong>direnv needs explicit allow</strong>: Every time you create or edit an <code>.envrc</code>, you must run <code>direnv allow</code>. This is a security feature — it prevents untrusted repos from injecting env vars into your shell.</li>
<li><strong>Token refresh</strong>: <code>gh auth token</code> returns the currently stored token. If a token expires or gets revoked, you'll need to re-authenticate with <code>gh auth login</code> for that account.</li>
<li><strong>Existing clones</strong>: If you have repos already cloned with <code>git@github.com:...</code> URLs that should use a different account, update the remote: <code>git remote set-url origin git@github-work:org/repo.git</code>.</li>
<li><strong>SSH agent confusion</strong>: If you use an SSH agent with multiple keys loaded, the agent may offer keys in an unpredictable order. <code>IdentitiesOnly yes</code> in the SSH config prevents this by forcing SSH to use only the specified key.</li>
</ul>
<hr>
<p>This is one of those setups that takes 15 minutes to configure and then you never think about it again. The right account is always active, in every tool, based purely on where you are in the filesystem.</p>]]></content:encoded>
            <author>Lucas McComb</author>
        </item>
        <item>
            <title><![CDATA[Week of February 16, 2026]]></title>
            <link>https://lem.fyi/blog/2026-W08/</link>
            <guid isPermaLink="false">https://lem.fyi/blog/2026-W08/</guid>
            <pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Shipped the lem.fyi journal system and pushed Darkly Suite through security compliance and OAuth checkout.]]></description>
            <content:encoded><![CDATA[<p>The week ended with this entry, which is fitting since I spent half of it building the system that generates it.</p>
<h2 id="what-i-built"><a class="heading-anchor" href="#what-i-built">What I built</a></h2>
<p><strong>lem-fyi</strong> got its journal pipeline. Issue #1 (Blog MVP) closed after 20 PRs. The markdown build runs on Cloudflare Pages, pulls weekly data from GitHub/Spotify/YouTube/Analytics, feeds it to Claude, and outputs entries like this one. I redesigned the theme toggle (#21), fixed the TOC (#18), and added RSS/Atom feeds (#14). The site went from static placeholder to functional blog in 10 commits and 12,872 lines.</p>
<p><strong>darkly-suite</strong> hit two milestones: Chrome Web Store compliance audit and OAuth-gated Stripe checkout. The compliance work (#304, #113) meant scrubbing host permissions, rewriting privacy forms to avoid <code>=======</code> (pre-commit hook saw them as merge conflicts), and testing distribution flows with placeholder extension IDs. The checkout flow (#402, #411, #416) requires Google OAuth before payment so Stripe can prefill the customer's email. Chrome Identity isn't available in content scripts, so the subscribe page generates throwaway tokens instead of using the extension's token. I chose prefill over lock after weighing tradeoffs - standard SaaS pattern, lets users edit if needed.</p>
<p>Admin tooling improved: membership sorting by email and sign-up date (#445), email notifications via Resend for business events (#397, #409), and endpoint cleanup (#443). The status endpoint no longer falls back to email lookup - token + product + status is enough. Lifetime licenses correctly show <code>stripe_subscription_id</code> as NULL since one-time payments don't create subscriptions.</p>
<p>Multi-agent sessions landed 9 milestones across darkly-suite and lem-fyi. One gotcha: branch name collision between agents on issue #300 required renaming to <code>302-docs-distribution-test</code>. Another: landing-suite has local copies of FAQ/Footer/Pricing alongside <code>@darkly/landing-shared</code> versions - both needed updates but grep only caught shared ones initially.</p>
<p><strong>lem-photo</strong> got minor about page edits (#1068, #1066). Two commits, twelve lines changed.</p>
<p>59-day commit streak still going. 100 commits to darkly-suite this week.</p>
<h2 id="what-i-consumed"><a class="heading-anchor" href="#what-i-consumed">What I consumed</a></h2>
<p><a href="https://open.spotify.com/search/Girl%20Talk" target="_blank" rel="noopener noreferrer">Girl Talk</a> topped the week - plunderphonics for deep focus during the compliance audit. <a href="https://open.spotify.com/search/The%20Durutti%20Column" target="_blank" rel="noopener noreferrer">The Durutti Column</a> and <a href="https://open.spotify.com/search/The%20Who" target="_blank" rel="noopener noreferrer">The Who</a> rounded out the rock mood. <a href="https://open.spotify.com/search/Aleksi%20Per%C3%A4l%C3%A4" target="_blank" rel="noopener noreferrer">Aleksi Perälä</a>'s IDM came in when I needed to shift gears from OAuth debugging.</p>
<p>Watched a <a href="https://www.youtube.com/results?search_query=Greenhill+Forge+magnetic+induction+water+heater" target="_blank" rel="noopener noreferrer">magnetic induction water heater build (Greenhill Forge)</a> and an <a href="https://www.youtube.com/results?search_query=Islestead+island+floor+installation" target="_blank" rel="noopener noreferrer">island floor installation (Islestead)</a>. Both scratched the same itch: people making physical things with their hands while I push pixels.</p>
<div class="spotify-tracks not-prose">
<a href="https://open.spotify.com/track/1f9qTGnov1fiaEK451GWjG" target="_blank" rel="noopener noreferrer" class="spotify-track"><img src="https://i.scdn.co/image/ab67616d000048510192ca742faf3f8dffa36f6b" alt="" class="spotify-track-art" loading="lazy" /><div class="spotify-track-info"><span class="spotify-track-name">Still Here</span><span class="spotify-track-artist">Girl Talk</span></div></a>
<a href="https://open.spotify.com/track/26xtASy24wnqcM9lNyWU72" target="_blank" rel="noopener noreferrer" class="spotify-track"><img src="https://i.scdn.co/image/ab67616d000048510d4707b41c14e960d4b726d2" alt="" class="spotify-track-art" loading="lazy" /><div class="spotify-track-info"><span class="spotify-track-name">Otis</span><span class="spotify-track-artist">The Durutti Column</span></div></a>
<a href="https://open.spotify.com/track/3qiyyUfYe7CRYLucrPmulD" target="_blank" rel="noopener noreferrer" class="spotify-track"><img src="https://i.scdn.co/image/ab67616d00004851fe24dcd263c08c6dd84b6e8c" alt="" class="spotify-track-art" loading="lazy" /><div class="spotify-track-info"><span class="spotify-track-name">Baba O'Riley</span><span class="spotify-track-artist">The Who</span></div></a>
<a href="https://open.spotify.com/track/5J3BH5RhOofonkFLZayikn" target="_blank" rel="noopener noreferrer" class="spotify-track"><img src="https://i.scdn.co/image/ab67616d0000485178319e54e815a56c932418be" alt="" class="spotify-track-art" loading="lazy" /><div class="spotify-track-info"><span class="spotify-track-name">UK74R1406090</span><span class="spotify-track-artist">Aleksi Perälä</span></div></a>
</div>
<h2 id="how-i-moved"><a class="heading-anchor" href="#how-i-moved">How I moved</a></h2>
<p>59,675 steps for the week - 13,740 on Sunday alone. Six workouts: two strength sessions, an elliptical day, and three shorter sessions including a sauna. Sleep averaged 6.4 hours with a resting heart rate of 52. Not my best sleep week - two nights didn't register - but the consistency held.</p>
<h2 id="reach"><a class="heading-anchor" href="#reach">Reach</a></h2>
<p>712 page views across all sites. lem.fyi's AI-enabled SWE system post pulled 78 views. darklysuite.com got 438 views, mostly landing page and admin dashboard traffic. lem.photo stayed quiet at 54 views, mostly API endpoints.</p>
<h2 id="closing"><a class="heading-anchor" href="#closing">Closing</a></h2>
<p>This was a week of systems. The journal pipeline writes itself now. The OAuth checkout flow handles real money. The compliance audit clears the path to Chrome Web Store publication. All infrastructure work - the kind that compounds.</p>]]></content:encoded>
            <author>Lucas McComb</author>
        </item>
        <item>
            <title><![CDATA[Building a Multi-Agent AI Development System with Claude Code]]></title>
            <link>https://lem.fyi/blog/ai-enabled-swe-system/</link>
            <guid isPermaLink="false">https://lem.fyi/blog/ai-enabled-swe-system/</guid>
            <pubDate>Wed, 18 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How I built a production-grade multi-agent development system with Claude Code — covering orchestration, quality gates, session coordination, and a reproducible configuration system.]]></description>
            <content:encoded><![CDATA[<p>I've spent the past several months building out what I'd call a production-grade AI-enabled software engineering workflow. Not just using Claude Code as a coding assistant, but designing an entire infrastructure layer around it: multi-agent orchestration, automated quality gates, session-level coordination, and a reproducible configuration system that deploys to a new machine in one command.</p>
<p>This post is a comprehensive walkthrough of what I've built, how it works, and what I've learned.</p>
<hr>
<h2 id="the-foundation-claude-dotfiles"><a class="heading-anchor" href="#the-foundation-claude-dotfiles">The Foundation: claude-dotfiles</a></h2>
<p>Everything starts with a single GitHub repository: <code>claude-dotfiles</code>. This repo is the single source of truth for my entire Claude Code development workflow. It contains global instructions, enforcement hooks, slash commands, MCP server configs, plugin settings, and workflow documentation, all version-controlled and deployable to any machine.</p>
<h3 id="whats-in-the-repo"><a class="heading-anchor" href="#whats-in-the-repo">What's in the repo</a></h3>
<pre><code>claude-dotfiles/
├── CLAUDE.md                    # Global instructions (~627 lines)
├── settings.json                # Hooks, permissions, plugins (template)
├── mcp.json                     # MCP server configs (template)
├── .claudeignore                # Template ignore file for repos
├── github-repo-protocols.md     # Full repo lifecycle guide (16KB)
├── multi-agent-system.md        # Multi-agent coordination
├── log-system.md                # Session logging documentation
├── commands/                    # 9 slash commands
│   ├── startup.md
│   ├── commit.md
│   ├── pr.md
│   ├── sync.md
│   ├── new-issue.md
│   ├── gs.md
│   ├── promote-rule.md
│   ├── dotsync.md
│   └── walkthrough.md
├── hooks/                       # 4 Python enforcement hooks
│   ├── auto-approve-bash.py
│   ├── auto-approve-file-ops.py
│   ├── enforce-git-workflow.py
│   └── enforce-issue-workflow.py
├── log/                         # Log analysis utilities
├── setup.sh                     # First-time setup script
└── sync-config.sh               # Reverse-sync config changes
</code></pre>
<h3 id="one-command-deployment"><a class="heading-anchor" href="#one-command-deployment">One-command deployment</a></h3>
<p>The <code>setup.sh</code> script handles everything:</p>
<ol>
<li>Creates <code>~/.claude/</code> if it doesn't exist</li>
<li>Backs up any existing <code>settings.json</code></li>
<li>Creates symlinks for files that don't need path templating (CLAUDE.md, commands/, hooks/, docs)</li>
<li>Generates <code>settings.json</code> and <code>mcp.json</code> from templates using <code>sed "s|__HOME__|$HOME|g"</code> to expand real paths</li>
<li>Optionally enables GitHub Repo Protocols (toggled by symlink presence)</li>
<li>Adds <code>GITHUB_TOKEN</code> to <code>~/.zshrc</code> if missing</li>
<li>Checks prerequisites (gh CLI, TypeScript LSP)</li>
</ol>
<p>The key design decision here is the split between <strong>symlinked files</strong> and <strong>generated files</strong>. Files like CLAUDE.md and commands/ are identical across machines, so they're symlinked directly. But <code>settings.json</code> and <code>mcp.json</code> contain absolute paths (<code>/Users/lem/code/...</code>), so they use <code>__HOME__</code> placeholders in the repo and get generated with real paths at setup time.</p>
<h3 id="reverse-sync"><a class="heading-anchor" href="#reverse-sync">Reverse sync</a></h3>
<p>When I modify settings locally (add a new permission, configure a new hook), I run <code>/dotsync</code>, which calls <code>sync-config.sh</code>. This script reads the live <code>~/.claude/settings.json</code>, replaces the real home path back to <code>__HOME__</code>, compares to the repo version, writes if changed, and pushes to GitHub. On another machine, a <code>git pull</code> plus re-running <code>setup.sh</code> applies the updates.</p>
<h3 id="standardized-claudeignore"><a class="heading-anchor" href="#standardized-claudeignore">Standardized .claudeignore</a></h3>
<p>Every repo gets the same <code>.claudeignore</code> template. It excludes <code>node_modules/</code>, <code>dist/</code>, lock files, IDE configs, binary/media files, <code>.env</code> files, and Supabase temp directories. This keeps Claude's context window clean and focused on actual source code.</p>
<hr>
<h2 id="the-permission-system"><a class="heading-anchor" href="#the-permission-system">The Permission System</a></h2>
<p>The <code>settings.json</code> file contains an exhaustive permission configuration with approximately 860 allow entries and 13 deny entries.</p>
<h3 id="the-deny-list"><a class="heading-anchor" href="#the-deny-list">The deny list</a></h3>
<p>The deny list is the safety layer. It blocks destructive operations at the tool level:</p>
<pre><code>rm, rmdir                          # File deletion
git reset --hard                   # Destructive git resets
git push --force, git push -f      # Force pushes
git clean                          # Working tree cleaning
git branch -D                      # Force-delete branches
docker rm, docker rmi              # Container/image deletion
docker system prune                # Docker cleanup
kubectl delete                     # Kubernetes resource deletion
DROP, TRUNCATE, DELETE FROM        # Destructive SQL
</code></pre>
<p>The philosophy: AI agents should never be able to accidentally destroy work. These operations require explicit human confirmation.</p>
<h3 id="the-allow-list"><a class="heading-anchor" href="#the-allow-list">The allow list</a></h3>
<p>The allow list covers essentially every CLI tool I use: git, gh, npm/yarn/pnpm/bun, cargo/rustup, python/pip, docker, kubectl, terraform, supabase, wrangler, aws/gcloud/az, psql/mysql/redis-cli, ffmpeg, and hundreds more. The breadth eliminates permission prompts for legitimate development commands while the deny list catches the dangerous ones.</p>
<h3 id="the-bug-that-inspired-the-hooks"><a class="heading-anchor" href="#the-bug-that-inspired-the-hooks">The bug that inspired the hooks</a></h3>
<p>Claude Code has known bugs (GitHub issues #15921 and #13340) where the VSCode extension ignores <code>settings.json</code> permissions entirely, and piped commands bypass the allow list. I documented this in a bug report and built a workaround: Python hooks that re-implement the permission logic at the tool-use interception layer, where they're reliably enforced regardless of the client.</p>
<hr>
<h2 id="enforcement-hooks"><a class="heading-anchor" href="#enforcement-hooks">Enforcement Hooks</a></h2>
<p>Four Python scripts intercept Claude Code's tool calls at different layers. They're the governance framework that ensures AI agents follow the same workflow rules as human developers.</p>
<h3 id="enforce-issue-workflowpy-userpromptsubmit"><a class="heading-anchor" href="#enforce-issue-workflowpy-userpromptsubmit">enforce-issue-workflow.py (UserPromptSubmit)</a></h3>
<p>Fires on every user prompt. It detects work-request verbs (update, fix, add, create, implement, build, change, modify, refactor, etc.) and filters out questions (starts with what/why/how, ends with <code>?</code>, contains explain/describe/list).</p>
<p>When a work request is detected, it injects a workflow reminder into Claude's context:</p>
<pre><code>STOP - Before making ANY code or file changes, you MUST:
1. CHECK: Does a GitHub issue exist for this work?
2. CREATE BRANCH: git checkout -b {issue-number}-{description}
3. IMPLEMENT: Make your changes
4. COMMIT &#x26; PR with issue-number prefix
</code></pre>
<p>This hook is toggled by the presence of <code>~/.claude/github-repo-protocols.md</code> as a symlink. Remove the symlink and the hook becomes a no-op. This lets me disable the full issue-tracking workflow for quick experiments without modifying any code.</p>
<h3 id="enforce-git-workflowpy-pretoolusebash"><a class="heading-anchor" href="#enforce-git-workflowpy-pretoolusebash">enforce-git-workflow.py (PreToolUse:Bash)</a></h3>
<p>Intercepts every <code>git commit</code> and <code>git push</code> command. It enforces three rules:</p>
<ol>
<li><strong>No commits on main</strong>: Must use a feature branch. The hook detects the current branch and blocks if it's <code>main</code> or <code>master</code>.</li>
<li><strong>Commit message format</strong>: Must match <code>^\d+:</code> (issue number prefix like <code>42: Fix the login bug</code>). Parses the message from <code>-m</code> or <code>--message</code> flags. Skips validation for heredoc-style messages (can't parse them reliably), merge commits, and amend-without-new-message.</li>
<li><strong>No pushes to main</strong>: Detects if the push target resolves to main (handles <code>HEAD</code>, explicit <code>main</code>, no refspec).</li>
</ol>
<p>There's an allowlist (<code>DIRECT_TO_MAIN_REPOS</code>) that exempts the dotfiles repo itself, and an emergency bypass via <code>ALLOW_MAIN_COMMIT=1</code> environment variable.</p>
<h3 id="auto-approve-bashpy-pretoolusebash"><a class="heading-anchor" href="#auto-approve-bashpy-pretoolusebash">auto-approve-bash.py (PreToolUse:Bash)</a></h3>
<p>The workaround for the settings.json permission bug. This hook reads all <code>Bash(pattern)</code> entries from <code>~/.claude/settings.json</code> at runtime. For each incoming Bash command, it checks the deny list first (higher priority), then the allow list. Pattern matching handles <code>:*</code> suffix (prefix match), <code> *</code> suffix (prefix match), and <code>startswith</code> fallback.</p>
<p>If a command matches a deny pattern, it returns <code>permissionDecision: deny</code> with a user-readable reason. If it matches an allow pattern, it returns <code>permissionDecision: allow</code>. If no match, it exits silently and falls through to Claude's default permission system.</p>
<h3 id="auto-approve-file-opspy-pretoolusereadeditwrite"><a class="heading-anchor" href="#auto-approve-file-opspy-pretoolusereadeditwrite">auto-approve-file-ops.py (PreToolUse:Read/Edit/Write)</a></h3>
<p>Same rationale as the Bash hook, same bug. Loads <code>Read(...)</code>, <code>Edit(...)</code>, <code>Write(...)</code> path patterns from settings. Normalizes paths, handles <code>**</code> glob as prefix match, falls back to <code>fnmatch</code>. Currently auto-approves all file operations within <code>~/code/**</code>, <code>~/.claude/**</code>, and <code>/tmp/**</code>.</p>
<hr>
<h2 id="slash-commands"><a class="heading-anchor" href="#slash-commands">Slash Commands</a></h2>
<p>Nine custom slash commands standardize the most common development workflows. Each is a markdown file in <code>commands/</code> with frontmatter specifying <code>description</code>, <code>allowed-tools</code>, and optional <code>argument-hint</code>. They use <code>!</code> prefix syntax to execute shell commands inline at invocation time.</p>
<h3 id="startup-session-initialization"><a class="heading-anchor" href="#startup-session-initialization">/startup (Session Initialization)</a></h3>
<p>The most complex command at ~250 lines. It runs at the beginning of every Claude Code session and bootstraps the agent's context:</p>
<p><strong>Section 0</strong>: Checks if the current directory is a git repo. If not, offers alternatives.</p>
<p><strong>Section 1 (Session Logging)</strong>: Pulls the latest logs from the centralized log repo. Derives agent identity from the working directory suffix (<code>lem-work-2</code> -> agent-2). Checks for today's log file. If the log repo has uncommitted changes older than 60 minutes, auto-commits and pushes them.</p>
<p><strong>Section 2 (Git Status)</strong>: Current branch, ahead/behind main, working directory cleanliness.</p>
<p><strong>Section 3 (Open PRs)</strong>: Lists all open pull requests for the current repo.</p>
<p><strong>Section 4 (Open Issues)</strong>: Lists issues by status: all open, in-progress, in-review. This gives the agent a clear picture of what work is available.</p>
<p><strong>Section 5 (Dependencies)</strong>: Checks if <code>package.json</code> exists, whether lock files are present, and if dependencies might be out of date relative to main.</p>
<p>After running all sections, it presents the agent with a prioritized recommendation of what to work on next.</p>
<h3 id="commit"><a class="heading-anchor" href="#commit">/commit</a></h3>
<p>Stages changes, runs lint/build verification, and commits with the enforced <code>{issue_number}: {description}</code> format. Reports the commit hash on success.</p>
<h3 id="pr"><a class="heading-anchor" href="#pr">/pr</a></h3>
<p>The full PR workflow: pre-flight cleanliness check, lint/build/test verification, rebase on <code>origin/main</code>, push with <code>-u</code>, check for PR template files in two locations (repo root and <code>.github/</code>), create the PR with <code>gh pr create</code>, update the issue label from <code>in-progress</code> to <code>in-review</code>, and report the PR URL.</p>
<h3 id="sync"><a class="heading-anchor" href="#sync">/sync</a></h3>
<p>Fetch origin and rebase the current branch on main. Handles uncommitted changes (offers to stash, commit, or abort). Reports any conflicts. Reminds about <code>--force-with-lease</code> for already-pushed branches.</p>
<h3 id="new-issue"><a class="heading-anchor" href="#new-issue">/new-issue</a></h3>
<p>Creates GitHub issues with the proper label taxonomy. Gathers title, type, and description. Maps types to labels (feature -> enhancement, bug -> bug). Supports a special <code>human-agent</code> type for tasks requiring manual intervention (env vars, account setup, etc.), which auto-assigns to the repo owner.</p>
<h3 id="gs"><a class="heading-anchor" href="#gs">/gs</a></h3>
<p>Quick git status overview: branch name, remote tracking, working directory status, sync status relative to main, last 5 commits, and open PRs. Summarizes the state and recommends the next action.</p>
<h3 id="promote-rule"><a class="heading-anchor" href="#promote-rule">/promote-rule</a></h3>
<p>Analyzes the current repo's CLAUDE.md for rules that should be promoted to the global config. Checks for explicit <code>&#x3C;!-- CANDIDATE:GLOBAL --></code> markers, detects implicit candidates (rules that are repo-agnostic), reads 2-3 other CLAUDE.md files to identify patterns, checks against the global config for duplicates, and presents findings in a table with a recommendation.</p>
<h3 id="dotsync"><a class="heading-anchor" href="#dotsync">/dotsync</a></h3>
<p>Runs <code>sync-config.sh --dry</code> first for a preview of what will change, then runs the actual sync. Reports what was updated and whether the changes were committed and pushed.</p>
<h3 id="walkthrough"><a class="heading-anchor" href="#walkthrough">/walkthrough</a></h3>
<p>Activates step-by-step guided mode for complex tasks. Identifies the task, breaks it into discrete steps, and presents one step at a time with a progress counter (Step N/Total). Waits for the user to confirm completion before proceeding to the next step. Never skips ahead.</p>
<hr>
<h2 id="the-audit-skill"><a class="heading-anchor" href="#the-audit-skill">The /audit Skill</a></h2>
<p>Beyond slash commands, I've built a comprehensive codebase audit system as a Claude Code skill. The <code>/audit</code> command is a 7-phase self-healing system that launches 8 parallel agents, auto-fixes what it can, and creates GitHub issues for what needs human review.</p>
<h3 id="phase-1-pre-flight"><a class="heading-anchor" href="#phase-1-pre-flight">Phase 1: Pre-Flight</a></h3>
<p>In fix mode (the default), it verifies the working directory is clean and creates a checkpoint branch (<code>audit-checkpoint-YYYYMMDD-HHMMSS</code>) for rollback. If there are uncommitted changes, it stops and offers options.</p>
<h3 id="phase-2-discovery"><a class="heading-anchor" href="#phase-2-discovery">Phase 2: Discovery</a></h3>
<p>Detects monorepo structure (checks for <code>apps/</code>, <code>packages/</code>, <code>workspaces</code>, <code>pnpm-workspace.yaml</code>), identifies the tech stack (TypeScript, React, package manager), reads existing CLAUDE.md and ESLint configs, and determines the audit scope.</p>
<h3 id="phase-3-parallel-audit-8-agents"><a class="heading-anchor" href="#phase-3-parallel-audit-8-agents">Phase 3: Parallel Audit (8 Agents)</a></h3>
<p>Eight Task agents launch simultaneously, each covering a different category:</p>
<table>
<thead>
<tr>
<th scope="col">Agent</th>
<th scope="col">Focus</th>
<th scope="col">Auto-fixable Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td>Security</td>
<td>Hardcoded secrets, SQL injection, XSS, auth issues</td>
<td>Remove console.logs with sensitive data</td>
</tr>
<tr>
<td>Dependencies</td>
<td>npm vulnerabilities, outdated packages, unused deps</td>
<td><code>npm audit fix</code>, <code>npm uninstall</code> unused</td>
</tr>
<tr>
<td>Code Quality</td>
<td>ESLint violations, unused vars, long methods, empty catches</td>
<td><code>eslint --fix</code>, remove unused imports</td>
</tr>
<tr>
<td>Architecture</td>
<td>Circular deps, god objects, layering violations</td>
<td>Limited (mostly human review)</td>
</tr>
<tr>
<td>TypeScript/React</td>
<td>Excessive <code>any</code>, hooks violations, Fast Refresh issues</td>
<td>Add inferred types, add missing keys</td>
</tr>
<tr>
<td>Testing</td>
<td>Missing test files, tests without assertions</td>
<td>Generate test stubs</td>
</tr>
<tr>
<td>Documentation</td>
<td>Missing JSDoc, stale comments, README gaps</td>
<td>Generate JSDoc from types</td>
</tr>
<tr>
<td>Performance</td>
<td>N+1 queries, missing React.memo, large imports</td>
<td>Add memo wrappers</td>
</tr>
</tbody>
</table>
<p>Each agent reports structured JSON with severity, file/line, description, auto-fixability status, and fix confidence level.</p>
<h3 id="phase-4-classify"><a class="heading-anchor" href="#phase-4-classify">Phase 4: Classify</a></h3>
<p>All findings are collected, deduplicated, and split into two queues: <code>auto_fix_queue</code> (high-confidence fixable) and <code>human_review_queue</code> (everything else).</p>
<h3 id="phase-5-fix-cycle"><a class="heading-anchor" href="#phase-5-fix-cycle">Phase 5: Fix Cycle</a></h3>
<p>For each auto-fixable finding, a Task agent implements the fix. Then the verification suite runs (lint, type-check, tests). If all pass, the fix is committed as an atomic commit (<code>audit: {category} - {brief title}</code>). If any verification fails, the fix is reverted with <code>git checkout -- .</code> and moved to the human review queue.</p>
<h3 id="phase-6-summary"><a class="heading-anchor" href="#phase-6-summary">Phase 6: Summary</a></h3>
<p>Displays results in a table by category and severity, with fix results (successfully fixed / failed verification / skipped for human review), a list of all commits made, and rollback instructions pointing to the checkpoint branch.</p>
<h3 id="phase-7-issue-creation"><a class="heading-anchor" href="#phase-7-issue-creation">Phase 7: Issue Creation</a></h3>
<p>Optionally creates GitHub issues for findings that need human review. Labels them with <code>audit</code> and <code>needs-human-review</code>. Groups findings by category into single issues. Checks for existing audit issues to avoid duplicates on re-runs.</p>
<h3 id="reference-files"><a class="heading-anchor" href="#reference-files">Reference Files</a></h3>
<p>The skill includes five reference documents that the audit agents consult:</p>
<ul>
<li><strong>security-patterns.md</strong>: OWASP Top 10 mapping, regex patterns for detecting API keys (OpenAI <code>sk-</code>, GitHub <code>ghp_</code>, AWS <code>AKIA</code>), injection patterns, auth weaknesses, insecure configurations, severity guidelines</li>
<li><strong>architecture.md</strong>: Clean Architecture patterns, god object detection thresholds (>1000 lines, >20 methods), circular dependency detection via <code>madge</code>, feature-based vertical slice recommendations</li>
<li><strong>code-quality.md</strong>: Martin Fowler refactoring patterns, cyclomatic complexity thresholds (>10/function), cognitive complexity (>15), nesting depth (>4 levels), naming consistency</li>
<li><strong>fix-patterns.md</strong>: Implementation guides for each fix type with when-to/when-not-to guidance, verification commands by project type</li>
<li><strong>output-template.md</strong>: GitHub issue templates with severity-based sections, collapsible details for medium/low findings, rollback instructions</li>
</ul>
<hr>
<h2 id="mcp-servers"><a class="heading-anchor" href="#mcp-servers">MCP Servers</a></h2>
<p>Seven MCP (Model Context Protocol) servers are configured globally, giving Claude Code direct access to external services:</p>
<table>
<thead>
<tr>
<th scope="col">Server</th>
<th scope="col">Purpose</th>
<th scope="col">Details</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>GitHub</strong></td>
<td>Issue/PR management</td>
<td>Uses <code>GITHUB_TOKEN</code> from environment</td>
</tr>
<tr>
<td><strong>Filesystem</strong></td>
<td>Local file access</td>
<td>Scoped to <code>~/code</code></td>
</tr>
<tr>
<td><strong>Memory</strong></td>
<td>Persistent context</td>
<td>Cross-session knowledge retention</td>
</tr>
<tr>
<td><strong>Fetch</strong></td>
<td>Web content retrieval</td>
<td>HTTP fetching for documentation, APIs</td>
</tr>
<tr>
<td><strong>Supabase (lem-work)</strong></td>
<td>Database access</td>
<td>Direct SQL, migrations, logs for lem-work project</td>
</tr>
<tr>
<td><strong>Supabase (lem-photo)</strong></td>
<td>Database access</td>
<td>Direct SQL, migrations, logs for lem-photo project</td>
</tr>
<tr>
<td><strong>n8n</strong></td>
<td>Workflow automation</td>
<td>Connected to cloud n8n instance for pipeline management</td>
</tr>
</tbody>
</table>
<p>The MCP config uses <code>__HOME__</code> placeholders in the dotfiles repo and gets expanded to real paths by <code>setup.sh</code>. This means the same config template works on any machine without modification.</p>
<p>Having Supabase as an MCP server is particularly valuable for debugging. Instead of using browser automation to diagnose database issues (slow, unreliable), I can run SQL directly, check logs, and apply migrations from within Claude Code. The debugging priority I've established is: Supabase MCP first for data issues, server logs for API issues, browser automation only as a last resort for UI issues.</p>
<hr>
<h2 id="plugins"><a class="heading-anchor" href="#plugins">Plugins</a></h2>
<p>Fourteen Claude Code plugins extend the base functionality. Grouped by function:</p>
<h3 id="code-quality"><a class="heading-anchor" href="#code-quality">Code Quality</a></h3>
<ul>
<li><strong>code-review</strong>: Multi-agent PR review that examines code changes across multiple dimensions</li>
<li><strong>pr-review-toolkit</strong>: Comprehensive review agents including a silent-failure hunter, code simplifier, comment analyzer, test analyzer, and type design analyzer</li>
<li><strong>security-guidance</strong>: Real-time security checks on file edits as they happen</li>
</ul>
<h3 id="development"><a class="heading-anchor" href="#development">Development</a></h3>
<ul>
<li><strong>feature-dev</strong>: Guided feature development with codebase analysis and architecture-focused planning</li>
<li><strong>frontend-design</strong>: Production-grade UI generation that avoids generic AI aesthetics</li>
<li><strong>typescript-lsp</strong>: TypeScript language server integration for type-aware code intelligence</li>
<li><strong>serena</strong>: Semantic code analysis and understanding</li>
</ul>
<h3 id="integration"><a class="heading-anchor" href="#integration">Integration</a></h3>
<ul>
<li><strong>github</strong>: GitHub platform integration (issues, PRs, branches, releases)</li>
<li><strong>supabase</strong>: Supabase project management and database operations</li>
<li><strong>playwright</strong>: Headless browser automation for E2E testing and screenshots</li>
<li><strong>figma</strong>: Figma design tool integration for implementing designs from Figma files</li>
<li><strong>greptile</strong>: Deep codebase search and understanding</li>
</ul>
<h3 id="workflow"><a class="heading-anchor" href="#workflow">Workflow</a></h3>
<ul>
<li><strong>hookify</strong>: Create custom Claude Code hooks from conversation analysis</li>
<li><strong>explanatory-output-style</strong>: Educational/explanatory output mode that provides insights about implementation choices</li>
</ul>
<p>All plugins are pre-authorized in the permissions system. If a plugin is installed, the user has already decided to grant access. No per-operation permission prompts.</p>
<hr>
<h2 id="multi-agent-orchestration"><a class="heading-anchor" href="#multi-agent-orchestration">Multi-Agent Orchestration</a></h2>
<p>This is the core of the system. Four Claude Code agents work in parallel on the same codebase without file conflicts, git collisions, or duplicated work.</p>
<h3 id="architecture"><a class="heading-anchor" href="#architecture">Architecture</a></h3>
<p>Each project that supports multi-agent work uses four independent clones:</p>
<pre><code>~/code/{repo}-repos/
├── {repo}-0/           # Clone 0 (agent-0)
├── {repo}-1/           # Clone 1 (agent-1)
├── {repo}-2/           # Clone 2 (agent-2)
└── {repo}-3/           # Clone 3 (agent-3)
</code></pre>
<p>Each clone is a full, independent git repository with its own <code>.git/</code> directory, branches, stash, reflog, and <code>node_modules/</code>. There are no shared resources between clones, which means:</p>
<ul>
<li><strong>Full isolation</strong>: Each agent has its own git state, its own branches, its own stash. No cross-agent interference.</li>
<li><strong>Standard git workflows</strong>: Every git command works exactly as documented. No special rules or workarounds.</li>
<li><strong>Independent fetches</strong>: Each clone fetches on its own schedule. No shared object store to reason about.</li>
</ul>
<p>Each agent runs in its own tmux pane, visible simultaneously. I manage all four from my phone via Blink terminal (iOS) through Mosh + Tailscale, which survives WiFi drops and device sleep.</p>
<h3 id="the-evolution-from-worktrees-to-clones"><a class="heading-anchor" href="#the-evolution-from-worktrees-to-clones">The evolution from worktrees to clones</a></h3>
<p>The original architecture used a bare git repo plus four worktrees. The theory was compelling: shared git object store, single fetch updates all worktrees, zero duplicated data. After several weeks of running this across four repos with four agents each, the theory fell apart.</p>
<p><strong>What broke:</strong></p>
<ol>
<li>
<p><strong>Branch locking was the killer issue.</strong> Worktrees can't have the same branch checked out in two places. This broke standard <code>git checkout -b</code> workflows. The enforce-git-workflow hook and issue-workflow hook both assumed standard branch creation. Agents would create a branch but commits would end up on the wrong branch because the worktree was locked to its parking branch.</p>
</li>
<li>
<p><strong>Shared stashes were a liability.</strong> Worktrees share the reflog and stash through the bare repo. An agent stashing changes in one worktree could interfere with another agent's stash pop.</p>
</li>
<li>
<p><strong>"Never checkout main" rule was confusing.</strong> Local main couldn't exist in bare repo worktrees. Every agent and every hook had to use <code>origin/main</code> everywhere. This constantly tripped up agents and broke workflow assumptions.</p>
</li>
<li>
<p><strong>Storage savings were negligible.</strong> The bare repos were 2-9MB each. Duplicating across four clones adds at most 36MB total. <code>node_modules/</code> (the real disk consumer) was already per-worktree anyway.</p>
</li>
<li>
<p><strong>Single fetch was rarely useful.</strong> Agents fetch at different times. The shared-fetch advantage only matters if all agents need the same new commits simultaneously, which almost never happens.</p>
</li>
<li>
<p><strong>Simpler mental model wins.</strong> A clone is a clone. Every developer and every AI agent understands it. Worktrees are a git power feature that adds cognitive overhead for agents that already have enough to track.</p>
</li>
</ol>
<p>The migration was straightforward: about 10 minutes per project. Delete the bare repo, clone four times, copy over <code>.env</code> files and <code>node_modules/</code>. The slightly higher disk usage is worth the dramatically simpler mental model.</p>
<h3 id="agent-identity"><a class="heading-anchor" href="#agent-identity">Agent identity</a></h3>
<p>Agent number is derived automatically from the working directory name suffix:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">AGENT_NUM</span><span style="color:#CF222E;--shiki-dark:#FF7B72">=</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$(</span><span style="color:#953800;--shiki-dark:#FFA657">basename</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">$PWD</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> |</span><span style="color:#953800;--shiki-dark:#FFA657"> grep</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -oE</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> '[0-9]+$'</span><span style="color:#CF222E;--shiki-dark:#FF7B72"> ||</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> echo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "0"</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">)</span></span></code></pre>
<table>
<thead>
<tr>
<th scope="col">Directory</th>
<th scope="col">Agent</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>lem-work-0</code> or <code>lem-work</code></td>
<td>agent-0</td>
</tr>
<tr>
<td><code>lem-work-1</code></td>
<td>agent-1</td>
</tr>
<tr>
<td><code>lem-work-2</code></td>
<td>agent-2</td>
</tr>
<tr>
<td><code>lem-work-3</code></td>
<td>agent-3</td>
</tr>
</tbody>
</table>
<p>No configuration needed. The agent knows who it is from where it's running.</p>
<h3 id="issue-claiming-protocol"><a class="heading-anchor" href="#issue-claiming-protocol">Issue claiming protocol</a></h3>
<p>Before any agent writes a single line of code, it must claim the issue through a multi-check protocol:</p>
<ol>
<li>
<p><strong>Check GitHub labels</strong>: Skip if any <code>agent-*</code> or <code>in-progress</code> label exists</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">gh</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> issue</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> view</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> {number}</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --json</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> labels</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --jq</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> '[.labels[].name]'</span></span></code></pre>
</li>
<li>
<p><strong>Check sibling clone branches</strong>: Skip if any clone already has a branch starting with <code>{issue-number}-</code></p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">for</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> dir </span><span style="color:#CF222E;--shiki-dark:#FF7B72">in</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> $(</span><span style="color:#953800;--shiki-dark:#FFA657">find</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> ~/code/{repo}-repos/</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -maxdepth</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> 1</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -name</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "{repo}-[0-9]*"</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -type</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> d</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">); </span><span style="color:#CF222E;--shiki-dark:#FF7B72">do</span></span>
<span class="line"><span style="color:#0550AE;--shiki-dark:#79C0FF">  echo</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "$(</span><span style="color:#953800;--shiki-dark:#FFA657">basename</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> $dir</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">): $(</span><span style="color:#953800;--shiki-dark:#FFA657">git</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> -C</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> $dir</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> branch </span><span style="color:#0550AE;--shiki-dark:#79C0FF">--show-current</span><span style="color:#0A3069;--shiki-dark:#A5D6FF">)"</span></span>
<span class="line"><span style="color:#CF222E;--shiki-dark:#FF7B72">done</span></span></code></pre>
</li>
<li>
<p><strong>Label the issue immediately</strong> (before creating a branch):</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#953800;--shiki-dark:#FFA657">gh</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> issue</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> edit</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> {number}</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --add-label</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "in-progress"</span><span style="color:#0550AE;--shiki-dark:#79C0FF"> --add-label</span><span style="color:#0A3069;--shiki-dark:#A5D6FF"> "agent-{N}"</span></span></code></pre>
</li>
<li>
<p><strong>Verify labels applied</strong> before proceeding</p>
</li>
</ol>
<p>Both checks must be clear. If there's a conflict (two agents try to claim simultaneously), the human supervisor resolves it. In practice, this hasn't been an issue because the label check and branch check together create a reliable mutex.</p>
<h3 id="git-workflow"><a class="heading-anchor" href="#git-workflow">Git workflow</a></h3>
<p>Standard git workflows apply. No special rules needed:</p>
<ul>
<li><strong>Branch from main</strong>: <code>git checkout main &#x26;&#x26; git pull &#x26;&#x26; git checkout -b {issue}-{desc}</code> or directly <code>git checkout -b {issue}-{desc} origin/main</code>. Both work.</li>
<li><strong>After PR merge</strong>: Return to main: <code>git checkout main &#x26;&#x26; git pull &#x26;&#x26; git branch -d {old-branch}</code></li>
<li><strong>Stashes are per-clone</strong>: Each agent has its own stash. No cross-agent interference.</li>
<li><strong>npm install is per-clone</strong>: Each has its own <code>node_modules/</code>.</li>
<li><strong>Env files are per-clone</strong>: <code>.env</code> and <code>.env.local</code> must be copied individually.</li>
</ul>
<hr>
<h2 id="session-logging-system"><a class="heading-anchor" href="#session-logging-system">Session Logging System</a></h2>
<p>The coordination layer that makes multi-agent work possible. Without it, agents would duplicate work, lose context between sessions, and have no awareness of what other agents are doing.</p>
<h3 id="architecture-1"><a class="heading-anchor" href="#architecture-1">Architecture</a></h3>
<p>A centralized git repo (<code>lem-agent-logs</code>) where every agent writes structured entries:</p>
<pre><code>~/code/lem-agent-logs/
├── biotechstonks/
│   └── 20260203/
│       ├── agent-0.md
│       └── agent-1.md
├── darkly-suite/
│   └── 20260216/
│       ├── agent-0.md
│       ├── agent-1.md
│       ├── agent-2.md
│       └── agent-3.md
├── gmail-darkly/
│   └── 20260212/
│       ├── agent-0.md
│       ├── agent-1.md
│       └── agent-2.md
└── ... (14 projects tracked)
</code></pre>
<p>Each agent writes exclusively to its own file. This eliminates merge conflicts entirely. When multiple agents push to the log repo simultaneously, <code>git pull --rebase</code> resolves cleanly because the files never overlap.</p>
<h3 id="mandatory-log-triggers"><a class="heading-anchor" href="#mandatory-log-triggers">Mandatory log triggers</a></h3>
<p>The log must be updated immediately at each of these events:</p>
<ol>
<li><strong>After every git commit</strong>: Issue number, branch, what changed, decisions made, gotchas encountered</li>
<li><strong>After creating a PR</strong>: Issue number, PR URL, mark <code>#in-review</code></li>
<li><strong>After PR merge</strong>: Mark <code>#completed</code>, commit and push the log repo</li>
<li><strong>After closing an issue</strong>: Resolution summary, mark <code>#completed</code>, commit and push</li>
<li><strong>Before context compaction</strong>: Current WIP state, uncommitted changes, next step (this preserves context when Claude's conversation gets too long and earlier messages are compressed)</li>
</ol>
<h3 id="cross-agent-awareness"><a class="heading-anchor" href="#cross-agent-awareness">Cross-agent awareness</a></h3>
<p>At session start (<code>/startup</code>), every agent reads other agents' logs for today's date directory. This builds a "Cross-Agent Awareness" section that documents what each sibling is working on:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#0550AE;--shiki-light-font-weight:bold;--shiki-dark:#79C0FF;--shiki-dark-font-weight:bold">## Cross-Agent Awareness</span></span>
<span class="line"></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">-</span><span style="color:#1F2328;--shiki-light-font-weight:bold;--shiki-dark:#E6EDF3;--shiki-dark-font-weight:bold"> **Agent-1**</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> (today): Working on issue #84, created PR #87. In-review.</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">-</span><span style="color:#1F2328;--shiki-light-font-weight:bold;--shiki-dark:#E6EDF3;--shiki-dark-font-weight:bold"> **Agent-2**</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> (today): Idle on agent-2 branch.</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">-</span><span style="color:#1F2328;--shiki-light-font-weight:bold;--shiki-dark:#E6EDF3;--shiki-dark-font-weight:bold"> **Agent-3**</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> (today): Working on issue #95 (bundle completion).</span></span></code></pre>
<p>The agent uses this to avoid claiming issues that are already in flight, even if the GitHub labels haven't been updated yet.</p>
<h3 id="log-file-format"><a class="heading-anchor" href="#log-file-format">Log file format</a></h3>
<p>Real example from a session:</p>
<pre class="shiki shiki-themes github-light-default github-dark-default" style="background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3" tabindex="0"><code><span class="line"><span style="color:#0550AE;--shiki-light-font-weight:bold;--shiki-dark:#79C0FF;--shiki-dark-font-weight:bold"># agent-0 — 20260217 — darkly-suite</span></span>
<span class="line"></span>
<span class="line"><span style="color:#116329;--shiki-dark:#7EE787">> Continued from previous session (20260216)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#0550AE;--shiki-light-font-weight:bold;--shiki-dark:#79C0FF;--shiki-dark-font-weight:bold">## Session Start</span></span>
<span class="line"></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">-</span><span style="color:#1F2328;--shiki-light-font-weight:bold;--shiki-dark:#E6EDF3;--shiki-dark-font-weight:bold"> **Time**</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: 2026-02-17 afternoon ET</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">-</span><span style="color:#1F2328;--shiki-light-font-weight:bold;--shiki-dark:#E6EDF3;--shiki-dark-font-weight:bold"> **Branch**</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0550AE;--shiki-dark:#79C0FF">`main`</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> (clean, up to date)</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">-</span><span style="color:#1F2328;--shiki-light-font-weight:bold;--shiki-dark:#E6EDF3;--shiki-dark-font-weight:bold"> **Previous session context**</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: Completed massive monorepo buildout...</span></span>
<span class="line"></span>
<span class="line"><span style="color:#0550AE;--shiki-light-font-weight:bold;--shiki-dark:#79C0FF;--shiki-dark-font-weight:bold">## Work Log</span></span>
<span class="line"></span>
<span class="line"><span style="color:#0550AE;--shiki-light-font-weight:bold;--shiki-dark:#79C0FF;--shiki-dark-font-weight:bold">### Issue #92 — Fix mini-panel white background in dark mode</span></span>
<span class="line"></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">-</span><span style="color:#1F2328;--shiki-light-font-weight:bold;--shiki-dark:#E6EDF3;--shiki-dark-font-weight:bold"> **Branch**</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="color:#0550AE;--shiki-dark:#79C0FF">`92-fix-mini-panel-dark-mode`</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> (from </span><span style="color:#0550AE;--shiki-dark:#79C0FF">`origin/main`</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">)</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">-</span><span style="color:#1F2328;--shiki-light-font-weight:bold;--shiki-dark:#E6EDF3;--shiki-dark-font-weight:bold"> **PR**</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: #93 — https://github.com/lucasmccomb/darkly-suite/pull/93</span></span>
<span class="line"><span style="color:#953800;--shiki-dark:#FFA657">-</span><span style="color:#1F2328;--shiki-light-font-weight:bold;--shiki-dark:#E6EDF3;--shiki-dark-font-weight:bold"> **Merged**</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: PR #93 → </span><span style="color:#0550AE;--shiki-dark:#79C0FF">`4f124a1`</span><span style="color:#1F2328;--shiki-dark:#E6EDF3"> on main #completed</span></span>
<span class="line"></span>
<span class="line"><span style="color:#1F2328;--shiki-light-font-weight:bold;--shiki-dark:#E6EDF3;--shiki-dark-font-weight:bold">**Wrong fix (commit 1)**</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: Added settings-container class to mini-panel.</span></span>
<span class="line"><span style="color:#1F2328;--shiki-dark:#E6EDF3">This broke layout because .settings-container has height: 100% and display: flex...</span></span>
<span class="line"></span>
<span class="line"><span style="color:#1F2328;--shiki-light-font-weight:bold;--shiki-dark:#E6EDF3;--shiki-dark-font-weight:bold">**Correct fix (commit 2)**</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: Added mini-panel to the CSS re-inversion rules.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#1F2328;--shiki-light-font-weight:bold;--shiki-dark:#E6EDF3;--shiki-dark-font-weight:bold">**Lesson**</span><span style="color:#1F2328;--shiki-dark:#E6EDF3">: When working with filter-based dark mode, prefer CSS-only fixes over DOM class changes.</span></span></code></pre>
<p>Key elements: continuation notes linking to previous sessions, issue/branch/PR references with URLs, status tags (<code>#completed</code>, <code>#in-progress</code>, <code>#blocked</code>), decision capture ("wrong fix / correct fix"), and lesson sections that preserve institutional knowledge across sessions.</p>
<hr>
<h2 id="per-project-configuration-layering"><a class="heading-anchor" href="#per-project-configuration-layering">Per-Project Configuration Layering</a></h2>
<p>Claude Code loads all CLAUDE.md files in the directory hierarchy from root to working directory. This creates a three-tier inheritance system:</p>
<ol>
<li>
<p><strong>Global</strong> (<code>~/.claude/CLAUDE.md</code>): Universal rules that apply everywhere. No AI attribution in commits, PR template usage, git workflow, session logging, code standards, security, MCP tool permissions.</p>
</li>
<li>
<p><strong>Workspace</strong> (<code>~/code/CLAUDE.md</code>): Describes the multi-project directory layout, repo shorthand references, git aliases.</p>
</li>
<li>
<p><strong>Project</strong> (<code>{repo}/CLAUDE.md</code>): Project-specific build commands, tech stack, unique behaviors.</p>
</li>
</ol>
<h3 id="how-projects-specialize"><a class="heading-anchor" href="#how-projects-specialize">How projects specialize</a></h3>
<table>
<thead>
<tr>
<th scope="col">Project</th>
<th scope="col">Tech Stack</th>
<th scope="col">Unique Rules</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>lem-photo</strong></td>
<td>React + Express + Supabase + Cloudflare + SwiftUI</td>
<td>"Build locally, deploy pre-built" server model (Render's free tier can't compile TS). Never use <code>--no-verify</code> for server pushes. CI disabled, replaced by 8-step pre-push hook. Migrations run via Supabase MCP directly.</td>
</tr>
<tr>
<td><strong>gmail-darkly</strong></td>
<td>Chrome Extension + TypeScript + React + Stripe</td>
<td>Never verify in browser via automation (extension requires manual chrome://extensions refresh). Dev mode bypasses Stripe payment gate. InboxSDK integration rules. <code>gd-</code> CSS prefix.</td>
</tr>
<tr>
<td><strong>sheets-darkly</strong></td>
<td>Chrome Extension + TypeScript + React + Stripe</td>
<td>Custom DOM injection (no InboxSDK). Waffle grid canvas handling (double-inversion technique). <code>sd-</code> CSS prefix.</td>
</tr>
<tr>
<td><strong>docs-darkly</strong></td>
<td>Chrome Extension + TypeScript + React + Stripe</td>
<td>Kix canvas tile handling for Google Docs. <code>dd-</code> CSS prefix.</td>
</tr>
<tr>
<td><strong>darkly-suite</strong></td>
<td>pnpm monorepo, 9 packages, 4 extensions</td>
<td>Default to dev mode (production build enables Stripe paywall locally). Auto-rebuild decision flow. Conflict detection via <code>data-darkly-active</code> attribute. Prefix resolution uses three strategies (CSS loader, React context, config injection).</td>
</tr>
<tr>
<td><strong>biotechstonks</strong></td>
<td>React + Express + Supabase + n8n</td>
<td>4-agent n8n AI pipeline. JSONB tags with GIN indexes. Entity lazy-creation pattern. Finnhub stock API integration with rate limiting.</td>
</tr>
<tr>
<td><strong>totomail</strong></td>
<td>Tauri 2 + React + Rust + SQLite</td>
<td>Tauri IPC patterns. OAuth tokens in OS keychain. Rust toolchain (<code>cargo check</code>).</td>
</tr>
<tr>
<td><strong>human-of-habit</strong></td>
<td>React + Supabase + Tailwind + Radix</td>
<td>Vanilla CSS imports (not CSS modules). Migrations require human-agent issues (older pattern).</td>
</tr>
<tr>
<td><strong>nadaproof</strong></td>
<td>React + Supabase + Vite</td>
<td>CI enabled and mirrors pre-push hooks exactly. Monorepo with shared package built first. Security headers in render.yaml.</td>
</tr>
</tbody>
</table>
<p>Project files that say "global instructions are in <code>~/.claude/CLAUDE.md</code>" are documenting what Claude Code already handles automatically. It's a human-readable reminder that these files intentionally contain only project-specific additions.</p>
<hr>
<h2 id="quality-gates"><a class="heading-anchor" href="#quality-gates">Quality Gates</a></h2>
<p>Quality is enforced at multiple levels, from individual keystrokes to deployment.</p>
<h3 id="pre-commit-every-commit"><a class="heading-anchor" href="#pre-commit-every-commit">Pre-commit (every commit)</a></h3>
<p>A global git hook runs on every commit across all repos:</p>
<ul>
<li><strong>gitleaks</strong>: Scans for accidentally committed secrets (API keys, tokens, passwords)</li>
<li><strong>lint-staged</strong>: Runs ESLint with <code>--fix</code> and TypeScript type-checking on staged files only</li>
</ul>
<h3 id="pre-push-every-push"><a class="heading-anchor" href="#pre-push-every-push">Pre-push (every push)</a></h3>
<p>The most sophisticated gate. lem-photo's pre-push hook is 113 lines and mirrors the full CI pipeline:</p>
<ol>
<li>Lint client workspace</li>
<li>Lint server workspace</li>
<li>Type-check client</li>
<li>Type-check server</li>
<li>Run client tests</li>
<li>Run server tests</li>
<li>Build client</li>
<li>Build server + verify <code>server/dist/</code> is committed and up-to-date</li>
</ol>
<p>A rapid mode (<code>SKIP_CHECKS=1 git push</code>) skips steps 1-6 but still builds both workspaces and verifies the server dist. The hook uses colored output and timing for each step.</p>
<p>Other projects adapt this pattern to their needs. nadaproof builds its shared package first, then runs the standard lint/type-check/test/build sequence. The darkly extensions run lint, type-check, tests, and webpack build.</p>
<h3 id="cicd-github-actions"><a class="heading-anchor" href="#cicd-github-actions">CI/CD (GitHub Actions)</a></h3>
<p>Where enabled, CI runs on PR events and push-to-main. The pipeline typically mirrors the pre-push hook exactly, ensuring that local and CI checks are identical. Some projects (lem-photo) have disabled CI to save GitHub Actions minutes and rely entirely on the pre-push hook.</p>
<h3 id="automated-accessibility-testing"><a class="heading-anchor" href="#automated-accessibility-testing">Automated accessibility testing</a></h3>
<p>All darkly extensions include WCAG AA contrast ratio tests that run programmatically across every theme preset and color pairing. The test computes relative luminance using the WCAG 2.1 formula and asserts >= 4.5:1 for body text and >= 3:1 for UI components. This catches accessibility regressions automatically across 78+ theme/color combinations.</p>
<h3 id="coverage-thresholds"><a class="heading-anchor" href="#coverage-thresholds">Coverage thresholds</a></h3>
<p>Test coverage is enforced at the framework level. lem-photo requires 60% coverage on the client and 70% on the server. Vitest fails the run if thresholds aren't met, which means the pre-push hook blocks the push.</p>
<hr>
<h2 id="real-results"><a class="heading-anchor" href="#real-results">Real Results</a></h2>
<h3 id="darkly-suite-full-monorepo-in-2-days"><a class="heading-anchor" href="#darkly-suite-full-monorepo-in-2-days">darkly-suite: Full monorepo in 2 days</a></h3>
<p>Four agents working in parallel built an entire pnpm monorepo from scratch:</p>
<ul>
<li>9 packages (shared core, 3 site packages, 4 extension packages, landing page)</li>
<li>4 Chrome extensions (Gmail, Sheets, Docs, bundle)</li>
<li>Self-hosted payment system (Cloudflare Pages Functions + D1 + Stripe)</li>
<li>23 PRs merged across 2 days</li>
<li>All tests passing, all extensions functional</li>
</ul>
<p>The git log of the log repo shows the real-time coordination: four agents committing to the shared log repo within seconds of each other, each working on a different package, with <code>git pull --rebase</code> resolving cleanly because each agent writes to its own file.</p>
<p>Day 1 parallel streams:</p>
<ul>
<li>Agent-0: Scaffolded monorepo, created all 61 GitHub issues, coordinated merges</li>
<li>Agent-1: Built site packages and individual extensions</li>
<li>Agent-2: Built CSS prefix loader, webpack factory, bundle extension, CI</li>
<li>Agent-3: Built landing page, payment APIs, D1 schema, marketing pages</li>
</ul>
<h3 id="gmail-darkly-50-issues-in-a-sprint"><a class="heading-anchor" href="#gmail-darkly-50-issues-in-a-sprint">gmail-darkly: 50 issues in a sprint</a></h3>
<p>50 agent issues closed across 7 PRs, with 120+ tests passing. 18 PRs merged on day 1 alone. The extension shipped with self-hosted Stripe payments (no SDK, raw <code>crypto.subtle</code> HMAC-SHA256 for webhook verification), WCAG-validated themes, InboxSDK integration, and a full admin portal.</p>
<h3 id="lem-photo-2267-tests"><a class="heading-anchor" href="#lem-photo-2267-tests">lem-photo: 2,267+ tests</a></h3>
<p>The most tested project in the portfolio:</p>
<ul>
<li>~1,388 client tests (Vitest + React Testing Library + MSW in strict mode)</li>
<li>~764 server tests (Vitest + Supertest)</li>
<li>~89 Playwright E2E tests with programmatic auth (two users: regular + admin)</li>
<li>26 Python pytest tests for the facial recognition microservice</li>
<li>Coverage thresholds enforced (60% client, 70% server)</li>
</ul>
<p>The Playwright setup authenticates via Supabase REST API directly (not through browser UI), serializes session state as JSON, and injects it into browser contexts via custom fixtures. This makes E2E tests deterministic and headless-friendly.</p>
<h3 id="biotechstonks-ai-pipeline-processing-17-rss-feeds"><a class="heading-anchor" href="#biotechstonks-ai-pipeline-processing-17-rss-feeds">biotechstonks: AI pipeline processing 17 RSS feeds</a></h3>
<p>A 4-agent n8n workflow running daily at noon:</p>
<ul>
<li>4 Claude Opus instances processing different RSS feed categories in parallel (Reddit, News/PR Newswire, BioSpace, GlobeNewswire)</li>
<li>17 RSS feed tools across the 4 agents</li>
<li>Structured output parsing (companies, topics, sectors, action classification)</li>
<li>Deduplication via <code>source_url</code> unique constraint + upsert RPC</li>
<li>JSONB tags with GIN indexes for flexible querying</li>
</ul>
<hr>
<h2 id="how-this-scales"><a class="heading-anchor" href="#how-this-scales">How This Scales</a></h2>
<p>The system I've built for personal use maps directly to a team environment.</p>
<h3 id="the-consultant-starter-kit"><a class="heading-anchor" href="#the-consultant-starter-kit">The consultant starter kit</a></h3>
<p>The claude-dotfiles repo becomes a team template. Fork it, swap in client-specific values (GitHub org, Supabase project, n8n instance), run <code>setup.sh</code>, and every developer has identical enforcement hooks, quality gates, slash commands, and MCP connections from day one. No manual configuration of individual repos.</p>
<h3 id="boilerplate-project-templates"><a class="heading-anchor" href="#boilerplate-project-templates">Boilerplate project templates</a></h3>
<p>A library of pre-configured project templates, each with CI/CD pipelines, test frameworks, pre-push hooks, and CLAUDE.md project instructions already baked in. A new client engagement starts with picking the right template (React + Express, Chrome Extension, n8n pipeline, etc.), spinning up the repo, and the quality infrastructure is already in place.</p>
<h3 id="multi-consultant-coordination"><a class="heading-anchor" href="#multi-consultant-coordination">Multi-consultant coordination</a></h3>
<p>The session logging system scales naturally. If three consultants are working on the same client project, they each write to their own log file in the same date directory. At session start, they read each other's logs for awareness. The same coordination layer that prevents my four personal agents from stepping on each other works across a team.</p>
<p>This could be adapted to integrate with external tools. An MCP server configured to update Jira tasks or Confluence docs as work progresses would give non-technical stakeholders visibility into what's happening without requiring them to read git logs.</p>
<h3 id="shared-infrastructure"><a class="heading-anchor" href="#shared-infrastructure">Shared infrastructure</a></h3>
<p>For MCP tooling, shared remote servers (GitHub, Supabase, n8n) rather than local instances. Onboarding a new team member is just connecting to existing infrastructure rather than rebuilding it from scratch.</p>
<h3 id="the-enforcement-hooks-as-governance"><a class="heading-anchor" href="#the-enforcement-hooks-as-governance">The enforcement hooks as governance</a></h3>
<p>The hooks ensure that AI agents follow the same workflow rules as human developers. No special treatment, no shortcuts. Every commit references an issue. Every push passes the full verification suite. Every destructive operation is blocked. This is the kind of policy layer that enterprises need when adopting AI in their development workflows.</p>
<hr>
<h2 id="closing-thoughts"><a class="heading-anchor" href="#closing-thoughts">Closing Thoughts</a></h2>
<p>The most interesting thing about this system isn't any individual component. It's how they compose. The dotfiles repo makes the system reproducible. The hooks make it self-enforcing. The logging makes it observable. The clones make it parallel. And the per-project CLAUDE.md files make it adaptable.</p>
<p>A solo developer with four AI agents and the right infrastructure can produce output that would typically require a small engineering team. But the productivity multiplier isn't just about speed. It's about maintaining quality at scale through automated enforcement.</p>
<p>The system works not because the AI models are perfect, but because the infrastructure around them is designed to catch mistakes, prevent conflicts, and preserve context. That's the real lesson: AI enablement isn't about the models. It's about the workflow you design around them.</p>]]></content:encoded>
            <author>Lucas McComb</author>
        </item>
    </channel>
</rss>