<?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: Paul Grimshaw | bounded.dev</title>
    <description>The latest articles on DEV Community by Paul Grimshaw | bounded.dev (@boundeddev).</description>
    <link>https://dev.to/boundeddev</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%2F3844560%2F1745c0d2-bafd-426b-b234-7bababa270c7.png</url>
      <title>DEV Community: Paul Grimshaw | bounded.dev</title>
      <link>https://dev.to/boundeddev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/boundeddev"/>
    <language>en</language>
    <item>
      <title>Disposable Sessions: Keep the Work, Not the Session</title>
      <dc:creator>Paul Grimshaw | bounded.dev</dc:creator>
      <pubDate>Mon, 20 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/boundeddev/disposable-sessions-keep-the-work-not-the-session-2o26</link>
      <guid>https://dev.to/boundeddev/disposable-sessions-keep-the-work-not-the-session-2o26</guid>
      <description>&lt;p&gt;Count the agent sessions you have open right now. If you work the way most people I talk to work, it is somewhere between six and fifteen, and you could not tell me what is in more than half of them. You will not close them either, because one of them might contain something: a decision you talked yourself into, a file you never committed, a piece of reasoning you would have to rebuild from scratch. So they sit there, and the pile grows, and every one of them is a small unpaid debt.&lt;/p&gt;

&lt;p&gt;The question that sorts this out is short enough to keep in your head:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Could you delete this session right now and lose nothing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is yes, close it, and feel nothing about it. If the answer is no, that is not a reason to keep the session. It is a signal that something inside it is sitting in the one place in your entire setup that has no backups, no history, no review, and no way for anyone else to reach it.&lt;/p&gt;




&lt;h2&gt;
  
  
  A session is another working copy
&lt;/h2&gt;

&lt;p&gt;There is a whole genre of question forming around this, and once you notice it you see it everywhere. How do I back up my sessions? How do I restore one from last month? How do I move a session from the terminal into the desktop app? There are now tools whose whole purpose is to persist session history into your repository so none of it is ever lost.&lt;/p&gt;

&lt;p&gt;And every few weeks comes the story that gives the genre its urgency: someone lost two days of work because a session was flagged, or expired, or was killed for looking like a security risk, and the post is a complaint about the vendor doing that to them.&lt;/p&gt;

&lt;p&gt;The sympathy is genuine, losing work is horrible, and I have been there. But the conclusion being drawn is the wrong one. If two days of work can be destroyed by one session ending, the problem is not that the session ended.&lt;/p&gt;

&lt;p&gt;We solved a version of this before. Everyone has lost a day to a dead laptop or a bad rebase, and the response was never to work out how to back up a working directory. It was to commit early, push often, and keep as little irreplaceable state in the working copy as possible, until the discipline became invisible and we stopped calling it a discipline at all. A session is another working copy, and right now it is the one part of the setup with no history, no remote, and no way for anyone else to reach it.&lt;/p&gt;

&lt;p&gt;Widen that back out to the genre and the same thing is true of all of it. Compaction squeezes the session so it can live longer. Summarisation writes a shorter version of it. Handoff skills dump it to a file so a new one can pick it up. Backup, restore, migrate, resume, and agent memory all work to keep the artefact itself alive and portable. Some of these are genuinely useful and I use a couple of them, but taken together as an answer they share an assumption I think is wrong, which is that the session is a thing worth preserving at all.&lt;/p&gt;

&lt;p&gt;I do not want better session management. I want sessions I do not care about. The only ones that should hold any value are the ones currently open and in progress, and even those should be one quick status check away from being closed and forgotten. If you find yourself asking how to back up a session, the more useful question is what is in there that you would miss, and why it is still in there.&lt;/p&gt;




&lt;h2&gt;
  
  
  A session should do one thing
&lt;/h2&gt;

&lt;p&gt;We already know how to handle this, we just have not applied it here yet.&lt;/p&gt;

&lt;p&gt;A function should do one thing. A class should have one responsibility. A module should have one reason to change. We hold these not because small is aesthetically nicer but because a small, scoped thing is easy to reason about, easy to replace, and cheap to throw away.&lt;/p&gt;

&lt;p&gt;A session is the same kind of unit, and it should follow the same rule. One session, one job, and the first prompt is where you set it. When the job is done, the session is done, and you close it without ceremony because there is nothing in there that you did not already put somewhere better.&lt;/p&gt;

&lt;p&gt;The reason people end up with a five-hour session holding four unrelated threads is exactly the reason people end up with a two-thousand-line god class: nobody drew the boundary at the start, and then every new thing had somewhere obvious to go. It happens by drift, not by decision.&lt;/p&gt;

&lt;p&gt;And this is genuinely hard, harder than the principle makes it sound. A good session with an agent constantly throws off side thoughts. You go in to fix a caching bug and come out having realised something about your deployment pipeline. That is one of the best properties of working this way and I would not want to suppress it. But those side thoughts are the exact material that turns a single-purpose session into a container of unfinished business, so they have to be routed somewhere rather than parked in the scrollback where they will die.&lt;/p&gt;




&lt;h2&gt;
  
  
  The work is building the homes
&lt;/h2&gt;

&lt;p&gt;Which is where the real effort goes, and it is not the effort people expect.&lt;/p&gt;

&lt;p&gt;Getting things out of the session is exactly the goal, so the question is only ever &lt;em&gt;when&lt;/em&gt; and &lt;em&gt;where to&lt;/em&gt;. Left as one lump at the end, it fails, because at that point you are tired, the session is long, and you have to invent a location for each thing before you can move it. You will do it three times out of ten, and the session becomes a store of record by default, because that was the path of least resistance.&lt;/p&gt;

&lt;p&gt;So the work is upstream. Your repositories and your projects have to already have a home for every kind of thing a session can produce, so that landing it is a reflex rather than a decision. A thought that is not ready yet goes into a technical note, which is &lt;a href="https://dev.to/blog/everything-is-a-technical-note"&gt;a system built precisely so that parking a half-formed idea costs nothing&lt;/a&gt;. A decision goes into a decision record. Work in progress goes into a commit on a branch. A thing you should do later goes into the tracker. None of this is novel, and that is the point: the homes are ordinary, they just have to exist and be cheap to reach before you need them.&lt;/p&gt;

&lt;p&gt;Get that right and disposability stops being a discipline you impose on yourself at the end of each session. It becomes a property you already have, because everything worth keeping went out of the session as it was produced, continuously, and what is left in the window is genuinely just conversation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Seeing what a session is holding
&lt;/h2&gt;

&lt;p&gt;I still need to see that, though, because I am not a machine and I do not route everything perfectly, and some of these sessions have been sitting for four days.&lt;/p&gt;

&lt;p&gt;So I built a small skill called &lt;strong&gt;flight status&lt;/strong&gt; , and &lt;a href="https://github.com/bounded-dev/skills" rel="noopener noreferrer"&gt;it is on GitHub&lt;/a&gt; if you want it. It is a read-only snapshot of the current session that flags anything the session has produced but not yet put anywhere durable. It sorts what it finds into the two ways this work actually gets lost:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔴 Only in the chat.&lt;/strong&gt; A decision, a draft, a conclusion, a follow-up that exists nowhere but the conversation. Gone the moment the session closes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🟠 On disk, but not landed.&lt;/strong&gt; Uncommitted changes, untracked files, unpushed commits, stashes, files written to a scratchpad outside the repo. It survives the session, but it is not durable and nobody else can see it.&lt;/p&gt;

&lt;p&gt;It prints a banner, one line per outstanding item. Here is that caching session from earlier, the one that went looking for a bug and came back with opinions about the deploy pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;────────── 🛬 FLIGHT STATUS 🛬 ──────────
🔴 cache invalidation approach — settled on TTL plus explicit purge, not in any ADR
🔴 deploy pipeline race — spotted mid-session, not raised as an issue
🟠 cache-key refactor — uncommitted in service-api
🟠 load test numbers — written to scratchpad, outside the repo
──────────────────────────────────────────────

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, on a good day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;────────── 🛬 FLIGHT STATUS 🛬 ──────────
🟢 All captured. Safe to close.
──────────────────────────────────────────────

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a handoff. It does not extract the session, summarise it, or write anything at all; it is strictly read-only, and the whole design goal is that it tells you the truth about the session rather than making a copy of it.&lt;/p&gt;

