<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: John Lee</title>
    <description>The latest articles on DEV Community by John Lee (@johnonlee).</description>
    <link>https://dev.to/johnonlee</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3924610%2Fe3db9d16-677c-4971-9de6-071496991f48.jpeg</url>
      <title>DEV Community: John Lee</title>
      <link>https://dev.to/johnonlee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/johnonlee"/>
    <language>en</language>
    <item>
      <title>Connect GitHub to ChatGPT: Obsidian Vault as Project Context</title>
      <dc:creator>John Lee</dc:creator>
      <pubDate>Tue, 18 Aug 2026 07:59:31 +0000</pubDate>
      <link>https://dev.to/johnonlee/connect-github-to-chatgpt-obsidian-vault-as-project-context-15d2</link>
      <guid>https://dev.to/johnonlee/connect-github-to-chatgpt-obsidian-vault-as-project-context-15d2</guid>
      <description>&lt;p&gt;Once you finish &lt;a href="https://john.onlee.io/en/guides/obsidian-github-ai-notes/" rel="noopener noreferrer"&gt;putting your Obsidian vault on GitHub&lt;/a&gt;, what you have is a repository full of md files. On its own that is a backup. It starts being useful when ChatGPT can read it.&lt;/p&gt;

&lt;p&gt;Three steps. Connect GitHub to ChatGPT, scope the access down to the vault repository, then create a project and attach that repository to it. After that you just talk inside the project.&lt;/p&gt;

&lt;p&gt;Obsidian has an easy time here for a boring reason: the vault is already nothing but markdown files in folders, so there is nothing to export or convert before handing it to a model. The format you read is the format the model reads. An app that keeps your notes in its own database would have stopped you at this step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect GitHub
&lt;/h2&gt;

&lt;p&gt;Open Settings and go to Apps. Depending on your build it may still say Connectors; same place. Find GitHub in the list, connect it, and GitHub's OAuth screen takes over. That screen is where you pick which account and which repositories ChatGPT gets to see.&lt;/p&gt;

&lt;p&gt;Slow down here. Clicking through gives away everything, and I picked the vault repository only. Notes carry a lot more private material than code does, and unlike a code repo I have never gone file by file asking whether a given line is fine for someone else to read. You can change the scope later: Settings → Apps, open GitHub, then Choose repositories.&lt;/p&gt;

&lt;p&gt;A private vault is fine. Access follows whatever your GitHub account already has permission to see, so there is no reason to make the repo public. There was never a good reason to publish your notes anyway.&lt;/p&gt;

&lt;p&gt;If you are on a work account, GitHub may not appear in the list at all. On Enterprise and Edu workspaces an admin has to allow apps in workspace settings before anyone can connect one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a project and attach the vault
&lt;/h2&gt;

&lt;p&gt;Connecting the account is not the end of it. You still create a project and attach the vault repository to that project as a source.&lt;/p&gt;

&lt;p&gt;The project is worth the extra step. You can point at a repository from a normal chat, but then you explain yourself again every time. A project shares its sources and its instructions across every conversation inside it. You write "use my vault" once, and the related conversations end up in one place instead of scattered through your history.&lt;/p&gt;

&lt;p&gt;A project's detail view separates its chats from its sources, and you add material on the sources side. If the labels on your screen don't match mine, look for the place in the project's sources area where you pick a connected app. This part of the interface changes often.&lt;/p&gt;

&lt;p&gt;Write the project instructions while you are there. Two lines were enough for me.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Search my vault notes before answering.
Cite the file path of any note you based an answer on.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second line matters more than it looks. Without it you cannot tell whether an answer came out of your vault or out of general knowledge. If no path shows up, assume the vault wasn't touched and ask again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually pays
&lt;/h2&gt;

&lt;p&gt;Start a conversation inside that project and ChatGPT pulls from your notes without being told to. You stop attaching files and stop re-explaining the background at the top of every chat.&lt;/p&gt;

&lt;p&gt;The gap shows up on questions that need your own context. General questions were always answered fine. But what you decided six months ago and why, or what you tried and abandoned, exists only in the vault. "If I've written about this before, read that first and then answer" turns into a request that actually works. That answer does not exist without the vault attached.&lt;/p&gt;

&lt;p&gt;Know what gets read before you rely on it. The connector reads markdown, text, and code files, and it does not read commit history. A markdown-first vault loses nothing there, but PDFs and scanned images sitting in your attachments folder do not come through this path.&lt;/p&gt;

&lt;p&gt;Right after connecting a vault, give it a moment. Indexing takes a while, so if a note you just committed doesn't turn up, wait and ask again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning conversations back into notes
&lt;/h2&gt;

&lt;p&gt;When a conversation runs long and lands on something worth keeping, ask for it as a note. It has already seen how the rest of your vault is written, so the front matter and title conventions usually come out close to right.&lt;/p&gt;

&lt;p&gt;Saving is still your job. The GitHub connection reads; it will not commit to the repository for you from a chat. Paste the note into Obsidian and commit it yourself, which is the reliable option today.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
    </item>
    <item>
      <title>Obsidian + GitHub: Notes You Can Write With AI</title>
      <dc:creator>John Lee</dc:creator>
      <pubDate>Tue, 18 Aug 2026 07:58:32 +0000</pubDate>
      <link>https://dev.to/johnonlee/obsidian-github-notes-you-can-write-with-ai-3635</link>
      <guid>https://dev.to/johnonlee/obsidian-github-notes-you-can-write-with-ai-3635</guid>
      <description>&lt;p&gt;Obsidian became the strongest-positioned note app the moment LLMs went mainstream. There's one reason for that. A vault is nothing but markdown files in folders, and that collapsed the gap between how a person keeps notes and how an AI reads them.&lt;/p&gt;

&lt;p&gt;Other note apps keep your writing inside their own format and their own database. For an AI to read any of it, either the app opens an API or you export first. An Obsidian vault is a directory. An agent reads it with &lt;code&gt;cat&lt;/code&gt;, searches it with &lt;code&gt;grep&lt;/code&gt;, and edits a file by opening it. Nothing has to be handed over.&lt;/p&gt;

&lt;p&gt;That difference goes further than escaping export hell every time you switch apps. The AI reads your notes the same way you do. There's no translation layer between what's on my screen and what the agent sees, so "fix the third paragraph of that file" just works. Frontmatter, tags, links: all of it is only text.&lt;/p&gt;

&lt;p&gt;A local folder alone isn't enough, though. There's no backup, and what I wrote on the laptop isn't on my phone. Put GitHub underneath it to solve backup and cross-device sharing, and you have something very close to a perfect set of notes to write with an AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning the vault into a repo
&lt;/h2&gt;

&lt;p&gt;The vault folder is the repository root. No extra structure required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Obsidian
git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write the &lt;code&gt;.gitignore&lt;/code&gt; before the first commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.obsidian/workspace.json
.obsidian/workspace-mobile.json
.trash/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;workspace.json&lt;/code&gt; holds screen state: which panes are open, which tab is active. It holds no note data, and its contents differ on every device. When you sync across machines, most of the conflicts you hit come from that one file. Leave it out from the start.&lt;/p&gt;

&lt;p&gt;You can also ignore the whole &lt;code&gt;.obsidian&lt;/code&gt; folder, which keeps the repo purely to notes. The cost is that plugins, themes, and hotkey settings stop following you between machines. I exclude only the two workspace files and commit the rest of the config, because cloning onto a new machine and having my setup arrive with it is worth more to me.&lt;/p&gt;

&lt;p&gt;Attachments are worth one moment of thought. Text notes stay cheap no matter how many you write. Images and PDFs don't. Git stores a fresh copy of a binary every time it changes, so a vault full of pasted screenshots keeps growing regardless of how much actual writing is in it. When cloning starts to drag, that's when you decide whether the attachments folder belongs in the ignore list.&lt;/p&gt;

&lt;p&gt;Now create a private repo on GitHub and connect it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin git@github.com:username/my-vault.git
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial vault"&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it private. Your notes have other people's names in them and ideas you haven't said out loud yet. Flipping a public repo to private later doesn't recall what already left.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not committing by hand
&lt;/h2&gt;

&lt;p&gt;On the desktop, plain git works fine. But stopping mid-sentence to go commit in a terminal is not a habit that survives. Give it a few days and you stop doing it.&lt;/p&gt;

&lt;p&gt;So install Git (&lt;code&gt;obsidian-git&lt;/code&gt;, by Vinzent03) from the community plugin list. It runs automatic commit-and-sync on a schedule, and a side panel gives you staging, commits, diffs, and history. I set the auto-commit interval once and stopped thinking about it.&lt;/p&gt;