&lt;p&gt;I run it in three situations. Coming back cold to a session I opened on Tuesday and have no memory of, where it answers the only question I have, which is whether I can just close this. Before stepping away from something, as a last check. And, increasingly, in the middle of a session that feels like it has got away from me, because if flight status comes back with four unrelated things in flight, the session has quietly taken on more than one job and the honest fix is to land them and start again with a smaller scope. That last use is the one I did not expect when I wrote it. It turns out to be a scope-creep detector as much as an exit check.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing has to be as cheap as opening
&lt;/h2&gt;

&lt;p&gt;Opening a session is essentially free now, and the tooling has gone a long way to make it so. Closing one has had less attention, and that asymmetry is what produces the graveyard of tabs.&lt;/p&gt;

&lt;p&gt;The friction of closing is not effort, it is doubt. You cannot remember what is in there, so the safe move is to keep it, and then you keep the next one, and now you have eleven open and you are afraid of all of them. Removing that doubt is most of the work, which is really all flight status does.&lt;/p&gt;

&lt;p&gt;To be clear about the nuance: long-lived is fine. Sessions sit for days because you got pulled onto something else, and age is not the problem. A three-day-old session with everything already landed is free to delete at any moment, which is precisely the point. The problem is never how old a session is, it is whether it is holding state that exists nowhere else.&lt;/p&gt;

&lt;p&gt;That is the whole thing, and it fits in one question. Could you delete this session right now and lose nothing? Build your workflow so that the answer is always yes.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>productivity</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Write It Down, Sort It Later</title>
      <dc:creator>Paul Grimshaw | bounded.dev</dc:creator>
      <pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/boundeddev/write-it-down-sort-it-later-41i4</link>
      <guid>https://dev.to/boundeddev/write-it-down-sort-it-later-41i4</guid>
      <description>&lt;p&gt;A lot of my thinking now happens in conversation with an agent. I open a problem, talk it through, let it push back, and because I start the conversation &lt;em&gt;inside the project repository&lt;/em&gt;, it already has the context: my past thinking on the topic, the code, the architecture, the decision records. It is not a blank assistant, it is one that knows the project. Do this across a whole team and the value compounds.&lt;/p&gt;

&lt;p&gt;Which leaves the one question that has stalled me more often than I would like: wherever the conversation gets to, where does that thinking go? Getting it written down was never the hard part. Deciding what it is, which folder it belongs in, and what to call it, that is where I would freeze.&lt;/p&gt;




&lt;h2&gt;
  
  
  The friction is never the writing
&lt;/h2&gt;

&lt;p&gt;I decide I need to introduce authentication into a project. I talk it through with an agent, get it to grill me on what I have not considered, and we reach some conclusions. Good. Now: what is this and where should it live?&lt;/p&gt;

&lt;p&gt;It is not a spec yet, it is more of an exploration, so I drop it in some "explore" folder. A few days later I pick it up and it has firmed into something closer to a spec. Later still I break it into steps, and now it is a plan. Every time I come back I am not just working on the problem, I am grappling with what this document &lt;em&gt;is&lt;/em&gt;, which folder it belongs in, and what it should be called.&lt;/p&gt;

&lt;p&gt;And once I had picked a folder, I would want it to match the &lt;em&gt;format&lt;/em&gt;: does this fit the spec template, have I filled in every heading, is the structure right?&lt;/p&gt;

&lt;p&gt;The doorway costs more than the room.&lt;/p&gt;




&lt;h2&gt;
  
  
  Types are metadata, not locations
&lt;/h2&gt;

&lt;p&gt;This is the solution I landed on. A document's type is not fixed: the auth note was an exploration, then a spec, then a plan, the same thinking throughout at different stages of maturity. A system that makes you commit to a folder and a template up front is fighting that, because every folder is a bet on a classification that is about to change.&lt;/p&gt;

&lt;p&gt;So stop deciding up front. Everything is a Technical Note.&lt;/p&gt;

&lt;p&gt;A TN is one primitive. It has a &lt;code&gt;status&lt;/code&gt;, a &lt;code&gt;kind&lt;/code&gt;, an incremental number, and a link to the issue it came from. The &lt;code&gt;kind&lt;/code&gt; is a loose label, closest fit rather than a rule: &lt;code&gt;exploration&lt;/code&gt;, &lt;code&gt;spec&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;proposal&lt;/code&gt;, &lt;code&gt;research&lt;/code&gt;. When the auth note graduates from exploration to spec, I change one word, and the note does not move, does not get renamed, keeps its number and its history. There is no template to satisfy, and a note can be a single bullet.&lt;/p&gt;

&lt;p&gt;Parking a thought becomes cheap. Wherever a conversation with an agent gets to, I drop the result into a TN and move on, and later I open it again, say "grill me further on this," and it grows. The note is a place the thinking rests between sessions, not a document I owe anyone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Nothing lives in human (or agents) memory
&lt;/h2&gt;

&lt;p&gt;The organising principle of working this way is that nothing important should live in someone's head, or in a chat log, or in an external tool that is not in the repository. Everything about a project, aligned in one place, so the whole team and every agent can see all of it. A thought I had about CI/CD that only exists in my memory is a thought the agent cannot use and a teammate cannot find.&lt;/p&gt;

&lt;p&gt;So the moment a thought forms, it goes in, at whatever level of completeness it happens to have. "I have been thinking about CI/CD, draft that into a TN." Now it is a &lt;code&gt;draft&lt;/code&gt;, out of my head and in the repo, and the next person to touch that area, human or agent, finds it.&lt;/p&gt;

&lt;p&gt;It is also why I turn the agent's own memory off. Most frameworks now offer one, Claude Code included, and they will quietly file away what they learn about a project into a store that lives on my machine. That store is invisible to everyone else: it does not travel to a teammate's checkout, it does not survive me switching machines, and it means nothing to a different agent tomorrow. The knowledge the agent is leaning on has gone somewhere no one else can read or review. Written into a TN instead, the same fact sits in the repo, versioned, and there for every person and every agent that clones it. If it matters to the project, I want it in the project, not in a sidecar owned by one machine and one vendor.&lt;/p&gt;




&lt;h2&gt;
  
  
  If in doubt, write it down
&lt;/h2&gt;

&lt;p&gt;The advice underneath this is plain: stop caring about structure, and if in doubt, write it down. There is no thought about the project too small or too unfinished to belong in the repo. The bar for starting a note is that you had a thought worth not losing, and that is the whole bar. It can be a couple scrappy bullets marked &lt;code&gt;draft&lt;/code&gt;, and it still counts, because even that beats the thought living in your head until the day you or a colleague need it and cannot find it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What keeps it from being a swamp
&lt;/h2&gt;

&lt;p&gt;If everything goes in, the fair worry is that the folder silts up into a swamp. Two small things I've found help with that.&lt;/p&gt;

&lt;p&gt;The first is &lt;code&gt;status&lt;/code&gt;. Every note says whether to trust it: &lt;code&gt;draft&lt;/code&gt; is thinking out loud, &lt;code&gt;active&lt;/code&gt; is agreed, &lt;code&gt;superseded&lt;/code&gt; points you at whatever replaced it. That one field does most of the work, because it is what lets you write freely; half-formed ideas can sit in the open without being mistaken for settled decisions, and when a note finally hardens into something the project has to obey, it graduates out into a proper decision record. Nothing is deleted along the way, it just changes state.&lt;/p&gt;

&lt;p&gt;The second is a number. Each note gets one, which gives it an identity to link to and gives the pile an order you can read top to bottom as the project's history. That is close to all the structure there is.&lt;/p&gt;

&lt;p&gt;It is also what an agent needs. It reads &lt;code&gt;status&lt;/code&gt; to know what it is allowed to act on, follows a &lt;code&gt;superseded&lt;/code&gt; pointer to the current thinking, and starts from the index instead of swallowing the whole folder. The thing that makes it cheap for me to park a thought at 11pm is the same thing that lets an agent pick it up the next morning or in a fortnight and trust the right parts of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do leave out
&lt;/h2&gt;

&lt;p&gt;There are a couple distinctions I do keep. The notes hold knowledge about the project: what it is, and how its thinking got to where it is. They are not a record of the work itself, items on the roadmap, where a task stands, who is on it, what is left to do. Currently I keep that in a agent managed issue tracker, and I am content to leave it there for now.&lt;/p&gt;

&lt;p&gt;I also have an explicit process to graduate TN's that encompass technical decisions (architectural and conventional) into a separate ADR/CDR documents, which have a known format. These are cross referenced to the TN system, but I use these additionally for agent guardrails (e.g. an "Architect reviewer" subagent blocks any new feature that is introduces new concepts or violates existing DRs).&lt;/p&gt;

&lt;h2&gt;
  
  
  A well-worn pattern
&lt;/h2&gt;

&lt;p&gt;Keeping project knowledge as markdown in a git repo is commonplace, especially nowadays, and it is settling into shared conventions: Google's Open Knowledge Format, llms.txt, the broader LLM-wiki idea. That so many people arrive at "markdown with a little front matter, versioned in git, readable by people and agents" is a fair sign it is the natural shape rather than a clever one.&lt;/p&gt;

&lt;p&gt;What I would like to see more discussion on is the process of storing information: what, when how and where. How should a piece of thinking move through its life, from a half-formed draft, to something agreed, to something superseded when a better idea replaces it.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>productivity</category>
      <category>writing</category>
    </item>
    <item>
      <title>It's (Still) All About Boundaries</title>
      <dc:creator>Paul Grimshaw | bounded.dev</dc:creator>
      <pubDate>Wed, 10 Jun 2026 18:45:02 +0000</pubDate>
      <link>https://dev.to/boundeddev/its-still-all-about-boundaries-2e7m</link>
      <guid>https://dev.to/boundeddev/its-still-all-about-boundaries-2e7m</guid>
      <description>&lt;p&gt;Why boundaries (module structure, clean interfaces, and strict types) are the single most important factor in whether AI-assisted development actually works.&lt;/p&gt;

&lt;p&gt;There is one thing that separates a codebase you can move quickly in from one that slowly suffocates your team. It is not the language, the framework, the cloud provider, or the number of tests. It is boundaries.&lt;/p&gt;

&lt;p&gt;This has always been true. In the age of AI, it is more true than ever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spaghetti
&lt;/h2&gt;

&lt;p&gt;Picture a bowl of spaghetti. Now picture each strand as an artery (sorry, a little gruesome, I know), each one carrying blood, performing some vital function, connected at both ends to something important.&lt;/p&gt;

&lt;p&gt;You are a surgeon who needs to operate on one section of this mass. You draw a circle around the area you are working on. Immediately you see a dozen arteries crossing the boundary, disappearing into the tangle in every direction. To understand what your section does, you have to trace each one. You follow an artery out, arrive somewhere else in the system, and now you have to understand what happens there too. Then you follow the next. And the next.&lt;/p&gt;

&lt;p&gt;By the time you have traced enough to feel confident, you have accumulated an enormous amount of context. And here is the brutal reality: you cannot hold it all at once. The parts you understood first have already started to fade. You are perpetually chasing a complete picture you can never quite assemble.&lt;/p&gt;

&lt;p&gt;And when the system needs to grow? Nobody wants to reach into the tangle. So the path of least resistance is to graft something onto the edge: a new piece bolted on the outside, with arteries spliced into various points in the existing mass. It works. It ships. And the tangle gets a little bigger, a little harder to trace, every time.&lt;/p&gt;

&lt;p&gt;This is a codebase without boundaries. This is why engineering teams slow down. Not because the developers get worse, but because the system gets bigger and more entangled, and the cognitive cost of making any single change becomes prohibitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spotlight
&lt;/h2&gt;

&lt;p&gt;Think of your working memory as a spotlight. Wherever you shine it, things brighten: you understand them, you can reason about them. Move it to illuminate a new area and the places you just lit start to dim. You can widen the beam, but only at the cost of less brightness everywhere. This is not a flaw in any particular engineer. It is how human cognition works.&lt;/p&gt;

&lt;p&gt;Good software architecture is, at its core, a set of techniques for keeping the required size of the spotlight small.&lt;/p&gt;

&lt;p&gt;If the part of the system you are changing has a clean, narrow interface (a small surface area), and the modules around it are equally well-defined, then your spotlight only needs to cover a small area. You understand your module. You understand the interface it exposes. You understand the interfaces it depends on. You do not need to understand the internals of anything else, because the boundaries guarantee those internals cannot surprise you.&lt;/p&gt;

&lt;p&gt;Return to our arterial tangle: boundaries are the equivalent of a surgeon operating on an organ with clearly defined blood vessels in and out, rather than reaching into a mass of undifferentiated tissue. Same arteries, same vital functions, but now there are only a handful crossing your boundary, and each one has a known purpose.&lt;/p&gt;

&lt;p&gt;The smaller and shorter-lived your spotlight, the more essential these boundaries become.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Is Not New
&lt;/h2&gt;

&lt;p&gt;Bounded modules with strictly defended interfaces are not a new idea. Eric Evans wrote Domain-Driven Design in 2003. Robert Martin has been articulating clean architecture for decades. The principles behind clean architecture, layered architecture, and hexagonal architecture patterns all converge on the same insight: keep modules focused, keep interfaces narrow, keep internals hidden.&lt;/p&gt;

&lt;p&gt;The industry broadly agrees this is the right approach. And yet many production codebases are the arterial tangle.&lt;/p&gt;

&lt;p&gt;Why? Because designing good boundaries is remarkably hard. It requires deep understanding of the domain: knowing where the natural seams are, which concepts belong together, and which should be kept apart. Get the boundaries wrong and you end up with modules that are constantly reaching into each other, or abstractions that fight the grain of the problem. Bad boundaries can be worse than no boundaries: they add indirection without reducing complexity.&lt;/p&gt;

&lt;p&gt;And even when boundaries start well, they erode. Every shortcut (reaching into another module’s internals, leaking implementation details through a public interface, adding a dependency because it is convenient) widens the surface area a little. Over time, those erosions compound. The spotlight has to grow. Eventually you are back in the tangle.&lt;/p&gt;

&lt;p&gt;This is not a reason to avoid boundaries. It is a reason to treat them as a discipline. Keep modules small and focused. Expose only what needs to be exposed. Treat every public interface as a contract. Enforce it. When you need to make a change that crosses a boundary, reason about interfaces, not internals.&lt;/p&gt;

&lt;p&gt;And layer it up. Individual adapters have boundaries. Modules have boundaries. Subsystems have boundaries. At each zoom level, you should be able to understand what you are looking at without descending into the level below. This is how systems remain comprehensible as they grow.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Makes This Harder
&lt;/h2&gt;

&lt;p&gt;Now introduce an AI coding agent.&lt;/p&gt;

&lt;p&gt;The spotlight problem does not get better. It gets dramatically worse.&lt;/p&gt;

&lt;p&gt;An AI agent’s spotlight is smaller than a human’s, and it fades faster. Models have a context window: a hard limit on how much they can hold at once. Beyond a certain size, adding more context does not help; it actively hurts. Quality degrades. The model loses track of constraints from earlier in the prompt and starts making decisions that are locally coherent but globally broken.&lt;/p&gt;

&lt;p&gt;Where a human developer’s understanding at least persists across a working day, an AI agent starts every task completely fresh. A developer on a team accumulates context over months and years: the discussions, the failed experiments, the decisions that were reversed. They carry a mental model built from thousands of hours of exposure. An AI agent has none of that. Every time it picks up a task it is the first day on the job, with a smaller spotlight than the engineer it is assisting and one that dims the moment the task ends.&lt;/p&gt;

&lt;p&gt;For all the progress in AI models, reasoning well across a large context window is still something they struggle with. Without a fundamental breakthrough, that is unlikely to change soon.&lt;/p&gt;

&lt;p&gt;The implication is stark: hand an AI agent a poorly-bounded system and ask it to make a change, and you will get code that looks plausible but breaks things in non-obvious ways. The model cannot trace all the arteries. It does not know what it does not know. It will write confident, syntactically correct, tests-pass code that quietly does the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution Is Still Boundaries
&lt;/h2&gt;

&lt;p&gt;The solution to the AI context problem is the same solution that existed before AI. It is not a silver bullet. As we have seen, getting boundaries right is genuinely hard work. But it is the most effective tool we have.&lt;/p&gt;

&lt;p&gt;Give an agent a bounded module with a clean interface. The module fits in context. Its dependencies are expressed as interfaces, not implementation details that need to be traced. The agent can understand what it is changing, reason about what will break, and produce correct output.&lt;/p&gt;

&lt;p&gt;At the system level, the agent can see how the main pieces connect without descending into each one. It can zoom into a subsystem and see what modules it comprises. It can zoom into a specific module and see its external interfaces without needing to understand the internals behind them.&lt;/p&gt;