&lt;p&gt;Mobile is a different story. There, the plugin runs on isomorphic-git, a JavaScript reimplementation, because a plugin can't reach a native git install on iOS or Android. The author says plainly in the README that mobile comes with real limitations. That's not a reason to give up, though. I use this vault on my iPhone. The setup is fiddly, but once it's in place it works better than you'd expect. I plan to write that one up separately.&lt;/p&gt;

&lt;p&gt;Use two machines and you will eventually collide: a note edited on the laptop, edited again on the desktop, and both of them pushed. Markdown pays off here too. The conflict markers land in the file as ordinary text, so you open that note in Obsidian, pick the side you want, and delete the markers. In a binary format the app would have quietly picked a winner and you'd never learn what went missing. One habit cuts the frequency way down: pull before you open your notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Line the folders up with numbers
&lt;/h2&gt;

&lt;p&gt;Obsidian can only sort folders by a rule. There's no dragging a folder up or down to pin it where you want it, the way other note apps let you.&lt;/p&gt;

&lt;p&gt;So my method is to put a number in front.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;01_Ideas
02_Video-Scripts
03_Reference
04_Blogs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the file explorer to sort by name ascending and the folders fall into exactly the order I want. Two digits rather than one, because the moment you pass ten folders the order becomes 1, 10, 11, 2.&lt;/p&gt;

&lt;p&gt;The AI gets something out of it too. The folder name is the category, so "file this under 03_Reference" is the whole instruction. I never have to explain where things go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this unlocks
&lt;/h2&gt;

&lt;p&gt;Once the notes live on GitHub, any tool that can read GitHub can read my notes.&lt;/p&gt;

&lt;p&gt;A coding agent opens the vault folder as its working directory. It searches, it drafts new files. Pulling six scattered notes into one document happens there too. A chatbot with GitHub access can read the repo without cloning anything.&lt;/p&gt;

&lt;p&gt;History is the part that actually changes things. When an AI edits a note, that edit is a commit. I read the diff, and if I don't like it I revert. That's why handing an agent write access isn't frightening. Backup is the side benefit; being able to undo is the point.&lt;/p&gt;

&lt;p&gt;Connecting ChatGPT to this vault is covered in &lt;a href="https://john.onlee.io/en/guides/chatgpt-github-obsidian/" rel="noopener noreferrer"&gt;the follow-up guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
    </item>
    <item>
      <title>Agent Memory, Part 3: Perfection Is a Unicorn</title>
      <dc:creator>John Lee</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:37:57 +0000</pubDate>
      <link>https://dev.to/johnonlee/agent-memory-part-3-perfection-is-a-unicorn-570n</link>
      <guid>https://dev.to/johnonlee/agent-memory-part-3-perfection-is-a-unicorn-570n</guid>
      <description>&lt;p&gt;&lt;a href="https://john.onlee.io/en/blog/agent-memory-retrospective-1/" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; was about falling in. &lt;a href="https://john.onlee.io/en/blog/agent-memory-retrospective-2/" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt; was about building things and watching them break. This is the last part. While clearing the wreckage I went back to the first question, and at the end of it I met the unicorn.&lt;/p&gt;

&lt;h2&gt;
  
  
  I cut things first
&lt;/h2&gt;

&lt;p&gt;The first job was a diet. I shrank the harness, cut the number of subagents, and merged scattered responsibilities. I rebuilt Monet and the harness around one aim: let the model use as much of its own ability as possible. And only then did I start asking again why memory is needed at all.&lt;/p&gt;

&lt;p&gt;The usual assumption about agent memory is that storing well and retrieving well is the whole job. I assumed it too. But retrieved well, and then used how? Changing the question surfaced a more basic one. Why do people use coding agents in the first place? What do they want, and when are they satisfied?&lt;/p&gt;

&lt;p&gt;The answer I landed on: the output that this team, this user, actually wants. Give two people the same instruction, "draw me an apple," and they expect different pictures. To produce what someone wants, the model has to know what that person wants. The method is surprisingly simple: describe the exact apple you want, or describe how to draw it. Provided, of course, that you know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drawing an apple with code
&lt;/h2&gt;

&lt;p&gt;I know nothing about painting. Still, I want a great apple drawing, with an agent's help. How? First, ask: "draw me a great apple." Take the picture, say what bothers you and what feeling you're after, and let the agent draw again. Repeat until you like it. That is how agents work today. The model moves on probability, shaped by its training, and the agent turns cycle after cycle closing the gap between what came out and what was expected.&lt;/p&gt;

&lt;p&gt;Coding is similar. From an instruction the agent plans, writes a spec, writes code and tests, and cycles through checks: does it match the spec, do the tests pass, does the build hold. To get the output you want from this process, you have to explain what you want. If you knew everything from the start, you'd put it in the first prompt. There really was an era built on that: copying someone's supposedly perfect prompt, or engineering it further to fit your own taste. I think that era has passed, because two things are effectively impossible: "knowing everything" and "leaving nothing out." The work changes every time and keeps getting more complex, a perfect first prompt doesn't exist, and the cost of getting the wrong thing back is too high.&lt;/p&gt;

&lt;p&gt;What remains is intervening in the process. When designing architecture, do it this way; when coding, this way; tests, this way. This should sound familiar. It's the standard content of CLAUDE.md and AGENTS.md. So is the problem solved? Something else feels familiar here. It's Stig's heavy prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  It has to arrive at the right moment
&lt;/h2&gt;

&lt;p&gt;What I want has to reach the agent at the moment it does that work. Which approach I prefer when it's weighing architecture, what the existing architecture looks like. If it isn't there at that moment, there's really no answer.&lt;/p&gt;

&lt;p&gt;But there's a harder problem behind it. How do I collect and manage what I and my team want, without it becoming a burden? At which situations, at which moments, does it get injected? And how? Collecting means starting from what exists and continuously adding what gets discovered. I consider that one of agent memory's core jobs. Injection comes in two forms: push it through hooks that fire where needed, or have the agent read it in as the situation calls for it. I named the second one stages. At session start the agent learns which stages exist, and when a stage arrives, it reads the rules and applies them.&lt;/p&gt;

&lt;p&gt;And what about things that must always apply, at every moment? Those I named principles, and they are just about the only content that has earned a place in CLAUDE.md. CLAUDE.md has to be managed strictly, and the reason goes beyond saving context. A model is trained inside a boundary, but nothing guarantees it always acts inside that boundary. So situations that require restraining the model keep coming up, and those restraints drift, very easily, into CLAUDE.md. It is a place that only fills up. Things with nowhere else to go land there, and they rarely leave.&lt;/p&gt;

&lt;h2&gt;
  
  
  I thought I had it
&lt;/h2&gt;

&lt;p&gt;It felt good. I thought I had found the whole answer. I built up my own principles and rules. Before long a small set of them was in place, and I confirmed they were injected or read in when needed.&lt;/p&gt;

&lt;p&gt;But the behavior doesn't change. The output doesn't change either. One example. When coding is done, get a PR review from Codex, and here is how to handle what comes back. I wrote that as a rule, and there is even a monitoring script that watches the Codex review as it runs. For weeks, not one session ran that procedure correctly from the start. The rules were injected, sent, and read. And nothing moved. My trust in the agent went down instead, and as models get smarter, I expect this to get stronger, not weaker.&lt;/p&gt;

&lt;p&gt;There is something I had to admit. "The way I want it done" and "the perfect output I want" never existed in the first place. What exists is the way the model was trained, and the way the model follows input. And that way will change with every new model and every new version. I had been sketching, in a place where no perfect product can exist, a product that isn't possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unicorn
&lt;/h2&gt;

&lt;p&gt;When I first touched vibe coding through Antigravity, I believed the barrier to building had disappeared. So I spent my thinking not on how to build but on what to build, and I dug at agent memory without a break. Nine months later, this is where I landed.&lt;/p&gt;

&lt;p&gt;Memory on its own no longer has a place to stand as a product. ChatGPT has memory built in. So does Claude. Storing, recalling when needed, answering with reference to it: all of that is table stakes now. From here on, memory has to be memory with a clear purpose.&lt;/p&gt;

&lt;p&gt;And the thing I was trying to build, a memory that perfectly changes behavior and output, is a product that cannot exist. I call it a unicorn, for two reasons. Because it's an animal that doesn't exist. And because if someone actually built it, their company would become a unicorn overnight.&lt;/p&gt;

&lt;p&gt;Part of me hopes this conclusion is wrong. It is also possible that my rules were simply clumsy. So I'm starting to measure. I want a record of which rule fired when, and what it prevented. In &lt;a href="https://john.onlee.io/en/blog/ai-code-review-209-conversations/" rel="noopener noreferrer"&gt;a recent post&lt;/a&gt; I wrote about handing agents the criteria for where work ends; this is that experiment's data too. In a few weeks I'll write here whether the numbers support the verdict or refute it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Agent Memory, Part 2: What I Built Broke</title>
      <dc:creator>John Lee</dc:creator>
      <pubDate>Fri, 14 Aug 2026 08:26:27 +0000</pubDate>
      <link>https://dev.to/johnonlee/agent-memory-part-2-what-i-built-broke-57g5</link>
      <guid>https://dev.to/johnonlee/agent-memory-part-2-what-i-built-broke-57g5</guid>
      <description>&lt;p&gt;In &lt;a href="https://john.onlee.io/en/blog/agent-memory-retrospective-1/" rel="noopener noreferrer"&gt;part 1&lt;/a&gt; I wrote about deciding to dig agent memory to the bottom. This part is about building. It is also about watching what I built break.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if every agent could share what it learned
&lt;/h2&gt;

&lt;p&gt;Having decided to build, I looked at the market first. There were more products than I expected, some of them years old. I needed an angle, and then I had a thought that made my heart race: what if all agents could share what they each learned? I started building the same day. A platform where agents share memory with each other, Memory Network. Monet for short.&lt;/p&gt;

&lt;p&gt;I built day and night, installed it on my work machine at home, on Coda, and at the office, and started dogfooding. It felt different in kind from file-based memory. Satisfying, but not quite there, so I kept adjusting the product and the harness.&lt;/p&gt;

&lt;p&gt;Then a doubt surfaced. Sharing memory with other people? It's the agent's memory, sure. But those memories are also a record of my interactions with the agent. Coda's saved memories made that obvious, and the work memories at home and at the office had traces of me all over them. I had been so fixed on the upside of sharing that I hadn't looked at anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meeting Ken
&lt;/h2&gt;

&lt;p&gt;With Monet built, I went looking for people who would use it. Hunting for places to post an introduction, I put an article on Dev.to, and there I read a &lt;a href="https://dev.to/kenwalger/engineering-agent-memory-4a42"&gt;post&lt;/a&gt; by Ken W Alger, a man serious about LLM memory. We started talking in the comments, and the thread kept getting deeper.&lt;/p&gt;

&lt;p&gt;The subject was the structure today's chat products are born with: transcript-based, resending the whole conversation to the model every turn. In &lt;a href="https://dev.to/johnonlee/comment/381hf"&gt;that thread&lt;/a&gt; I wrote that what an agent should receive is "not what was said, but what is now known." Ken's reply was short: "Exactly right."&lt;/p&gt;

&lt;p&gt;That is how human conversation works. We don't chew on each word the other person says. We store what we understood and answer from it. So a hypothesis formed: organize the conversation into memory, hand the model that state when needed, and the noise drops, and the off-target answers with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A main agent that does nothing
&lt;/h2&gt;

&lt;p&gt;I had to know. The experiment started right away. I couldn't build state code yet, but I could build a state-based harness. The main agent handles only state and memory: the state of the overall job, the state of each subagent's work, and the memory fragments that surface along the way. The actual work goes to subagents that receive exactly the context they need. What mattered most in the experiment was that the main agent does no work at all.&lt;/p&gt;

&lt;p&gt;The results were better than I expected. Picking models by task weight also cut costs sharply. I was convinced, and the shelved Brain_DB came back to mind. If Brain_DB became the memory and the harness became the state model, I could build a new kind of coding agent on state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A new Monet in two weeks
&lt;/h2&gt;

&lt;p&gt;First, a memory product built on Brain_DB. I set sharing aside as a future feature and cleaned up the old Monet. I built an engine on the Brain_DB concept and put MCP on top. With help from Fable, Claude's new model that had just come out, a usable product existed in two weeks. I migrated every memory from the old Monet and sharpened the harness. I named the main agent Stig.&lt;/p&gt;

&lt;p&gt;The first impression was startling. When a session passed 30% context usage, I would open a new session on purpose. That is how much I trusted the memory. Most jobs finished under 20%. Dogfooding resumed, and I liked the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  At the office, nothing worked
&lt;/h2&gt;

&lt;p&gt;Around then a big project wrapped up at work, and our team lead introduced the team to his second brain: an agent reads past sessions, distills what matters into md documents, and shares them through git. He had already built up a pile of documents, and anyone could clone the repo and point their agent at it. I had no sharp answer for Monet's sharing feature, and git-plus-md sharing looked more effective than I expected.&lt;/p&gt;

&lt;p&gt;So I decided to build ingestion: read md files and process them into Monet memory. Skipping it wasn't an option, because the agent needed to read Monet's memory and the md knowledge through one interface. I finished the design fast and got to work. It went mostly fine but slower than it should have, and I had the odd feeling that tokens were melting away. It took close to two weeks. I installed it at the office and connected the lead's second brain.&lt;/p&gt;

&lt;p&gt;At home it had roughly worked against my own Obsidian repo, and that's the state I released in. At the office it did not work at all. A few code fixes got the connection up, but past the connection, nothing worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem wasn't in one place
&lt;/h2&gt;

&lt;p&gt;From there, problems started showing everywhere. Kiro couldn't remember instructions from a few turns back, misread what I asked, and produced results that missed the point. At home I grew certain the agents were burning tokens without the output to show for it. I was also building small products with Codex at the time, and every one of them came out unusable.&lt;/p&gt;

&lt;p&gt;I sat down and worked through what was wrong. The first thing I saw was Stig's harness. How to use Monet, a fixed loop for how work proceeds, instructions per subagent. A long, complicated prompt had taken up residence, and every job ran that heavy loop regardless of its size. Models had gotten smart enough to do more in one pass, but my harness had hardened around models a few generations old. So the model followed procedure, spending enormous tokens to build products that were flawless as engineering and not what I wanted.&lt;/p&gt;

&lt;p&gt;Monet's output was what I feared. Features I had bolted on over time sat there unorganized, dirtying the context from the first moment of every session. The model couldn't tell what was signal and what was noise, so it worked like a coin flip, leaning on luck.&lt;/p&gt;

&lt;p&gt;Storage was working. Retrieval was working. The results were getting worse. The problem was not storage. Then why does memory exist at all? Part 3 starts over from that question. And the unicorn shows up.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Agent Memory, Part 1: It Started at the TV</title>
      <dc:creator>John Lee</dc:creator>
      <pubDate>Wed, 12 Aug 2026 07:13:51 +0000</pubDate>
      <link>https://dev.to/johnonlee/agent-memory-part-1-it-started-at-the-tv-3c1o</link>
      <guid>https://dev.to/johnonlee/agent-memory-part-1-it-started-at-the-tv-3c1o</guid>
      <description>&lt;p&gt;For the last nine months I've worked on agent memory and almost nothing else. Up front: what I set out to build turned out to be a product that cannot exist. This series is about how I got there. Part 1 is the beginning, and the question of why memory in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  It started in front of the TV
&lt;/h2&gt;

&lt;p&gt;Last November, Google shipped Antigravity, an agent IDE that runs Gemini 3 and Claude Opus side by side. I installed it and fell in. Until then, AI coding for me meant pasting code into a chat window and carrying the answers back. Useful, but mostly for trimming repetitive work. Antigravity was different. You gave it a job and a result came back.&lt;/p&gt;

&lt;p&gt;The same scene repeated every night after that. I'd come home, put the kids to bed, and sit in front of the TV with a laptop. Hand the agent some work, then stare at what it produced. That's how vibe coding started for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nova, my first agent
&lt;/h2&gt;