&lt;p&gt;This is the same zoom-level reasoning that makes a well-bounded system comprehensible to a human engineer. It works for the same reasons. And when the spotlight is as small and short-lived as an AI agent’s, boundaries are the difference between a useful result and a confident mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard Boundaries: The Type System as Guardrail
&lt;/h2&gt;

&lt;p&gt;There is a second kind of boundary that matters in the AI era, beyond module structure.&lt;/p&gt;

&lt;p&gt;Hard, machine-enforced boundaries (your type system, your linter, your compiler) become extraordinarily valuable when AI is writing code. Not because the AI is careless, but because it should not need to hold certain things in its head at all.&lt;/p&gt;

&lt;p&gt;A strongly-typed codebase in strict mode tells the agent exactly what shape every value takes, what every function expects and returns, what is allowed and what is not. The agent does not have to guess or infer. It can lean on the type system the way a developer leans on IDE autocomplete: an authoritative source of truth that does not consume context to remember.&lt;/p&gt;

&lt;p&gt;This shrinks the effective spotlight dramatically. The agent can focus on intent and logic because the mechanics are already enforced by the system.&lt;/p&gt;

&lt;p&gt;Strict types. Enforced linting. Meaningful compile-time errors. In a codebase where AI agents are contributing, these are not hygiene concerns. They are load-bearing guardrails.&lt;/p&gt;

&lt;h2&gt;
  
  
  bounded.dev
&lt;/h2&gt;

&lt;p&gt;This is the founding principle behind everything at bounded.dev.&lt;/p&gt;

&lt;p&gt;The promise of AI-assisted development (genuine, sustainable speed) is only reachable in codebases with good structure. Boundaries throughout the system. Strictly defended module interfaces. Hard type and linting enforcement. Structured context that agents can read and reason about.&lt;/p&gt;

&lt;p&gt;Without those things, AI makes development faster in the short term and catastrophically messier in the long term. The tangle grows faster. The surface area expands. The system becomes less and less understandable, and the initial acceleration reverses.&lt;/p&gt;

&lt;p&gt;With them, you get something genuinely new: a codebase that a human and an AI can navigate together, at multiple levels of abstraction, without either needing to hold the whole system in focus at once. The speed of AI with the quality that good engineering has always produced.&lt;/p&gt;

&lt;p&gt;Boundaries are not a constraint on what AI can do. They are what makes AI assisted development actually work.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the first post on bounded.dev. Future posts will go deeper on specific techniques: how to structure context for AI agents, where the monolith/microservices line actually sits, and what a production-ready, AI-optimised project template looks like.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>ddd</category>
    </item>
    <item>
      <title>Design from the Leaves</title>
      <dc:creator>Paul Grimshaw | bounded.dev</dc:creator>
      <pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/boundeddev/design-from-the-leaves-8dc</link>
      <guid>https://dev.to/boundeddev/design-from-the-leaves-8dc</guid>
      <description>&lt;p&gt;Most codebases have a surface where new features get added: command handlers, query handlers, CRUD endpoints, repository methods, validators. Small, similar units, replicated every time someone builds something. Call them leaves.&lt;/p&gt;

&lt;p&gt;When an engineer adds another one, they open an existing leaf, copy it, rename a few things, and edit. This is not laziness. It is correct. The existing leaf is the most reliable spec of how a leaf should look here.&lt;/p&gt;

&lt;p&gt;Agents do the same. Give one a vague task and the first thing it does is grep for similar examples, read one or two, use them as a template. With a smaller spotlight than a human, an agent leans on this even harder. Whatever it finds at the leaves is what it will produce.&lt;/p&gt;

&lt;p&gt;So here is the principle: &lt;strong&gt;design your codebase from the leaves up.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The leaves are your design language
&lt;/h2&gt;

&lt;p&gt;Look at a leaf node and ask: if every new feature produces another one of these, am I happy with the shape it is in?&lt;/p&gt;

&lt;p&gt;Most leaf nodes I see in the wild are the wrong shape. Type annotations duplicated from the interface they implement. A constructor wiring up dependencies through a base class. A try/catch and a logger call identical to every other leaf. The actual &lt;em&gt;thing&lt;/em&gt; this leaf does, the one or two interesting lines, is buried in ceremony.&lt;/p&gt;

&lt;p&gt;The next engineer copies this. The agent copies this. The cycle continues.&lt;/p&gt;

&lt;p&gt;The discipline is the opposite. A leaf should read as if it has no boilerplate. The smallest possible declaration of what makes this leaf different from every other leaf. Nothing more.&lt;/p&gt;

&lt;p&gt;If a line of code appears in every command handler, it does not belong in any command handler. It belongs in the layer above.&lt;/p&gt;




&lt;h2&gt;
  
  
  Push complexity up the tree
&lt;/h2&gt;

&lt;p&gt;The complexity does not vanish. It moves.&lt;/p&gt;

&lt;p&gt;If the leaf is a single declarative call, something is doing the wiring. If there are no type annotations at the leaf, something is inferring them. If error handling, logging, transactions, and event publishing are not in the leaf, something is wrapping them around it.&lt;/p&gt;

&lt;p&gt;That something is the layer above. It is heavier. It uses generics. It has a non-trivial type signature. It might be hard to read on first glance.&lt;/p&gt;

&lt;p&gt;That is fine, because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It gets written once.&lt;/li&gt;
&lt;li&gt;It can be unit tested with the kind of rigour that pays off when something is used a hundred times.&lt;/li&gt;
&lt;li&gt;It rarely changes.&lt;/li&gt;
&lt;li&gt;Once it is right, agents and humans use it without reading it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade: concentrate cognitive load in a few places that are read rarely, in exchange for shrinking it in every place that is read often. The framework gets harder to write. Every feature gets easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Push the types up too
&lt;/h2&gt;

&lt;p&gt;The same principle applies to typing. This is where TypeScript earns its keep.&lt;/p&gt;

&lt;p&gt;I work in strict mode. Every value is typed. But the leaves do not show it. No explicit type parameters, no &lt;code&gt;: ReturnType&amp;lt;...&amp;gt;&lt;/code&gt; annotations, no manual &lt;code&gt;as const&lt;/code&gt; ceremonies. The types are there in the sense that the compiler will reject any mistake. They are absent in the sense that you cannot see them in the source.&lt;/p&gt;

&lt;p&gt;This is what a well-designed factory with inference gives you. You declare the shape once at the framework level, with generics connecting inputs and outputs. At the leaf, you write plain values. The compiler threads the types through.&lt;/p&gt;

&lt;p&gt;A command handler looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineCommandHandler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;emailTaken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsByEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emailTaken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EmailTaken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&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;Four fields. No type annotations. Reads top to bottom.&lt;/p&gt;

&lt;p&gt;Behind the scenes, &lt;code&gt;defineCommandHandler&lt;/code&gt; is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CommandHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;C&lt;/span&gt;
  &lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
  &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;defineCommandHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommandHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;handler&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;C&lt;/code&gt; is inferred from &lt;code&gt;parse&lt;/code&gt;'s return type. &lt;code&gt;S&lt;/code&gt; is inferred from what &lt;code&gt;load&lt;/code&gt; returns. By the time &lt;code&gt;check&lt;/code&gt; and &lt;code&gt;execute&lt;/code&gt; run, the compiler already knows the shape of &lt;code&gt;state&lt;/code&gt; and &lt;code&gt;command&lt;/code&gt;. Full type safety, zero type ceremony.&lt;/p&gt;

&lt;p&gt;A reader, human or agent, has to learn one thing: a command handler is a &lt;code&gt;defineCommandHandler&lt;/code&gt; call with four fields. The state shape and command shape are inferred. The agent does not need the framework in its context to write a correct new handler. It needs one existing handler. The pattern is the spec.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sketch the leaf first
&lt;/h2&gt;

&lt;p&gt;I try to design the leaf shape before anything else exists. Before the framework. Before the wiring. Before any types are connected up.&lt;/p&gt;

&lt;p&gt;When the shape has not been established yet, I write the sketch myself, or describe it precisely to an agent. Four fields. These names. This is what every new handler should look like when there are forty of them. The leaf is the artefact I am most opinionated about, because it is the one that gets repeated.&lt;/p&gt;

&lt;p&gt;Then I hand the rest off. "Make &lt;code&gt;defineCommandHandler&lt;/code&gt; such that all of these types infer through to the leaf." The agent writes the generics. I do not particularly care what shape the framework code takes (conditional types, helper interfaces, mapped types), as long as the leaf reads the way I sketched it and the compiler refuses incorrect inputs.&lt;/p&gt;

&lt;p&gt;As the agent iterates on the wiring, I question every line that ends up at the leaf. Why is that import there? Why does that field need an annotation? Why is &lt;code&gt;state&lt;/code&gt; being passed explicitly when the framework should already know? Anything that looks like ceremony at the leaf gets pushed back up until the leaf is the bare declaration I sketched.&lt;/p&gt;

&lt;p&gt;I am less precious about the framework's internals. Not because I will not look at them again, but because they are cheap to change. The framework lives in one place. The leaves live in fifty. Refactoring a generic helper is a one-file change. Refactoring the leaf shape is a fifty-file change. The leaf is where care has to be invested up front, because it is the most expensive thing to revise later.&lt;/p&gt;

&lt;p&gt;This inverts the usual review focus. Most reviews zoom in on the parts that look complicated. With this pattern, the complicated parts are the parts hardly anyone touches. The simple parts get multiplied. That is where the attention goes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the agent does next
&lt;/h2&gt;

&lt;p&gt;Ask an agent to add a &lt;code&gt;deleteUser&lt;/code&gt; handler.&lt;/p&gt;

&lt;p&gt;If the leaf is full of ceremony, the agent reads the ceremony, understands each piece, and reproduces it. The boilerplate is load-bearing. Get any of it wrong and tests fail. The agent burns spotlight on plumbing.&lt;/p&gt;

&lt;p&gt;If the leaf is the declarative form above, the agent reads &lt;code&gt;createUser&lt;/code&gt;, recognises the four-field shape, and writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deleteUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineCommandHandler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DeleteUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserNotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&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;It does not need to understand &lt;code&gt;defineCommandHandler&lt;/code&gt;. It does not need to understand transaction boundaries or the dispatcher. It needs to know what a delete does and copy a shape it has already seen. That is exactly what agents are best at.&lt;/p&gt;

&lt;p&gt;Every new feature that fits the leaf-node pattern is a low-context, low-risk task. The framework absorbs the weight.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layers all the way down
&lt;/h2&gt;

&lt;p&gt;This is a multi layered story.&lt;/p&gt;

&lt;p&gt;In a real application the same lift repeats at multiple scales. First, leaves into a generic &lt;code&gt;defineCommandHandler&lt;/code&gt;. Then you notice every user-domain handler wires up the user repository the same way, and you lift again into a &lt;code&gt;defineUserCommandHandler&lt;/code&gt; that pre-wires it. Then the user-domain framework itself shares transactions and event publishing with every other domain framework, and that lifts again.&lt;/p&gt;

&lt;p&gt;At every layer, the question is the same: what ceremony is being repeated below me? Push it up. The result is a stack where each layer reads as a declaration in the language of the layer above. The deeper you go, the heavier the code is allowed to be. The closer to the surface, the closer it reads to pure intent.&lt;/p&gt;

&lt;p&gt;Most feature work happens at the surface, and the surface is where complexity has been most aggressively starved.&lt;/p&gt;




&lt;h2&gt;
  
  
  The discipline
&lt;/h2&gt;

&lt;p&gt;Designing from the leaves is harder than letting them grow organically. It means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refusing to ship the first leaf until it is something you would be happy to see copied a hundred times.&lt;/li&gt;
&lt;li&gt;Treating every line that repeats across leaves as a defect, to be lifted into the layer above.&lt;/li&gt;
&lt;li&gt;Investing in generics and inference at the framework level so the leaf stays free of annotations.&lt;/li&gt;
&lt;li&gt;Writing the heavy framework code carefully and testing it well, because everything above it inherits its correctness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More upfront work for fewer files. It pays back every time a new leaf is added, which in a healthy codebase is most of the work.&lt;/p&gt;

&lt;p&gt;In the AI era the multiplier is even larger. The leaf is the unit of work an agent produces most often. The leaf is what it copies when given a vague instruction. The leaf is the part of your codebase that ends up replicated hundreds of times. Adding a new leaf should not demand reasoning about transactions, error handling, or how the framework wires itself together. None of that lives at the leaf. The agent matches a shape it has seen before, and the framework absorbs the rest. Whatever exists at the leaves is what the codebase keeps producing.&lt;/p&gt;

&lt;p&gt;Make them worth copying.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of an ongoing series on the patterns that make AI-assisted development actually work. See also &lt;a href="//its-still-all-about-boundaries.md"&gt;It's (Still) All About Boundaries&lt;/a&gt; for the broader principle: keep the cognitive surface area small at every zoom level. Designing from the leaves is what that looks like at the smallest zoom.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>architecture</category>
      <category>design</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Claude Code your household</title>
      <dc:creator>Paul Grimshaw | bounded.dev</dc:creator>
      <pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/boundeddev/claude-code-your-household-4m90</link>
      <guid>https://dev.to/boundeddev/claude-code-your-household-4m90</guid>
      <description>&lt;p&gt;I build software systems for a living. For the last couple of years, a large part of that work has been figuring out how to make AI agents effective inside professional codebases: structuring context so agents can navigate, reason, and act without a human holding their hand.&lt;/p&gt;

&lt;p&gt;At some point I started wondering: what would happen if I applied the same patterns to my personal life?&lt;/p&gt;

&lt;p&gt;Not as a productivity experiment. Not as a second brain. Just: what if I had a repo that knew about my household, my finances, my property, my contracts, and an AI agent that could operate on it?&lt;/p&gt;

&lt;p&gt;So I created a git repo called &lt;code&gt;home&lt;/code&gt;, and started using it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup is boring
&lt;/h2&gt;

&lt;p&gt;I want to get this out of the way because it is the least interesting part.&lt;/p&gt;

&lt;p&gt;It is a git repository. It contains markdown files organised into folders. There is a &lt;code&gt;CLAUDE.md&lt;/code&gt; at the root that gives the agent context about the repo, the household, and how things are structured. Git LFS is enabled so PDFs, scans, and invoices live alongside the markdown that describes them.&lt;/p&gt;

&lt;p&gt;That is it. There is no app. No database. No custom tooling. If you have used a static site generator or a docs-as-code setup, you have seen this before.&lt;/p&gt;

&lt;p&gt;The structure is not the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  The inbox
&lt;/h2&gt;

&lt;p&gt;The hardest part of any knowledge system is getting things into it. If filing a document requires deciding where it goes, naming it correctly, and writing a summary, you will not do it. You will leave it in your email or your downloads folder and deal with it never.&lt;/p&gt;

&lt;p&gt;So the repo has an inbox. It is just a folder, a drop zone with no rules. Anything that arrives gets dumped in: scanned post, digital documents, email attachments. My wife and I share a Google Drive folder for this. Scan something with your phone, drop it in, forget about it. Flagged emails work the same way: flagging is the only action needed.&lt;/p&gt;

&lt;p&gt;When I have time for admin, I ask the agent to process the inbox. It opens each item, identifies what it is, suggests where it belongs, files it, and writes a summary. The human effort is: scan, drop, move on. The agent does the filing.&lt;/p&gt;

&lt;p&gt;This is the pattern that makes the whole system sustainable. Capture is near-zero effort. Processing is batched and agent-driven. The inbox separates the moment something arrives from the moment it gets dealt with, and that separation is what makes the whole thing actually work in practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  It is all conversation
&lt;/h2&gt;

&lt;p&gt;I want to set expectations before showing what the repo looks like now, because it would be easy to read the rest of this post and think I sat down and engineered a system. I did not. Everything in this repo emerged from conversations with an AI agent.&lt;/p&gt;

&lt;p&gt;Every interaction is a dialogue. I have a problem (an email to respond to, a tariff to check, a contract to find), and I talk it through with the agent. The agent researches, drafts, pulls in documents, answers the question. That is the primary track: getting the thing done.&lt;/p&gt;

&lt;p&gt;But every conversation has a second track. While we are solving the problem, the agent is also suggesting how to organise what we have found. Where should this go? Is there an existing section or do we need a new one? Sometimes I guide it, sometimes it suggests something better than I had in mind. "You have three insurance policies that reference this property. Should I consolidate them into a single summary?" I would not have thought to do that. The agent spotted the pattern.&lt;/p&gt;