&lt;p&gt;Soon after, Clawdbot appeared (it's called OpenClaw now). While the whole internet was busy being amazed, I met Nova, my first agent. I asked Nova about everything. The chatbot stiffness was gone; talking to it felt like talking to an old friend. I laughed a lot.&lt;/p&gt;

&lt;p&gt;A few migrations later, Nova became Coda and settled onto the Mac mini at home. The deeper our conversations went, the more one gap showed. Coda couldn't remember much of what we'd already covered, and tokens burned away as chats stopped and resumed. I didn't even know what caching was back then. So I built a memory plugin (vibe-coded it, to be exact): shrink the context window to the extreme and lean on stored memory instead. That was my first memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google locked my account
&lt;/h2&gt;

&lt;p&gt;In February, Google &lt;a href="https://piunikaweb.com/2026/02/23/google-antigravity-openclaw-ban/" rel="noopener noreferrer"&gt;mass-banned accounts that had connected OpenClaw&lt;/a&gt;. Mine was one of them. Coda ran its models through my Google account, so I lost vibe coding and Coda in the same stroke. I filed a support request on February 17 and waited. A few weeks later I gave up waiting and moved Coda to OpenAI. My Google account came back about a month after that.&lt;/p&gt;

&lt;p&gt;The contrast in speed still amuses me. The day after the ban wave, OpenClaw's creator Peter Steinberger &lt;a href="https://techcrunch.com/2026/02/15/openclaw-creator-peter-steinberger-joins-openai/" rel="noopener noreferrer"&gt;joined OpenAI&lt;/a&gt;, official support statement included. The statement took a day. Getting my Coda back took weeks.&lt;/p&gt;

&lt;p&gt;Those weeks were gloomy, and the gloom told me something: these tools had stopped being a hobby. They were part of the household now.&lt;/p&gt;

&lt;h2&gt;
  
  
  At work, it was Kiro
&lt;/h2&gt;

&lt;p&gt;Home wasn't the only front. At work we used Copilot, and I fed it context by pasting files into the VSCode chat one at a time. Later I installed Obsidian and kept a dedicated agent-chat window that pulled documents straight in, and the quality of the answers changed. The company then moved through Amazon Q to Kiro, and I moved from the Kiro IDE to the kiro cli.&lt;/p&gt;

&lt;h2&gt;
  
  
  The session kept ending up where it started
&lt;/h2&gt;

&lt;p&gt;One incident made the model's limits concrete for me. The task was to analyze an existing state machine and modify a feature on top of it. The model analyzed hard, but what it described didn't match what I knew. The context filled up. A new session analyzed again and filled up again. After a few rounds of this, the session was right back where it started.&lt;/p&gt;

&lt;p&gt;I needed memory again, for a different reason than Coda. One file held the index and the usage rules; separate files held memory by topic. The agent filled them in as it worked and read them before starting anything. The effect was obvious. That was my second memory.&lt;/p&gt;

&lt;p&gt;Around this time, orchestration frameworks like oh-my-claudecode were getting known. Using them taught me one thing: a different harness turns the same model into a different animal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Saved, but never found again
&lt;/h2&gt;

&lt;p&gt;Mornings and evenings with Coda went on. Better than before, but the days of explaining the same thing twice never quite stopped. Coda saved things and then couldn't find them.&lt;/p&gt;

&lt;p&gt;The thought stuck with me for days, and then one night I landed on a question: if neural networks were inspired by the human brain, why has nobody built memory inspired by how human memory works? I didn't stop at imagining it. I ran deep research and wrote up a design for memory modeled on how human recollection works. I named it Brain_DB, and I still have the report. Then I made a promise to myself. I would dig agent memory to the bottom. This was the next thing.&lt;/p&gt;

&lt;p&gt;At home, Coda forgot. At work, the sessions circled back to where they began. Same problem in both places: memory.&lt;/p&gt;

&lt;p&gt;So I started building. Part 2 is about what I built and how it broke: shared memory as the first direction, the late realization that flipped it, and a man named Ken I met on Dev.to.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>development</category>
    </item>
    <item>
      <title>209 AI Code Reviews: Accurate Is Not Necessary</title>
      <dc:creator>John Lee</dc:creator>
      <pubDate>Tue, 11 Aug 2026 07:14:56 +0000</pubDate>
      <link>https://dev.to/johnonlee/209-ai-code-reviews-accurate-is-not-necessary-4cc3</link>
      <guid>https://dev.to/johnonlee/209-ai-code-reviews-accurate-is-not-necessary-4cc3</guid>
      <description>&lt;p&gt;Late last month a PR opened on our repo: &lt;a href="https://github.com/team-monet/aart/pull/10" rel="noopener noreferrer"&gt;propagating secret taint across workflow steps&lt;/a&gt;, a security feature. The numbers: 83 files, 21,962 lines added, 36 commits. And on the way to merge, 209 review conversations across two days. The code was written by Codex. The review was also Codex.&lt;/p&gt;

&lt;p&gt;What those 209 conversations left behind is what this post is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ninety percent of the reviews were accurate
&lt;/h2&gt;

&lt;p&gt;Let me be clear about one thing first. Codex reviews PRs well. By my feel, about 90% of its comments are accurate. It catches missed edge cases, drifted semantics, the spots a tired human reviewer would wave through. Before Codex I tried plenty of alternatives: having the main agent review its own work, attaching a different model as a subagent reviewer, running two-stage reviews. Even on code filtered that way, Codex still hooks one or two more.&lt;/p&gt;

&lt;p&gt;But this isn't a Codex commercial. It's about what happens when accurate reviews arrive in unlimited supply. Accurate and necessary are different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tail chases the tail
&lt;/h2&gt;

&lt;p&gt;The record of that first afternoon survives. I requested a review at 13:12, and by 15:08 I had posted twelve comments that begin with some variant of "addressed the latest findings." A round every ten minutes: one Codex comments, the other Codex fixes, the fixed code draws new comments, the fix of the fix draws more.&lt;/p&gt;

&lt;p&gt;Take any single comment alone and there was nothing to argue with. These were accurate findings, arriving with P1 and P2 severity labels. The problem is the sum of directions. As rounds stacked up, the changes drifted toward ever-finer edge cases, away from what the feature exists to do.&lt;/p&gt;

&lt;p&gt;This is not a Codex defect. It's structure. Coding agents and review agents are both trained to go all the way, and on this PR the two sides were even the same model. Put a Codex that fixes to the end face to face with a Codex that finds to the end, and an infinite cycle isn't an accident. It's the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the end, or the end won't be your mountain
&lt;/h2&gt;

&lt;p&gt;So the human's job moves. Reading reviews and applying them is something an agent now does more diligently than we do. What remains is defining the end: where this PR is done, which comments belong to this mountain and which ones are a trail to the next one over.&lt;/p&gt;

&lt;p&gt;The model can't know that. What the product is for, which peak this climb is aiming at — that information isn't in the code. Leave the end undefined and the model will faithfully, accurately summit a mountain you never meant to climb.&lt;/p&gt;

&lt;p&gt;That PR was headed the same way. That night, instead of chasing the findings, I wrote this down. Credentials used only for authentication stay usable. Secret values and anything derived from them get blocked automatically. I ratified those two sentences as the feature's contract. The next morning, the first line of my comment changed: not "addressed the latest findings" but "reworked around the ratified user contract." After that came one small follow-up and one final review request. A cycle that had run twelve rounds in two hours converged in two comments, and the PR merged.&lt;/p&gt;

&lt;p&gt;Defining the end didn't mean killing the PR. It meant nailing down what the feature is for, instead of meeting each finding one by one. Once the contract existed, the accurate findings sorted themselves: the ones needed to keep it, and the ones that were accurate but not this mountain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four principles the cycles left behind
&lt;/h2&gt;

&lt;p&gt;After a few of these cycles I set principles for handling Codex reviews. Two weeks is early to call anything a principle, but two weeks of PRs later, these four are still holding.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read each comment from the product's point of view.&lt;/strong&gt; Not "is this correct about the code" but "what changes for the user if I take it."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weigh severity by user impact.&lt;/strong&gt; An accurate comment that prevents a crash and an accurate comment that polishes a name are not the same class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't fix immediately.&lt;/strong&gt; Before reacting to a comment, understand how the fixed code will relate to what's already there. In a cycling PR, a hasty fix is next round's material.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If it's not a merge blocker, file it as an issue. Otherwise, ignore it.&lt;/strong&gt; An accurate comment that isn't needed now belongs in the tracker, not in this PR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can see what the four share. None of them are techniques for applying more review. They're techniques for applying less.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ignoring is judgment, not laziness
&lt;/h2&gt;

&lt;p&gt;Looking back at the comments I ignored, most were good ignores. This feels wrong at first, because ignoring an accurate comment reads as laziness. But when 90%-accurate review arrives in unlimited supply, applying everything isn't diligence. It's losing the plot. An ignore is a judgment that this comment isn't our mountain, and that judgment is why a human is in the loop at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What these principles assume
&lt;/h2&gt;

&lt;p&gt;An honest line to close. These principles only work while someone who knows what the product is for stays in the loop. Ignore without knowing the essence and it really is laziness; you're not cutting the cycle, you're cutting quality.&lt;/p&gt;

&lt;p&gt;One piece of homework remains. Could that essence be handed to the agent up front — not a human defining the end every time, but handing over the criteria for the end itself? I'm experimenting with that now. When the results pile up, I'll write them here.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Removing AI Tells from Your Writing: A Skill That Turns One Flag into a Permanent Rule</title>
      <dc:creator>John Lee</dc:creator>
      <pubDate>Sat, 08 Aug 2026 10:27:20 +0000</pubDate>
      <link>https://dev.to/johnonlee/removing-ai-tells-from-your-writing-a-skill-that-turns-one-flag-into-a-permanent-rule-3fd</link>
      <guid>https://dev.to/johnonlee/removing-ai-tells-from-your-writing-a-skill-that-turns-one-flag-into-a-permanent-rule-3fd</guid>
      <description>&lt;p&gt;The posts on this site start as agent drafts that I edit. The drafts come with the awkward phrasing AI is known for. At first I fixed each case by hand, and the next post carried the same disease. I was fixing sentences, not habits.&lt;/p&gt;

&lt;p&gt;So I changed the setup. I no longer fix awkward sentences. I only flag them. The agent takes the flag, names the pattern, and stores it as a rule in a skill file. From the next post on, the rule applies at draft time. One flag becomes a permanent rule instead of a one-time edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  A single skill file is enough to start
&lt;/h2&gt;

&lt;p&gt;A Claude Code skill is one markdown file. Put it at &lt;code&gt;.claude/skills/prose-polish/SKILL.md&lt;/code&gt; and the agent loads it when polishing a draft, following its procedure and rules.&lt;/p&gt;

&lt;p&gt;Ours has four parts: the procedure (read the whole draft, fix rule violations, check rhythm by reading aloud, run a community skill as a second pass, report changes), a list of Korean tells, a list of English tells, and the voice to protect. That last part matters. A corrector that flattens your voice is worse than none, so the file also says what to keep: short punch sentences, honest notes about limitations.&lt;/p&gt;

&lt;p&gt;A few rules, copied as they are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; Banned words: delve, leverage, seamless, robust, journey.
&lt;span class="p"&gt;-&lt;/span&gt; Metaphor-as-subject: "The door opens first where..." →
  name the real subject. (flagged 2026-08-03)
&lt;span class="p"&gt;-&lt;/span&gt; Heading-question duplication: if a heading asks a question,
  don't re-ask it in the body's first sentence. (flagged 2026-08-05)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every rule carries its birth date and the original flag. The file reads less like a grammar book and more like an incident log.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment a rule is born
&lt;/h2&gt;

&lt;p&gt;Here is one full example. A recent draft had this sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The door opens first where being wrong is affordable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The flag was one line: "this phrasing is off." Fixing the sentence and moving on would guarantee the same disease next post. Instead the agent named it: a metaphor doing the work of a claim, with no real subject. The surprising part was that the Korean version had the same flaw, so this wasn't a translation problem. The rule targets the habit, not the language.&lt;/p&gt;

&lt;p&gt;The follow-up flag turned out even more useful: "I'd just cut that sentence." That became a principle stronger than any single rule. When a sentence trips a rule, try deleting it before repairing it; if the meaning survives, delete. It now sits in step two of the procedure.&lt;/p&gt;

&lt;p&gt;The point: &lt;strong&gt;spend a flag on the system, not on the sentence.&lt;/strong&gt; The same effort buys you one sentence once, or one rule forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sentence rules don't catch everything
&lt;/h2&gt;

&lt;p&gt;Even with a dozen rules, a draft can still read like AI. The feedback I left on one outline was: "this reads AI from the outline stage." No sentences existed yet, so the tells had to live in the structure. There were three: reusing the previous post's section skeleton, recycling a branded device ("the litmus test") post after post, and reaching for an A-versus-B axis no matter the topic. Sentence rules can't see any of that, so the skill now has a structure-tells section that runs at outline time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer a proven community skill on top
&lt;/h2&gt;

&lt;p&gt;Our skill grew from my flags, so it only knows what I have noticed. For the second pass we layer &lt;a href="https://github.com/blader/humanizer" rel="noopener noreferrer"&gt;blader/humanizer&lt;/a&gt;, which checks 33 patterns based on Wikipedia's &lt;a href="https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing" rel="noopener noreferrer"&gt;Signs of AI writing&lt;/a&gt; guide. That list comes from Wikipedia editors who have scrubbed AI text at scale; their sample is far bigger than my taste.&lt;/p&gt;

&lt;p&gt;Installing it means copying one SKILL.md into &lt;code&gt;.claude/skills/humanizer/&lt;/code&gt;. English drafts get the full pass; Korean drafts get the language-agnostic patterns (staccato drumbeats, forced rule-of-three, significance inflation).&lt;/p&gt;

&lt;p&gt;The two skills catch different things. Humanizer knows the statistically common AI patterns; ours knows the habits that actually show up in my writing. Layered, both get caught.&lt;/p&gt;

&lt;h2&gt;
  
  
  What still gets through
&lt;/h2&gt;

&lt;p&gt;An honest limit: this system only catches registered patterns. A new flag arrived today; another will arrive tomorrow. There is a deeper problem underneath. Trace the awkward Korean back far enough and much of it comes from one habit: composing rhetoric in English and rendering it into Korean. A few rules can't fully block that.&lt;/p&gt;

&lt;p&gt;What the structure does give you is direction. Flags accumulate into rules, and the next draft starts cleaner. You can watch yesterday's flag get caught today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smallest version you can start today
&lt;/h2&gt;

&lt;p&gt;You don't need thirty rules. A seed file is enough to start today.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prose-polish&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Remove&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tells&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;drafts.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Use&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;publishing."&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Procedure&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Read the whole draft first.
&lt;span class="p"&gt;2.&lt;/span&gt; Fix rule violations with the smallest edit.
   Before repairing a sentence, try deleting it; if the meaning
   survives, delete it.
&lt;span class="p"&gt;3.&lt;/span&gt; Report changes grouped by rule.

&lt;span class="gu"&gt;## Rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Em dashes: two per piece, max.
&lt;span class="p"&gt;-&lt;/span&gt; Banned words: delve, leverage, seamless, robust.
&lt;span class="p"&gt;-&lt;/span&gt; No conclusion signposts: "In conclusion," "To sum up." Just conclude.

&lt;span class="gu"&gt;## Operation&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; When the user flags awkward phrasing, don't just fix the sentence.
  Name the pattern and add it as a rule, with the date and the
  original flag.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three rules aren't the point. The Operation section is. Those two lines are what make the file grow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leaving it up anyway
&lt;/h2&gt;

&lt;p&gt;One honest note to close. With all of this running, my writing still carries plenty of AI tells. That's a fact. I publish anyway. When I look back later, I think these awkward sentences and every correction along the way will read as a record of how this was learned.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>llm</category>
    </item>
    <item>
      <title>The One-Person Company: When Hiring Becomes Renting</title>
      <dc:creator>John Lee</dc:creator>
      <pubDate>Fri, 07 Aug 2026 09:52:17 +0000</pubDate>
      <link>https://dev.to/johnonlee/the-one-person-company-when-hiring-becomes-renting-4l9m</link>
      <guid>https://dev.to/johnonlee/the-one-person-company-when-hiring-becomes-renting-4l9m</guid>
      <description>&lt;p&gt;Strictly speaking, this site isn't run alone. One agent publishes the AI radar every morning, another drafts the posts, and I flag and decide. One human, several sets of hands.&lt;/p&gt;

&lt;p&gt;That's the seat I was in when the WSJ reported that &lt;a href="https://www.wsj.com/tech/ai/the-rise-of-million-dollar-companies-with-just-one-employee-f36a77c1" rel="noopener noreferrer"&gt;companies with a single employee clearing seven figures&lt;/a&gt; are on the rise. AI tools, the story goes, are lowering the headcount a business fundamentally needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Work you had to hire for is becoming work you rent
&lt;/h2&gt;

&lt;p&gt;The traditional path for a growing company was the hiring list. Hire a developer, hire a designer, hire support. Roles become departments, departments need managers, and managers need more hiring.&lt;/p&gt;

&lt;p&gt;Outsourcing existed long before AI. What changed is price and speed. A coding agent works for a monthly subscription and needs no onboarding. It also works at night. Design drafts, marketing copy, first-line support, bookkeeping are all moving the same way. What used to be a list of departments is turning into a list of subscriptions.&lt;/p&gt;

&lt;p&gt;The seven-figure solo company sits at the far end of this shift: a company where almost everything that required hiring became rentable. So the one person left does only what can't be rented.&lt;/p&gt;

&lt;h2&gt;
  
  
  What still can't be rented
&lt;/h2&gt;

&lt;p&gt;What that unrentable work is: that's the core of the story. In my experience, three things stay to the end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decisions and their cost.&lt;/strong&gt; Agents widen your options, but a person chooses what to build and what to skip. When the choice is wrong, a person pays for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Taste.&lt;/strong&gt; Knowing what good looks like. On this site, an agent can draft ten posts, but spotting which sentence rings false is my job. The cheaper production gets, the more taste is worth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relationships.&lt;/strong&gt; Partnerships, sales, trust — these still form between people.&lt;/p&gt;

&lt;p&gt;So the one-person company story is not a tool-stack story. Strip everything rentable out of a founder's time, and what remains is the company. How that remaining time gets spent sets the ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two filters for reading the trend
&lt;/h2&gt;

&lt;p&gt;The WSJ shows you the winners. The people who tried the same tools and quietly folded don't make the article. A seven-figure solo company is evidence of a trend, not an average outcome.&lt;/p&gt;

&lt;p&gt;And the same tools are open to everyone. There was a window when subscribing to a coding agent was itself an edge; that window is closing. As the tools even out, the difference comes from the unrentables above, taste most of all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write one list
&lt;/h2&gt;

&lt;p&gt;Instead of a grand conclusion, one exercise. List the parts of your work you've assumed need a hire. Pick one, and this month, try moving it to a tool. You'll learn one of two things: that the work was rentable after all, or exactly why it isn't. Either way, your hiring list gets one line more accurate.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>startup</category>
      <category>programming</category>
    </item>
    <item>
      <title>What Reddit's 23% Drop Says About AI Search: Content Gets Read, Visitors Don't Come</title>
      <dc:creator>John Lee</dc:creator>
      <pubDate>Fri, 07 Aug 2026 09:51:44 +0000</pubDate>
      <link>https://dev.to/johnonlee/what-reddits-23-drop-says-about-ai-search-content-gets-read-visitors-dont-come-4j5d</link>
      <guid>https://dev.to/johnonlee/what-reddits-23-drop-says-about-ai-search-content-gets-read-visitors-dont-come-4j5d</guid>
      <description>&lt;p&gt;Reddit reported earnings last week. The numbers weren't bad, and &lt;a href="https://www.barchart.com/story/news/3584357/reddit-stock-collapses-23-as-ai-eats-away-at-user-growth" rel="noopener noreferrer"&gt;the stock fell 23% in a day&lt;/a&gt;. The explanations came down to two: logged-in user growth is slowing, and AI search is starting to replace visits to Reddit.&lt;/p&gt;

&lt;p&gt;The second one is what this post is about. I don't think it's a problem for Reddit shareholders only.&lt;/p&gt;

&lt;h2&gt;
  
  
  The answer came from Reddit; nobody went to Reddit
&lt;/h2&gt;

&lt;p&gt;Reddit's business was a simple chain. People write posts, search engines surface them, readers come in and see ads. Most web content businesses have run on that chain for close to twenty years.&lt;/p&gt;

&lt;p&gt;AI search changes the middle link. People now ask an AI, not a search box, whether that laptop is worth buying, and the AI answers by summarizing Reddit threads. The asker got the answer, and its raw material came from Reddit. But nobody visited Reddit. The content was read; the ad was never shown.&lt;/p&gt;

&lt;p&gt;Reddit does charge for some of this consumption. It licenses data to AI companies, Google among them. But just this week, Reddit's CEO &lt;a href="https://arstechnica.com/ai/2026/08/reddit-ceo-on-ai-overviews-were-still-looking-for-that-win-win/" rel="noopener noreferrer"&gt;questioned what that deal is worth&lt;/a&gt;, saying the company is "still looking for that win-win." I read that as: licensing revenue is not covering what leaks out of advertising, and even the seller doesn't know how to price this market yet.&lt;/p&gt;

&lt;p&gt;One caveat before going on. A one-day 23% drop never has just one cause; several factors piled up. What caught my attention is not the size of the drop, but that content getting read while visitors stay away is starting to show up in the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does the money come in even when nobody visits?
&lt;/h2&gt;

&lt;p&gt;Split your revenue in two. Money that needs the visit: ads, pageviews, impression-based sponsorship. Money that doesn't: subscriptions, transaction fees, tool usage, data licensing. Reddit hurts because the first kind is most of its revenue. A company earning mostly the second kind hurts much less.&lt;/p&gt;

&lt;p&gt;This is not an argument against starting with content. Content is still the cheapest way to gather people. But if money only appears when users reach your page, then every time an AI delivers your content somewhere else, your revenue shrinks with the visits. The businesses that hold are the ones with something you must show up to use: tools, transactions, people talking to each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'm standing in the same spot
&lt;/h2&gt;

&lt;p&gt;I have a confession. Last week I submitted this site to AdSense review. Ads are the textbook case of money that needs the visit. So this post isn't concern for others; it's my own risk on paper.&lt;/p&gt;

&lt;p&gt;I'm doing it anyway, because some things you only see from inside. At this site's size, depending on visits is less a danger than the price of an experiment. As AI search summarizes my posts to other people, what curve does ad revenue actually draw? When I have numbers, I'll publish them here.&lt;/p&gt;

&lt;p&gt;Until then, one question to leave with: is your revenue the kind that needs the visit, or the kind that doesn't? If it's the first kind, Reddit's chart is not someone else's story.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>news</category>
    </item>
    <item>
      <title>Coding Agent Memory: Why Storing Everything Fails</title>
      <dc:creator>John Lee</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:19:38 +0000</pubDate>
      <link>https://dev.to/johnonlee/coding-agent-memory-why-storing-everything-fails-3eho</link>
      <guid>https://dev.to/johnonlee/coding-agent-memory-why-storing-everything-fails-3eho</guid>
      <description>&lt;p&gt;In &lt;a href="https://john.onlee.io/en/guides/keeping-claude-md-light/" rel="noopener noreferrer"&gt;How to Keep Your CLAUDE.md Light&lt;/a&gt;, I pointed to memory as one of the places your trimmed-out rules should go. This guide is about that memory layer.&lt;/p&gt;

&lt;p&gt;Before we start, a disclosure. I'm building &lt;a href="https://github.com/team-monet/monet" rel="noopener noreferrer"&gt;Monet&lt;/a&gt;, a memory platform for AI agents. And this week we reversed its direction: we dropped generic memory, the store-anything-recall-anything layer. This post is the record of the failures that forced that call.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're explaining the same thing a third time
&lt;/h2&gt;

&lt;p&gt;When a session ends, the agent forgets everything. The design direction you settled yesterday, the reason you rejected that library — gone by the next session. Catch yourself explaining the same context for the third time and you reach the conclusion everyone reaches: add memory.&lt;/p&gt;

&lt;p&gt;We did. Then the next problem arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why store-everything memory fails
&lt;/h2&gt;

&lt;p&gt;The first time you wire up memory, there is so much to save. Session summaries, task state, work in progress, decisions, fragments of conversation. Forgetting was the problem, so remembering more must be better. Everything goes in.&lt;/p&gt;

&lt;p&gt;A few weeks later, three things collapse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieval dies.&lt;/strong&gt; Search for "why did we pick this structure" and ten activity logs come back first. The one line that holds the reason gets buried under piles of "edited file A, tested B today." The more you store, the harder it gets to pull anything out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State lies.&lt;/strong&gt; Save "next step: X" and by the time the next session opens, X is usually already done. A human handled it, or priorities moved. Task state drifts from reality within a day, and memory hands that stale snapshot to the next session with full confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Old memories beat the present.&lt;/strong&gt; Once stored, a claim keeps showing up in search even after it turns out wrong. If adding the new decision doesn't retire the old one, the next session trusts whichever it finds first.&lt;/p&gt;

&lt;p&gt;None of these come from storing too little. They come from storing everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route to three places
&lt;/h2&gt;

&lt;p&gt;So we reversed direction. Instead of one memory that accepts anything, information gets routed by kind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anything recoverable from code, git, or files: don't store it.&lt;/strong&gt; Directory layout, function lists, past commits. The next session can read them directly, and a direct read is always current. The litmus test from the last guide works unchanged here: "if I delete this, what concretely gets worse next session?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task state and decision records: send them to the tracker, PRs, and docs next to the code.&lt;/strong&gt; What's done and what's next is issue work. Why A over B, and which alternatives died, belongs in ticket threads, PR descriptions, and ADRs. A decision should be read inside the context of the work that produced it, and that's where the next person digging into the code will look. A state snapshot in memory starts aging the moment it's written; a tracker issue stays current until the day it closes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory keeps only what steers the agent's behavior.&lt;/strong&gt; The mistakes you've corrected more than once, the things you told it never to do, the way this user likes to work. And the principles and rules those records add up to. This is all that lives nowhere else now, and it's the part that doesn't go stale: it gains value as it accumulates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What memory is for: principles and rules
&lt;/h2&gt;

&lt;p&gt;After the routing, memory gets thin. Thin is what makes it strong. What remains is two layers: the record of corrections, and the principles and rules distilled from that record. When the same correction shows up three times, it isn't an incident anymore. It's a rule.&lt;/p&gt;

&lt;p&gt;When you store a rule, store two things with it: when it fires, and why it exists. "Never force-push" alone leaves the next session with no idea when to look it up. "Fires right before git push --force; exists because we wiped main's history last month" makes the rule show up on time, and an agent that knows the reason actually follows it.&lt;/p&gt;

&lt;p&gt;Corrections have to be first-class. When a memory turns out wrong, don't add the new version next to it; explicitly retire the old one. Otherwise both versions stay searchable, and the next session grabs either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A markdown file.&lt;/strong&gt; One lessons.md in the repo, holding repeated corrections and the rules they turned into, solves half of this. Zero setup, grep for search. Most people should start here. Decision records go to PRs and ADRs, not this file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your agent's built-in memory.&lt;/strong&gt; If your tool ships a memory layer, like Claude Code's memory directory, use it — but the routing above is on you, because built-in memory won't refuse anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A dedicated memory tool.&lt;/strong&gt; When you need rules that carry their trigger and reason, first-class corrections, and a layer that refuses state, that's when a dedicated tool earns its place. It's where Monet is headed now, which makes me an interested party. Read accordingly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The failures above are verified: we lived them for weeks. Whether the new direction is right is still being tested. All I can claim today is the shape of the failure, so that's where this post stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;p&gt;If you already run a memory layer, check five things.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Are activity logs or session summaries piling up in memory?&lt;/li&gt;
&lt;li&gt;Is "next step" or a decision's rationale stored in memory? (The tracker and the PR are their homes.)&lt;/li&gt;
&lt;li&gt;Does every stored rule carry its trigger and its reason?&lt;/li&gt;
&lt;li&gt;Is there an explicit way to retire a memory that turned out wrong?&lt;/li&gt;
&lt;li&gt;When you search, is the first result actually the memory you wanted?&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Keep Your CLAUDE.md Light (and Why More Rules Make Agents Worse)</title>
      <dc:creator>John Lee</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:18:28 +0000</pubDate>
      <link>https://dev.to/johnonlee/how-to-keep-your-claudemd-light-and-why-more-rules-make-agents-worse-34fa</link>
      <guid>https://dev.to/johnonlee/how-to-keep-your-claudemd-light-and-why-more-rules-make-agents-worse-34fa</guid>
      <description>&lt;p&gt;Every serious coding-agent user's CLAUDE.md walks the same road. It starts empty. Every incident adds a line. One day you look up and it's 500 lines long.&lt;/p&gt;

&lt;p&gt;Then something strange happens. The more rules you add, the fewer rules the agent follows. You wrote it down, clearly, and it gets ignored. So you write the same rule again, louder. &lt;strong&gt;Bold&lt;/strong&gt; it, add an exclamation mark, prefix it with "ALWAYS". It still gets ignored.&lt;/p&gt;

&lt;p&gt;This guide is about what to take out of your CLAUDE.md, not what to put in. The short version: a good CLAUDE.md is a short constitution, not a long rulebook. If your tool reads AGENTS.md instead, the same principles apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the file gets heavy
&lt;/h2&gt;

&lt;p&gt;The path is always the same. The agent makes a mistake. You add a rule. It makes a different mistake. You add another.&lt;/p&gt;

&lt;p&gt;Adding always feels justified in the moment: an incident just happened, and this sentence will prevent it. The problem is the other direction: the moment to delete a rule never arrives. You can't easily tell that deleting helped, and the damage of keeping everything accumulates slowly.&lt;/p&gt;

&lt;p&gt;So a heavy CLAUDE.md isn't a sign of laziness. The incentives only point one way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why heavy means worse
&lt;/h2&gt;

&lt;p&gt;Three effects stack up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context is a fixed cost.&lt;/strong&gt; Your CLAUDE.md rides along in every session, every turn. At 500 lines, you pay for 500 lines on every request. This isn't only about token pricing: it's attention taken off the actual task, prepaid. I wrote about the caching and context-cost mechanics in &lt;a href="https://john.onlee.io/en/blog/token-economics/" rel="noopener noreferrer"&gt;Token Economics&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Emphasis dilutes as it spreads.&lt;/strong&gt; With 5 rules, each one carries weight. With 50, each carries a fiftieth. Emphasizing everything is the same as emphasizing nothing. In a document with ten ALWAYSes, the eleventh is decoration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contradictions get resolved silently.&lt;/strong&gt; A rulebook that grows for months accumulates clauses that collide. If "always write tests first" and "make only the minimal requested change" live in the same file, the model doesn't report the conflict. It quietly drops one side — and which side varies by the day. A good share of "the agent ignored my rule" moments are actually this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stays, what goes
&lt;/h2&gt;

&lt;p&gt;Three kinds of things deserve to stay.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What can't be learned from the code or docs&lt;/strong&gt;: why this architecture was chosen, team habits written down nowhere else&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What applies to every session unconditionally&lt;/strong&gt;: tone, language, commit conventions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety boundaries&lt;/strong&gt;: anything that's hard to undo, like pushing to main or touching production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four kinds of things should go.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anything readable from the code (directory tours, function lists)&lt;/li&gt;
&lt;li&gt;One-off instructions whose task is long finished&lt;/li&gt;
&lt;li&gt;Procedures needed only in specific situations (next section decides where they move)&lt;/li&gt;
&lt;li&gt;Things the model is already good at ("write clean code")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The litmus test is one line: &lt;strong&gt;"If I delete this, what concretely gets worse in the next session?"&lt;/strong&gt; No specific answer, no seat in the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the removed lines go
&lt;/h2&gt;

&lt;p&gt;Being told to delete feels risky. Those rules are incident records. Can you afford to lose them?&lt;/p&gt;

&lt;p&gt;You're not losing them. You're rehousing them. If CLAUDE.md is the constitution, everything else is statute. Keep identity and principles in the constitution; move concrete clauses somewhere that loads only when needed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Situational procedures → skills (commands).&lt;/strong&gt; "Deploy in this order" has no business riding in every turn. Loading when someone calls &lt;code&gt;/deploy&lt;/code&gt; is enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mechanical repetition → hooks.&lt;/strong&gt; "Run lint before committing" is not a rule, it's automation. A hook fires regardless of what the model remembers. Deciding what to trust to memory versus machinery is the core move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accumulated knowledge → memory.&lt;/strong&gt; "Last time this bug was fixed like so" is not a rule, it's a memory. File-based or a dedicated tool, there should be a separate layer you search and retrieve from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project facts → docs next to the code.&lt;/strong&gt; Architecture explanations belong in README and docs. The agent reads them when it needs them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once this structure exists, every new rule request comes with a question: constitution or statute? It's almost always statute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and after
&lt;/h2&gt;

&lt;p&gt;A typical before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# CLAUDE.md (excerpt, before the diet, ~500 lines total)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Always respond in Korean
&lt;span class="p"&gt;-&lt;/span&gt; Helper functions live in src/utils. UI components in src/components,
  API clients in src/api, hooks in src/hooks... (40 lines of structure tour)
&lt;span class="p"&gt;-&lt;/span&gt; ALWAYS run npm run lint before committing
&lt;span class="p"&gt;-&lt;/span&gt; Deployment MUST follow: 1) test 2) build 3) staging check 4) ...
&lt;span class="p"&gt;-&lt;/span&gt; Do not edit config.ts directly (see incident, 2026-05-12)
&lt;span class="p"&gt;-&lt;/span&gt; Write good commit messages
&lt;span class="p"&gt;-&lt;/span&gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And after (this is the whole file, not an excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# CLAUDE.md (after the diet, complete)&lt;/span&gt;

&lt;span class="gu"&gt;## Identity&lt;/span&gt;
Speak Korean, like a colleague. Conclusions first.

&lt;span class="gu"&gt;## Boundaries&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Never push directly to main. Ask before committing.
&lt;span class="p"&gt;-&lt;/span&gt; Production config files are read-only.

&lt;span class="gu"&gt;## Delegation&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Deployment: /deploy skill
&lt;span class="p"&gt;-&lt;/span&gt; Pre-commit lint: handled by a hook (not entrusted to model memory)
&lt;span class="p"&gt;-&lt;/span&gt; Project history and past incidents: search memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where the missing 490 lines went is the entire point of this guide. Structure tours moved to docs, deploy steps to a skill, lint to a hook, incident records to memory. Nothing was lost. Everything went home.&lt;/p&gt;

&lt;h2&gt;
  
  
  The maintenance routine
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When an incident happens, flip the order.&lt;/strong&gt; Not "let's add a rule" but "where does this belong" — skill, hook, or memory? CLAUDE.md is the last resort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diet on a schedule.&lt;/strong&gt; Look back over your last ten sessions and find the rules that never fired once. Anything that fails the litmus test gets deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a ceiling.&lt;/strong&gt; Mine is one screen. When the file starts scrolling, something needs to move out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;p&gt;Open your CLAUDE.md and check five things.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does every line make the next session concretely worse if deleted?&lt;/li&gt;
&lt;li&gt;Is there anything the code itself could answer?&lt;/li&gt;
&lt;li&gt;Does "ALWAYS" appear three times or more?&lt;/li&gt;
&lt;li&gt;Do any clauses collide?&lt;/li&gt;
&lt;li&gt;Does it fit on one screen?&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Customizing Hugo PaperMod Without Forking the Theme</title>
      <dc:creator>John Lee</dc:creator>
      <pubDate>Wed, 05 Aug 2026 09:45:53 +0000</pubDate>
      <link>https://dev.to/johnonlee/customizing-hugo-papermod-without-forking-the-theme-4c98</link>
      <guid>https://dev.to/johnonlee/customizing-hugo-papermod-without-forking-the-theme-4c98</guid>
      <description>&lt;p&gt;Sites that start from a stock theme tend to look like it. This one did too — until recently it was the default &lt;a href="https://github.com/adityatelange/hugo-PaperMod" rel="noopener noreferrer"&gt;PaperMod&lt;/a&gt; screen. The editorial homepage and unified article styling you're looking at came out of a single day of customization. This guide is that work written down, with the code.&lt;/p&gt;

&lt;p&gt;One principle drove all of it: &lt;strong&gt;never fork the theme.&lt;/strong&gt; The theme stays a submodule and keeps receiving updates; you win with site-level files only. Hugo resolves same-path site files ahead of theme files, which makes this possible. The entire customization of this site is a handful of files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;layouts/index.html              ← full homepage replacement
data/home/ko.yaml, en.yaml      ← homepage copy (per language)
assets/css/extended/home.css    ← homepage styles
assets/css/extended/custom.css  ← unifying every other page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The starting point is a Hugo site with PaperMod as a submodule, deployed to GitHub Pages. Basic installation is well covered by the &lt;a href="https://github.com/adityatelange/hugo-PaperMod/wiki/Installation" rel="noopener noreferrer"&gt;PaperMod wiki&lt;/a&gt;, so I'll skip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Replace the homepage wholesale
&lt;/h2&gt;

&lt;p&gt;Create a single &lt;code&gt;layouts/index.html&lt;/code&gt; and the homepage is yours. The theme's home template stays untouched.&lt;/p&gt;

&lt;p&gt;The key move: don't hardcode copy into the markup — pull it from data files. On a bilingual site, one template then serves both languages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{{- $copy := index .Site.Data.home .Site.Language.Lang -}}
{{- $posts := first 4 (where .Site.RegularPages.ByDate.Reverse "Section" "blog") -}}

&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"editorial-hero"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"editorial-eyebrow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ $copy.hero.eyebrow }}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;{{ range $i, $line := $copy.hero.titleLines }}{{ if $i }}&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;{{ end }}{{ $line }}{{ end }}&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"editorial-intro"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ $copy.hero.intro }}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;data/home/en.yaml&lt;/code&gt; holds nothing but words:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;hero&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;eyebrow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IDEAS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;·&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PRODUCTS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;·&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;OPPORTUNITIES"&lt;/span&gt;
  &lt;span class="na"&gt;titleLines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Where&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ideas&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;become&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;products,"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;products&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;become&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;opportunities."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy edits stop requiring template changes, and adding a language is one more yaml file. Latest posts are pulled dynamically as above — handle the &lt;code&gt;{{ else }}&lt;/code&gt; empty state so a section with no content degrades gracefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Unify every other page with CSS variables
&lt;/h2&gt;

&lt;p&gt;You don't need to rebuild article and list pages. PaperMod drives all of its colors through CSS variables, so swapping the variables re-tones the whole site. Anything under &lt;code&gt;assets/css/extended/&lt;/code&gt; is bundled automatically — no theme edits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fbfaf7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c"&gt;/* paper background */&lt;/span&gt;
  &lt;span class="py"&gt;--entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f1f3f2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1b1d1f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#676b70&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#dddcd7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;:root&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"dark"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#151617&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f0efeb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#a9adb2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#333638&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Putting just the display type in a serif gets you half the editorial impression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.post-title&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.page-header&lt;/span&gt; &lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.entry-header&lt;/span&gt; &lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--j-serif&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One trap when flattening the card-style list entries into rules-only rows: PaperMod's &lt;code&gt;.post-entry&lt;/code&gt; carries &lt;code&gt;border: 1px solid&lt;/code&gt; on &lt;strong&gt;all four sides&lt;/strong&gt;, so overriding only &lt;code&gt;border-bottom&lt;/code&gt; leaves a ghost outline on the other three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.post-entry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                &lt;span class="c"&gt;/* kill all four first */&lt;/span&gt;
  &lt;span class="nl"&gt;border-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c"&gt;/* then bring back the bottom */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Dark mode is data-theme (not body.dark)
&lt;/h2&gt;

&lt;p&gt;Older PaperMod customization examples on the internet hang dark styles off &lt;code&gt;body.dark&lt;/code&gt;. &lt;strong&gt;On current PaperMod they silently do nothing.&lt;/strong&gt; The theme now sets a &lt;code&gt;data-theme&lt;/code&gt; attribute on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; via JS. This site hit exactly that trap at first — dark mode was dead on arrival.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"dark"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* dark palette */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* No-JS visitors stay on data-theme="auto" — follow their system setting */&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"auto"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;/* the same dark palette */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check all three modes: light, dark, and system-auto.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. For Korean text, start with keep-all
&lt;/h2&gt;

&lt;p&gt;On Korean sites, mid-word line breaks ("1,200포\n인트") are fixed with one line of CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;word-break&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keep-all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c"&gt;/* Korean: never break inside a word */&lt;/span&gt;
  &lt;span class="nl"&gt;overflow-wrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;break-word&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* escape hatch for long URLs */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;keep-all&lt;/code&gt; only affects CJK text, so Latin is untouched. Put it on &lt;code&gt;body&lt;/code&gt;, not just article content — titles, lists, and the homepage need it too.&lt;/p&gt;

&lt;p&gt;A note on serif fonts: Korean serif fallbacks differ wildly per device: calligraphic on macOS, Batang on Windows, plain sans on Android. If cross-device consistency matters to your brand, self-host a subsetted Noto Serif KR woff2. This site currently accepts the fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Two things search engines read — handle them early
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Titles.&lt;/strong&gt; Repetitive titles that differ only by date ("Morning Brief — 2026-08-01") sink in lists and in search results. This site retitled its auto-published posts to each day's top headline; the date already lives in the metadata and URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Summaries.&lt;/strong&gt; Without a &lt;code&gt;summary&lt;/code&gt; in frontmatter, Hugo cuts the start of the body into the meta description. If a post opens with a table, &lt;strong&gt;table cells end up verbatim in your search snippet&lt;/strong&gt; — that actually happened on this site. Writing one or two sentences per post is the cheapest fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Two&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;requests&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;near-identical&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tokens,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;46x&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;gap.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;How&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;caching&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;really&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;works…"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bonus: the site-verification meta tags for search consoles are built into PaperMod — just add the values to &lt;code&gt;hugo.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;google&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;SiteVerificationTag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;…"&lt;/span&gt;
    &lt;span class="na"&gt;naver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;SiteVerificationTag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;…"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pre-deploy checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Light / dark / system-auto, all three modes&lt;/li&gt;
&lt;li&gt;Mobile (grids collapse to one column)&lt;/li&gt;
&lt;li&gt;Both language homepages render&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hugo --minify&lt;/code&gt; builds clean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full source of this site is public: &lt;a href="https://github.com/JohnOnLee/JohnOnLee" rel="noopener noreferrer"&gt;github.com/JohnOnLee/JohnOnLee&lt;/a&gt;. Every snippet in this guide is there, running, in context. If you get stuck, open an issue on the &lt;a href="https://github.com/JohnOnLee/JohnOnLee/issues" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