&lt;p&gt;The two tracks are not separate. They happen in the same dialogue, often in the same sentence. This is not overhead. It is just how the conversations go.&lt;/p&gt;

&lt;p&gt;The result is that the repo grows and improves as a byproduct of using it. Nothing you will read below was planned or designed upfront. It all came from conversations.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is the point
&lt;/h2&gt;

&lt;p&gt;The point is what happens when an AI agent can not just &lt;em&gt;read&lt;/em&gt; a knowledge base, but &lt;em&gt;operate&lt;/em&gt; on it. Build it. Maintain it. Act on what it finds.&lt;/p&gt;

&lt;p&gt;Here is a concrete example.&lt;/p&gt;

&lt;p&gt;A few weeks ago I got an email from an architect asking me to sign a contract. I had been working with this architect for a couple of years, but there was nothing about them in the repo yet. No context. No history.&lt;/p&gt;

&lt;p&gt;So instead of manually digging through email, I asked Claude to go through all my correspondence with this architect (every email, every attachment, every invoice, every document) going back two years. Organise the documents. Create a correspondence log. Write a markdown summary of everything: the project timeline, what was agreed, what was invoiced, what is outstanding. File it all in the repo.&lt;/p&gt;

&lt;p&gt;Then draft a response to the email, using that context.&lt;/p&gt;

&lt;p&gt;Here is the part I did not expect. While sweeping through the correspondence, the agent found that we had already signed that exact contract, six months earlier, in October. I had forgotten. The agent drafted a polite reply, attached the signed copy, and pointed out the duplicate. Without the sweep, I would have signed the same contract twice, using a lot of unnecessary cognitive effort.&lt;/p&gt;

&lt;p&gt;Twenty minutes of work. A complete history of the relationship, properly organised, every document filed and linked, a draft reply, and a mistake caught that I would not have caught myself.&lt;/p&gt;

&lt;p&gt;The next time something comes in from that architect, the context is already there. No digging. No remembering. The repo knows.&lt;/p&gt;

&lt;p&gt;That is the pattern. Not "file things neatly." It is: &lt;strong&gt;every question you ask grows the knowledge base, and the knowledge base makes the next question easier.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  You do not fill it. You grow it.
&lt;/h2&gt;

&lt;p&gt;The instinct (and this is the instinct that kills every personal knowledge base) is to sit down and backfill everything. Catalogue all your insurance. Index every appliance warranty. Transcribe every contract.&lt;/p&gt;

&lt;p&gt;Do not do this. You will burn out in a weekend and never open the repo again.&lt;/p&gt;

&lt;p&gt;Instead, let real questions drive it.&lt;/p&gt;

&lt;p&gt;"Am I on the right solar tariff?" That was a real question I had. So I asked Claude to scan my email for all solar correspondence and attachments. The recent install was well covered, but the old panels (the ones already on the house when I bought it) were missing. Those documents were buried in the property purchase folder on Google Drive. So I copied the whole purchase folder into my inbox and asked the agent to organise it into the property section as a side task, then carry on with the solar thread.&lt;/p&gt;

&lt;p&gt;Now I have a solar section I never planned, and a properly organised property section I never sat down to build. Both came from one question about tariffs.&lt;/p&gt;

&lt;p&gt;Succession notes. What happens if I am unexpectedly not around: how to access stuff, where to find things, who to contact. That is in there too. Not because I had a morbid weekend of planning, but because the question occurred to me one evening and it took fifteen minutes to get a first draft into the repo.&lt;/p&gt;

&lt;p&gt;The repo accretes like a coral reef. Each question leaves behind a little more structure. Over six months, it becomes genuinely useful without anyone having planned it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The patterns that emerged
&lt;/h2&gt;

&lt;p&gt;Here is where the software architecture background becomes relevant. Not because I designed these patterns into the repo, but because I recognised them after they appeared.&lt;/p&gt;

&lt;p&gt;When you work with AI agents professionally, you learn that certain patterns make agents dramatically more effective. Structured context. Explicit boundaries. Repeatable workflows with documented decisions. These are not AI-specific ideas. They are just good engineering practices that happen to matter more when the operator has a context window instead of a career of institutional knowledge.&lt;/p&gt;

&lt;p&gt;Several of those patterns showed up in my home repo through conversation, without anyone planning them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runbooks
&lt;/h3&gt;

&lt;p&gt;In a professional setting, a runbook is a step-by-step procedure for a repeatable operation: deploying a service, handling an incident, onboarding a customer. The documentation &lt;em&gt;is&lt;/em&gt; the procedure. An agent reads it and executes.&lt;/p&gt;

&lt;p&gt;In my home repo, the same pattern emerged for quarterly accounts. Through several quarters of doing VAT filings together, the agent and I built up a markdown file that describes the full process: which bank statements to pull, how to categorise expenses, what goes to the business entity versus self-employment, what needs to be uploaded to the tax authority. The agent reads it and follows it.&lt;/p&gt;

&lt;p&gt;Critically, each step is marked as either fully automatable or requiring human input. "Bank statement classification: agent." "KBC security confirmation: Paul's keyboard." This is a human-in-the-loop contract expressed in prose, something I have designed into professional systems but never expected to use for my household accounts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision tables
&lt;/h3&gt;

&lt;p&gt;Every quarter, the agent doing my accounts hits judgment calls. Is this Broadband bill a business expense? What percentage? What about the home office deduction?&lt;/p&gt;

&lt;p&gt;Early on, it asked me every time. After the second quarter of the same questions, the agent suggested we lock the recurring decisions down. We worked through the edge cases together, and it wrote the table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vehicle costs&lt;/td&gt;
&lt;td&gt;Calculate based on logbook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon&lt;/td&gt;
&lt;td&gt;Books only, 100% professional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broadband&lt;/td&gt;
&lt;td&gt;90% deductible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home office share&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now the agent reads the table and applies the rules. No "are you sure?" loops. I made the calls. The agent wrote them down, and keeps the table maintained. When a new edge case surfaces, we resolve it and it adds a row, locked down so it never comes up again.&lt;/p&gt;

&lt;p&gt;This is a pattern I have not often seen articulated in any AI tooling. It is somewhere between a config file and a policy engine, but it is neither. It is just a markdown table that a human can read and an agent can follow. Durable delegation in prose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supplier playbooks
&lt;/h3&gt;

&lt;p&gt;I have a file that documents how to download invoices and statements from nineteen different suppliers. I did not write it. The agent did, by doing.&lt;/p&gt;

&lt;p&gt;The first time I needed Amazon invoices, I pointed the agent at Amazon and said "download everything." It spent a long time figuring it out: navigating the UI, hitting dead ends, discovering that the invoice popover requires a real click rather than a programmatic one. When it was done, I asked it to write back what it had learned into a playbook. Next time, it follows the playbook instead of rediscovering the process from scratch.&lt;/p&gt;

&lt;p&gt;Nineteen suppliers later, the file contains exact URLs, download steps, naming conventions, and the specific gotchas (the part that takes the longest to accumulate). Chrome blocks multi-downloads from Coolblue. GoDaddy does not offer PDF export at all, so the agent reconstructs receipts from extracted text. Each entry is hard-won knowledge that the agent earned by trial and error and then documented for its future self.&lt;/p&gt;

&lt;p&gt;The same feedback loop, again: attempt, learn, write it down, improve next time. I am not doing the learning. The agent is. I am just asking it to remember what it learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it is going
&lt;/h2&gt;

&lt;p&gt;The repo is six months old. It is not finished (it will never be finished), but it is already at the point where I reach for it instinctively when a question comes up about the household, finances, property, or admin.&lt;/p&gt;

&lt;p&gt;I am considering adding a vector database with an MCP interface, so that as the repo grows, search can scale beyond grep and agent context windows. That is a future problem. Right now, structured markdown and a good index file are more than enough.&lt;/p&gt;

&lt;p&gt;I am experimenting with GitHub issues as a task queue for outstanding actions, another professional habit following me home. The repo is the knowledge base, issues are the to-do list, the agent is the worker that connects them.&lt;/p&gt;

&lt;p&gt;I am also experimenting with more sentimental content. Historical dates, family milestones, past holidays. The kind of information that is not urgent but is surprisingly pleasant to have at your fingertips. "What year did we go to Spain?" is a question I should not need to scroll through photos to answer.&lt;/p&gt;




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

&lt;p&gt;If any of this sounds useful, here is what I would suggest:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not plan.&lt;/strong&gt; Do not design a folder structure. Do not write a schema. Do not spend a weekend "setting up."&lt;/p&gt;

&lt;p&gt;Create an empty git repo. Enable Git LFS. Add a &lt;code&gt;CLAUDE.md&lt;/code&gt; that gives the agent basic context: who you are, what the repo is for, how files should be named. Create an &lt;code&gt;inbox/&lt;/code&gt; folder for dumping documents.&lt;/p&gt;

&lt;p&gt;Then wait until you have a real question.&lt;/p&gt;

&lt;p&gt;When you do, ask the agent to help you answer it. Let it pull in documents, write summaries, organise files. When it is done, you will have the beginnings of a knowledge base, not one you designed, but one that grew from an actual need.&lt;/p&gt;

&lt;p&gt;Next question, same thing. And the one after that.&lt;/p&gt;

&lt;p&gt;Six months from now, you will have a repo that knows a surprising amount about your life. Not because you filed things diligently, but because you kept asking questions and letting the answers accumulate.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I have been writing about the patterns that make AI agents effective in professional software: &lt;a href="//its-still-all-about-boundaries.md"&gt;boundaries&lt;/a&gt;, &lt;a href="//i-googled-claude-code-install.md"&gt;security&lt;/a&gt;, structured context. This post is the same ideas applied to a different domain. The principles transfer. The formality does not. And that is rather the point.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>claude</category>
      <category>productivity</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>I Googled 'Claude Code Install' and Nearly Lost Everything</title>
      <dc:creator>Paul Grimshaw | bounded.dev</dc:creator>
      <pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/boundeddev/i-googled-claude-code-install-and-nearly-lost-everything-3pna</link>
      <guid>https://dev.to/boundeddev/i-googled-claude-code-install-and-nearly-lost-everything-3pna</guid>
      <description>&lt;p&gt;I consider myself security-aware. I run a locked-down local environment: credentials in 1Password, SSH keys never on disk, short-lived cloud sessions, the works. I know what &lt;code&gt;curl | sh&lt;/code&gt; does. And I nearly handed my entire machine to an attacker because I clicked a sponsored Google result.&lt;/p&gt;

&lt;p&gt;This is that story, and what I did afterwards.&lt;/p&gt;




&lt;h2&gt;
  
  
  The curl that almost took everything
&lt;/h2&gt;

&lt;p&gt;I was setting up this blog. I needed Claude Code on a personal laptop and did what every developer does: I googled "Claude Code install."&lt;/p&gt;

&lt;p&gt;The first result was a sponsored ad. And there on (what I thought was) the Anthropic home page: the familiar one-liner. The &lt;code&gt;curl | sh&lt;/code&gt; that every developer gets a dopamine hit from, because it means copy, paste, enter, done.&lt;/p&gt;

&lt;p&gt;So I copied it. I pasted it. I hit enter.&lt;/p&gt;

&lt;p&gt;The script ran. A macOS system password prompt appeared. Nothing visually unusual about it, just a standard-looking sudo request. But something felt off. I cannot tell you exactly what. It was not a conscious analysis. It was a gut reaction, the kind you develop after years of working in terminals.&lt;/p&gt;

&lt;p&gt;I had started typing my password. I stopped. I cancelled. I rebooted.&lt;/p&gt;

&lt;p&gt;That gut reaction saved my machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  What was behind it
&lt;/h2&gt;

&lt;p&gt;The command I had run was not what it appeared. The URL was obfuscated using &lt;code&gt;tr&lt;/code&gt;-based character substitution, the kind of trick designed to survive a casual glance but defeat basic content filtering. The &lt;code&gt;curl&lt;/code&gt; flags included &lt;code&gt;-k&lt;/code&gt;, which skips TLS certificate verification entirely. The output was piped straight to &lt;code&gt;zsh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When I later analysed the decoded URL, the 64-character hex string at the end was almost certainly a unique victim identifier. Someone was tracking who ran what.&lt;/p&gt;

&lt;p&gt;The payload itself was serious. This was not a script-kiddie prank. The full attack would have installed a Nix-based hidden partition on the machine: the kind of persistence mechanism that could potentially survive a reformat. On top of that: full credential exfiltration, everything readable by the current user sent off to a remote server.&lt;/p&gt;

&lt;p&gt;The password prompt was right at the beginning of the script. It needed admin access to install that partition. Because I cancelled before entering my password, the deepest parts of the attack never executed. But everything the script could do as my unprivileged user (reading files, sending data), it could have done in the seconds before I cancelled.&lt;/p&gt;




&lt;blockquote&gt;
&lt;h3&gt;
  
  
  I expected better from you, Google
&lt;/h3&gt;

&lt;p&gt;A paid sponsored result serving obfuscated malware impersonating a mainstream developer tool. I reported it to Google Safe Browsing, but this should never have got through ad review.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The investigation
&lt;/h2&gt;

&lt;p&gt;My first instinct after rebooting was: &lt;strong&gt;do not trust this machine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I did not open a terminal. I did not run anything locally. Instead, I went to Gemini's web console (a completely separate environment, nothing running on my hardware) and pasted the script URL. I asked Gemini to inspect the contents. Not run it. Just read it and tell me what it does.&lt;/p&gt;

&lt;p&gt;It decoded the obfuscation, identified the payload, and confirmed that every script hosted on that site was malware. It could also see that the destructive payload (the partition install, the deep persistence) required the admin password I had not provided.&lt;/p&gt;

&lt;p&gt;Armed with Gemini's report, I opened Claude Code and asked it to investigate further. It swept the machine systematically across every common attack vector: crontab, launch agents, launch daemons, shell profiles, temporary directories, running processes. I also ran a full malware scan. No trace of the malware beyond the command in my shell history, which I then deleted.&lt;/p&gt;

&lt;p&gt;I asked Claude Code to write up a full incident report: timestamped, structured, with a complete credential audit. That report went straight into my git-backed "home" knowledgebase, because when something goes wrong, the first thing you want is a clear written record of exactly what happened and what you did about it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What did this thing have access to?
&lt;/h2&gt;

&lt;p&gt;My gut reaction was immediate and visceral: what could this thing have got hold of?&lt;/p&gt;

&lt;p&gt;Luckily, I already work in a fairly sandboxed way. Everything lives in 1Password. Nothing in the macOS Keychain, nothing saved in the browser. AWS goes through Granted SSO with short-lived tokens. So the blast radius was already smaller than it could have been.&lt;/p&gt;

&lt;p&gt;But it was not zero. A few things were still on disk:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH private key&lt;/strong&gt; : sitting in &lt;code&gt;~/.ssh/id_rsa&lt;/code&gt;. An older key that coexisted alongside my 1Password-managed one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub CLI token&lt;/strong&gt; : stored in &lt;code&gt;~/.config/gh/hosts.yml&lt;/code&gt;. Access to my personal repositories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pulumi Cloud token&lt;/strong&gt; : plaintext in &lt;code&gt;~/.pulumi/credentials.json&lt;/code&gt;. Access to infrastructure-as-code organisation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS credentials&lt;/strong&gt; : fine. Temporary STS token via Granted SSO, already expired by the time I investigated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes config&lt;/strong&gt; : home server client certificates in &lt;code&gt;~/.kube/config&lt;/code&gt;. Mitigated by the server not being internet-accessible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS Keychain&lt;/strong&gt; : worth noting for others: the login keychain unlocks automatically when you log in. A script running as your user can silently read items that do not have explicit password-protection ACLs set, and many CLI-added items fall into this category. I do not use the Keychain for credentials, but if you do, be aware of this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser-stored passwords&lt;/strong&gt; : Chrome, Firefox, and Safari all store credentials in databases readable by the current user. Another reason to use a dedicated credential manager rather than browser-saved passwords. Again, not a risk for me, but worth calling out.&lt;/p&gt;

&lt;p&gt;The picture was not catastrophic, most of my setup was already locked down. But those few stragglers on disk were enough to make me uncomfortable. It only takes one leaked SSH key or API token to cause real damage.&lt;/p&gt;




&lt;h2&gt;
  
  
  The response
&lt;/h2&gt;

&lt;p&gt;I rotated everything immediately. Even though the investigation suggested nothing had been exfiltrated, I treated it as if it had.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSH key&lt;/strong&gt; : removed from GitHub, deleted from disk, new key generated inside 1Password, never exported, never on the filesystem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub CLI token&lt;/strong&gt; : rotated, moved to 1Password, now injected at runtime via &lt;code&gt;op run&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pulumi token&lt;/strong&gt; : revoked, new token in 1Password, plaintext file deleted, now via &lt;code&gt;op run&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS&lt;/strong&gt; : no action needed, already temporary tokens via Granted SSO&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell history&lt;/strong&gt; : malicious command removed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I audited the security logs. GitHub's audit log showed no suspicious activity during the incident window: no new keys added, no unexpected pushes, no OAuth authorisations I did not recognise. Same story across other services. Clean.&lt;/p&gt;

&lt;p&gt;It seems I was lucky. This time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The postmortem: what if (when) it happens again?
&lt;/h2&gt;

&lt;p&gt;Here is where my thinking shifted. The question was no longer "did this attack succeed?" It was: &lt;strong&gt;when the next one does, how do I make sure it does not matter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not because it definitely will, but because I need to assume it could. A moment of inattention. A prompt injection in an inbox scanner. A supply chain compromise in a dependency. The attack surface for developers is enormous and growing, especially as we grant AI agents more access to our systems.&lt;/p&gt;

&lt;p&gt;So my assumption now is: &lt;strong&gt;assume compromise is possible, and make sure it does not matter.&lt;/strong&gt; The goal is not to prevent every possible attack. The goal is to ensure that if something gets through, the blast radius is zero.&lt;/p&gt;




&lt;h2&gt;
  
  
  Zero trust starts on your own machine
&lt;/h2&gt;

&lt;p&gt;The protocol I built afterwards has one rule: &lt;strong&gt;no credential should be usable if exfiltrated from disk.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Everything lives in 1Password
&lt;/h3&gt;

&lt;p&gt;Every secret (SSH keys, API tokens, CLI credentials) is stored in 1Password and injected at runtime. Nothing sits on the filesystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH keys&lt;/strong&gt; are generated inside 1Password and served by the 1Password SSH agent. The private key never touches disk. It never appears in a file. The agent signs requests in memory, gated by biometric authentication. If someone copies your entire home directory, they get nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI tools&lt;/strong&gt; : the first port of call is to check whether 1Password has a plugin for the tool. GitHub CLI and Pulumi both have native 1Password plugins that handle authentication seamlessly. No aliases needed, just configure the plugin and it works:&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="c"&gt;# These tools have 1Password plugins. Zero config after setup.&lt;/span&gt;
gh auth status &lt;span class="c"&gt;# authenticated via 1Password plugin&lt;/span&gt;
pulumi up &lt;span class="c"&gt;# token injected by 1Password plugin&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For tools without a plugin, &lt;code&gt;op run&lt;/code&gt; injects the token into the subprocess environment without ever writing it to disk:&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="c"&gt;# Tools without plugins. Use op run via a shell alias.&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;cloudflare&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'op run --env-file=~/.config/cloudflare/env -- cloudflare'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No friction in daily use. No credential on disk. Ever.&lt;/p&gt;

&lt;h3&gt;
  
  
  Short-lived sessions only
&lt;/h3&gt;

&lt;p&gt;AWS access goes through Granted SSO. Every session is a temporary STS token with a tight expiry. Even if exfiltrated, it is useless within hours. I deliberately keep these sessions short.&lt;/p&gt;

&lt;p&gt;For any tool that requires a persistent token: it goes in 1Password. There are no exceptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Periodic credential audits
&lt;/h3&gt;

&lt;p&gt;I built a Claude Code skill called &lt;code&gt;credential-audit&lt;/code&gt; that scans the machine for plaintext credentials. The usual suspects: &lt;code&gt;~/.aws/credentials&lt;/code&gt;, &lt;code&gt;~/.ssh/*.key&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt; files, anything that looks like a token in a config file. I run it regularly. Think of it like running your test suite, but for your own security posture. I have an outstanding action to harden this up or use an off the shelf skill.&lt;/p&gt;

&lt;p&gt;1Password's Watchtower feature does something similar at a broader level: flagging compromised passwords, weak credentials, services without MFA. Turn it on.&lt;/p&gt;

&lt;h3&gt;
  
  
  MFA everywhere
&lt;/h3&gt;

&lt;p&gt;Hardware keys or TOTP on every web service. Non-negotiable. This deserves its own post, and fortunately I've followed this practice for years. But for now: if a service supports it and you have not enabled it, do it today.&lt;/p&gt;




&lt;h2&gt;
  
  
  Living with it: permission hygiene for AI agents
&lt;/h2&gt;

&lt;p&gt;This protocol means I get prompted by 1Password a lot. Every time an agent needs to push to GitHub, every time Pulumi runs, every time an SSH connection is made, 1Password asks for confirmation.&lt;/p&gt;

&lt;p&gt;This is where the balance gets delicate.&lt;/p&gt;

&lt;p&gt;If you get prompted too much, you will start blindly accepting. And blind acceptance is exactly as dangerous as having no protection at all. You need to find the sweet spot: enough prompts that you are making conscious decisions about access, few enough that you actually read them.&lt;/p&gt;

&lt;p&gt;My approach: be somewhat lenient on permissions for common operations, but for every prompt, spend one second asking: &lt;strong&gt;what is this session doing, and does it need this?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usually the answer is obvious. The agent is pushing a PR, of course it needs the SSH key. But occasionally something feels off. An agent asking for credentials it should not need. A session that has been running longer than expected. A prompt that does not match what you thought was happening.&lt;/p&gt;

&lt;p&gt;This is where intuition matters. Anthropic's own guidance on working with AI agents is good here: you cannot review every single command. That is not feasible. But you can develop a feel for when things are going as expected and when they are not. If something feels off, stop the session and investigate what is happening. It is always cheaper to interrupt and check than to let something run that should not be running.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Feature request: 1Password, please give us agent context
&lt;/h3&gt;

&lt;p&gt;When multiple AI agents are running concurrently, the 1Password prompt just says "Terminal wants to use your SSH key." I would love it to say: "The DynamoDB performance agent wants the Git SSH key to push a PR." Linking the access request to the session context would make the approve/deny decision trivial. Would be a nice addition to the multi-agent workflow.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I still want
&lt;/h2&gt;

&lt;p&gt;An emergency kill switch. A single command (ideally runnable from a separate trusted device) that invalidates every active session across every service. If my machine is compromised right now, I want one button that revokes everything, immediately.&lt;/p&gt;

&lt;p&gt;Some services support this individually. But a unified "revoke all sessions everywhere" capability, integrated with 1Password, would be genuinely useful. Today I would have to manually cycle through each service. Under pressure, in an incident, that is too slow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The uncomfortable truth
&lt;/h2&gt;

&lt;p&gt;I am someone who thinks about security daily. I work with AI agents, I understand the risks, I read about prompt injection and supply chain attacks. And I nearly lost my machine to a Google search.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;curl | sh&lt;/code&gt; pattern is a social engineering masterpiece. It is the exact shape of something developers trust: a clean one-liner, the promise of zero friction, the dopamine hit of a fast install. It is designed to bypass the part of your brain that asks questions.&lt;/p&gt;

&lt;p&gt;I got lucky. A vague feeling. A few seconds of hesitation. That is not a security strategy.&lt;/p&gt;

&lt;p&gt;What I have now is a security strategy: 1Password holding everything, nothing on disk, short-lived sessions, regular audits, conscious permission decisions. It assumes I will make mistakes. It assumes something will get through. And it ensures that when it does, there is nothing to take.&lt;/p&gt;

&lt;p&gt;If you have read my &lt;a href="//its-still-all-about-boundaries.md"&gt;first post&lt;/a&gt;, you will know that boundaries are the overarching theme of everything I write about here. The same principle applies to your local environment: if credentials are isolated, sessions are scoped, and access requires explicit approval, then a breach in one area cannot cascade into everything else. Zero trust is boundary discipline applied to your own machine.&lt;/p&gt;

&lt;p&gt;Treat your laptop as a hostile environment, because one day it will be.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you work with AI coding agents, I would strongly recommend auditing what credentials are sitting on your local disk right now. You might be surprised. The &lt;code&gt;security find-generic-password&lt;/code&gt; command on macOS, a quick &lt;code&gt;ls&lt;/code&gt; of your home directory config files, and a check of &lt;code&gt;~/.ssh/&lt;/code&gt; will tell you most of what you need to know. Then move everything into a credential manager and never look back.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
