{"@attributes":{"version":"2.0"},"channel":{"title":"Sarah Deaton","link":"https:\/\/sdeaton.com","description":"On AI, developer tools, technical writing, and whatever else catches my interest","generator":"Zola","language":"en","lastBuildDate":"Sat, 16 May 2026 00:00:00 +0000","item":[{"title":"Making All Caps as a Service","pubDate":"Sat, 16 May 2026 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/making-acaas\/","guid":"https:\/\/sdeaton.com\/blog\/making-acaas\/","description":"<p>For my recent <a href=\"..\/git-talk-recap\/\">Write the Docs talk<\/a>, I needed a docs site that felt real enough to demo against. I didn\u2019t have a convenient API lying around to document, so I built a toy API and a full docs site on top of that; overengineered, but did what I wanted and it was a fun side quest.<\/p>\n<p>This is the full walkthrough of building ACAAS, <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/acaas.mintlify.app\/\">All Caps as a Service<\/a>. Looking back, I thought it was an interesting, abbreviated case study in some AI\/docs tooling and building a site from scratch, so I wanted to jot down the decisions and steps.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/making-acaas\/acaas-home.png\" alt=\"ACAAS docs homepage, showing the hero and primary navigation.\" \/><\/p>\n<h2 id=\"1-build-the-service\">1. Build the service<\/h2>\n<p>First, I used the Claude Code CLI to build a small API from scratch. I gave it the concept (you send text, it sends it back in all caps) and asked it to make it realistic enough to write docs against (like rate limits and error messages, and a few endpoints). It chose Python for the backend and came back with a few new endpoints like <code>\/shout<\/code> and <code>\/whisper<\/code>, and the supporting machinery to go with them.<\/p>\n<p>I also told it I wanted one AI-powered feature, ideally something on the path to GA; a feature graduating from experimental is the kind of documentation moment I wanted to demo (and every SaaS company needs an AI feature). It proposed <code>\/experimental<\/code>, which uses a small model to decide which words in your sentence deserve to be bolded.<\/p>\n<p>After it finished building the API, I ran <code>\/simplify<\/code>, which is <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/skills#bundled-skills\">a bundled Claude Code skill<\/a> that does a quick cleanup pass to look for dead code and unnecessary indirection. This wasn\u2019t important for anything, but I wanted to at least feel like the code was cleaner than a one-shot pass. Then I <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/best-practices#write-an-effective-claude-md\">ran <code>\/init<\/code><\/a> to generate a README and CLAUDE.md. The README is for future me, in case I ever need to run or modify this thing again. The CLAUDE.md is just good practice at this point, the scaffold I\u2019d want if I came back to this project for something else.<\/p>\n<h2 id=\"2-find-a-documentation-platform\">2. Find a documentation platform<\/h2>\n<p>My first instinct for the docs platform was <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.getzola.org\/\">Zola<\/a> or <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/astro.build\/\">Astro<\/a>. But before choosing, I did a sanity check on <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.mintlify.com\/\">Mintlify<\/a>, which I\u2019ve worked with professionally for a couple of years and have never tried spinning up for a personal project. I\u2019d assumed the free tier would be limited in some way that didn\u2019t fit a one-off demo site, but it wasn\u2019t. I created <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/acaas.mintlify.app\">acaas.mintlify.app<\/a> in about a minute, cloned the repo, and was ready to go.<\/p>\n<p>I was mildly embarrassed not to have known about the free tier. I also kind of want to move my blog over to Mintlify now.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/making-acaas\/mintlify-dashboard.png\" alt=\"The Mintlify analytics dashboard for ACAAS, with 19 visitors and 41 views over a recent date range, a Humans\/Agents traffic split, a Visitors Over Time bar chart, and a left sidebar covering Home, Editor, Analytics, Settings, and an Agents group for Workflows, Agent, Assistant, and MCP.\" \/><\/p>\n<h2 id=\"3-design-the-site\">3. Design the site<\/h2>\n<p>I didn\u2019t need to do this step at all, but it was the most fun. I have 0 design sense whatsoever, and <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/claude.ai\/design\">Claude Design<\/a> has been a lifesaver for me. I started a new prototype, described what I was building (\u201ca Mintlify docs site for a fake API called All Caps as a Service\u201d), and it started guiding me through the design steps until I had a lovely new branded docs site.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/making-acaas\/claude-design-landing.png\" alt=\"Two Claude Design landing-page variations for ACAAS in dark mode, side by side: Variation A (Loud, Amber) with a large \u201csend whisper. RECEIVE THUNDER.\u201d hero, and Variation B (Restrained, Cobalt) with a status dashboard, quickstart steps, and an endpoint table.\" \/><\/p>\n<p>I like that Claude Design asks clarifying questions as it works; things like what kind of feel I was after (bold or minimal or standard SaaS), whether I had color directions or wanted it to pick, etc. I landed on a theme I was happy with, had it generate a favicon and logo to match, and the whole time it was working inside Mintlify\u2019s design constraints rather than producing something I\u2019d have to retrofit. When I was done, I chose the \u201cHandoff to Claude Code\u201d export option and went on with my day.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/making-acaas\/claude-design-marks.png\" alt=\"Claude Design output for ACAAS branding: eight candidate icon marks (Monogram A, Sound bars, Bang, Stacked carets, Megaphone, aA transform, Boost, Chevrons) with short rationales, three wordmark lockups, and a recommended pick of the aA transform with sizing and dark-mode previews.\" \/><\/p>\n<h2 id=\"4-apply-the-design\">4. Apply the design<\/h2>\n<p>I used Claude Code Desktop for this because it has a built-in local preview. I could run the site locally and watch it render while Claude made changes, and Claude can see the preview too, so it catches its own rendering errors without me having to describe what I\u2019m seeing. The workflow felt closer to pairing than to instructing. You could also do this with the CLI and then use the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/chrome\">Claude in Chrome extension<\/a> or <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/github.com\/microsoft\/playwright-mcp\">Playwright MCP<\/a> to have Claude iterate on the design itself locally, but I just like the Desktop in-app experience.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/making-acaas\/claude-code-desktop-preview.png\" alt=\"Claude Code Desktop with the conversation in the left pane (a turn confirming that docs.json, index.mdx, styles.css, and new logo files were copied to the worktree, with Mintlify hot-reloading on localhost:3000) and the live ACAAS docs site rendered at localhost:3000 in the right pane, showing the freshly-applied \u201csend whisper. RECEIVE THUNDER.\u201d hero.\" \/><\/p>\n<p>This step was pretty anticlimactic in a nice way. Claude took the design handoff, implemented it cleanly, and there wasn\u2019t anything to debug. As an aside, I\u2019ve shipped so many ugly, half-finished prototypes and waved it away with \u201cI don\u2019t like CSS,\u201d and it feels like the days of that are finally over.<\/p>\n<h2 id=\"5-scaffold-the-docs\">5. Scaffold the docs<\/h2>\n<p>I let AI generate the content with little direction, and I feel very fine about that. I said I wanted getting-started guides, cookbooks, and a changelog, and it produced them. I\u2019m sure a few are confidently wrong in ways that don\u2019t matter for a demo.<\/p>\n<p>One thing that surprised me was that new Mintlify sites ship with a pre-built <code>AGENTS.md<\/code> file and a <code>.claude\/<\/code> directory with settings and skills scaffolding for working with Mintlify, along with some basic style guides. The core instructions felt fairly useful\/didn\u2019t include anything I\u2019d argue didn\u2019t deserve to be there.<\/p>\n<h2 id=\"6-create-an-openapi-spec\">6. Create an OpenAPI spec<\/h2>\n<p>The last piece was an API reference section. This is one of my favorite Mintlify features, even though the docs I work on now don\u2019t have an API component. The API reference pages are built in and include an interactive playground where visitors can make live calls to the service from inside the docs themselves. All it needs is an OpenAPI spec, which Claude Code generated directly from the service code.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/making-acaas\/acaas-openapi.png\" alt=\"The ACAAS API reference section generated from the OpenAPI spec, with a \u201cTry it\u201d panel and request\/response examples.\" \/><\/p>\n<hr \/>\n<p>Start to finish, the whole build was about two hours, most of which was the design step and a small detour deciding how much realism the rate limits needed. A year ago I would have spent the same two hours scaffolding just the initial API and decided nevermind maybe I\u2019ll go a different direction.<\/p>\n<p>End result: I used the demo in my talk, <a href=\"..\/git-talk-recap\/\">the git commands I avoided for nine years<\/a>.<\/p>\n"},{"title":"The git commands I avoided for nine years","pubDate":"Fri, 15 May 2026 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/git-talk-recap\/","guid":"https:\/\/sdeaton.com\/blog\/git-talk-recap\/","description":"<!--\nTODO before publishing:\n- copy videos from ~\/src\/git-talk-slides\/videos\/ into this directory:\n  with-worktrees.mp4, with-reflog.mp4, update-refs.mp4\n-->\n<p>I recently gave a <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.youtube.com\/watch?v=FiGT3XYICSE\">talk<\/a> at <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.writethedocs.org\/\">Write the Docs<\/a> about three git commands I\u2019d been avoiding for most of my career, or in two cases didn\u2019t know existed at all. For the demo environment behind the talk, I built <a href=\"..\/making-acaas\/\">All Caps as a Service<\/a>, a fake API and docs site that turned into its own side quest.<\/p>\n<p>To properly set expectations up front: I\u2019m not a git expert. I\u2019ve used git almost daily for a decade with a workflow I\u2019ll describe in a minute, and recently discovered some parts of git that changed my thinking on it. I gave the talk and wrote this post because you don\u2019t need to be a git expert to get a long way with the tool, or to look into the parts you\u2019ve been avoiding.<\/p>\n<iframe\n  width=\"100%\"\n  height=\"415\"\n  src=\"https:\/\/www.youtube.com\/embed\/FiGT3XYICSE\"\n  title=\"The git commands I avoided for nine years\"\n  frameborder=\"0\"\n  allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n  allowfullscreen\n><\/iframe>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/git-talk-recap\/git-talk-sketchnote.jpg\" alt=\"Sketchnote of the talk by Dennis Dawson. A timeline of sticky notes across the top reads \u201cThe Git Commands I Avoided for Nine Years (and why I wish I hadn\u2019t).\u201d Below, three commands are illustrated: git worktree, described as creating a second checkout of the repo; git reflog, described as an undo button for recovering lost commits, deleted branches, and bad rebases; and git rebase \u2013update-refs, described as automagically force-updating all local branches that point to commits being rebased. Jujutsu (jj) appears at the bottom, described as reimagining what a commit is with no staging area and conflicts as first-class objects. On the right, \u201ccommit often\u201d is written vertically in large pink letters.\" \/><\/p>\n<p><em>Sketchnote by <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/dennissdawson.wixsite.com\/mr--dawson\/portfolio\">Dennis Dawson<\/a>, via the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.flickr.com\/photos\/writethedocs\/55266348341\/in\/album-72177720333614185\">Write the Docs Flickr<\/a>. I love the reflog drawing.<\/em><\/p>\n<h2 id=\"how-i-got-here\">How I got here<\/h2>\n<p>For the majority of my time in tech, I used about six git commands: <code>add<\/code>, <code>commit -m \"wip\"<\/code>, <code>rebase -i<\/code>, <code>commit --amend<\/code>, <code>cherry-pick<\/code>, <code>push<\/code>. Add, save, clean up the history, polish, and push. It worked, and it produced nice clean PRs.<\/p>\n<p>When I first learned git in a software engineering bootcamp, the framing was unambiguous. Git was esssential but could be dangerous. You could lose work, and you should stay inside a small set of safe commands. Above all, don\u2019t touch rebase while you\u2019re learning. If you got into a mess with it, you were on your own.<\/p>\n<p>I eventually went to work on teams with other engineers, learned about collaborating on projects using git, added <code>git rebase -i<\/code> to my toolkit, and felt pretty accomplished. My daily git commands worked for the most part\u2026 except for the few times when I\u2019d do a bad rebase, and think I lost hours of work, despair and start over. Or get into a really tangled merge conflict and just end up deleting the branch and pulling down again from the remote. I thought that was the way it was; git was powerful with some rough edges.<\/p>\n<p>What changed this for me had nothing to do with git initially. Last year, I wanted to run two Claude Code sessions on the same repo on different branches at once, purely for parallelism. I didn\u2019t know how to do this, because as far as I knew, you could only have one branch checked out for a repo at a given time on your computer.<\/p>\n<p><a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/worktrees\">The fix turned out to be <code>git worktree<\/code><\/a>, a git feature rather than anything to do with Claude Code. It also addressed a paper cut I\u2019d been living with for years that I didn\u2019t even realized I\u2019d had: the stash-checkout-checkout-back-stash-pop dance every time I wanted to spin up a quick branch for a typo fix when I was in the middle of other work. Fixing a problem I hadn\u2019t let rise to consciousness made me wonder what else was there and what I should incorporate into my toolkit.<\/p>\n<h2 id=\"the-three-commands\">The three commands<\/h2>\n<p>I picked these three commands because they were the most directly useful ones that I found, and, in the case of git reflog, changed my view of git entirely.<\/p>\n<h3 id=\"git-worktree\"><code>git worktree<\/code><\/h3>\n<p>You\u2019re mid-edit on a quickstart change, and someone asks for a one-line typo fix on a different branch. The dance is <code>git stash<\/code>, <code>git checkout main<\/code>, <code>git checkout -b typo-fix<\/code>, fix it, <code>git checkout quickstart<\/code>, <code>git stash pop<\/code>. So much context switching and heaven forbid you get pulled away in the middle of it and have to remember where you were in the dance when you left off.<\/p>\n<p>Worktrees solve this context switching problem. The command <code>git worktree add ..\/typo-fix main -b typo-fix<\/code> makes a second working directory on your machine, pointing at the same repo, on a new branch. Then you can cd into that directory (<code>..\/typo-fix<\/code>) and have two branches open at the same time, without needing to wind down\/stash your work from one task to switch to the other.<\/p>\n<p>The worktrees share the same <code>.git<\/code> history, but all the files are duplicated and won\u2019t clobber each other as you make changes in each branch. The quickstart edits stay sitting in the original directory, untouched. Open the new folder in a second editor window, fix the typo, push, and the original is exactly where you left it.<\/p>\n<p><strong>When you\u2019d reach for it:<\/strong> any time you\u2019d otherwise stash or do the checkout dance. Especially worth it if you context-switch between branches a lot.<\/p>\n<p><strong>When it shipped:<\/strong> 2015, a year before I started learning git.<\/p>\n<h3 id=\"git-reflog\"><code>git reflog<\/code><\/h3>\n<p>A pronunciation note: it\u2019s <em>ref-log<\/em>, short for \u201creference log.\u201d I\u2019d always pronounced it <code>git re-flog<\/code>, which made it sound made up and was a primary reason I didn\u2019t investigate it any further. Based on the people who came up to me after the talk, I\u2019m not the only one who did this. This is also the one command that made me rethink my whole vision of git and realize it\u2019s so much safer than I\u2019d been taught.<\/p>\n<p><code>git reflog<\/code> shows everywhere <code>HEAD<\/code> has been recently (every commit, reset, checkout, rebase, all of it). If you ever accidentally <code>git reset --hard<\/code> too far or drop a commit during a rebase, or get a tangled git history through a merge conflict, this is the command that saves you. The \u201clost\u201d commit, or the good place you used to be but now aren\u2019t, is sitting right there in the reference log. You can always go back to it. Just run <code>git reflog<\/code>, find the SHA for the commit\/place in time you want to return to, and <code>git reset<\/code> back to that place.<\/p>\n<p>The piece that finally clicked for me, which I\u2019d been missing when I was taught: HEAD in your repository is just a <strong>pointer<\/strong>. Doing something like <code>reset<\/code> or changes branches moves the pointer, but the commits themselves don\u2019t go anywhere. <code>reflog<\/code> is the trail the pointer left behind, kept for 90 days by default.<\/p>\n<p>The one catch is that it only saves <em>committed<\/em> work, and uncommitted changes blown away by <code>reset --hard<\/code> are gone. Commit early and often!<\/p>\n<p><strong>When you\u2019d reach for it:<\/strong> any time you think you\u2019ve lost work or are in an otherwise irreparable state.<\/p>\n<p>Neat trick: I also learned recently that you can even do things like <code>main@{one.week.ago}<\/code> to go back to where your main branch was a week ago. You don\u2019t even need to find exact commits but can say things like \u201cI knew this was working two days ago, let\u2019s go back there.\u201d<\/p>\n<h3 id=\"git-rebase-update-refs\"><code>git rebase --update-refs<\/code><\/h3>\n<p>You have a stacked PR (branch A off main, branch B off A). Someone lands a change on main that conflicts with A. The naive flow is to rebase A onto main, resolve the conflict, then rebase B onto the new A, where the same conflict is waiting for you to resolve a second time.<\/p>\n<p><code>git rebase --update-refs main<\/code>, run from the top of the stack, rebases the whole stack in one pass and moves the branch pointers along the way. You only need to resolve the conflict once, and then all the downstream branches get updated with the changes from main as well.<\/p>\n<p><strong>When you\u2019d reach for it:<\/strong> stacked PRs that should all get updates from a branch at the same time.<\/p>\n<p><strong>When it shipped:<\/strong> 2022. I\u2019d originally planned to talk about <code>git rerere<\/code> (it remembers a conflict resolution and replays it next time the same one shows up), and found <code>--update-refs<\/code> while writing the talk.<\/p>\n<h2 id=\"the-one-i-think-actually-matters\">The one I think actually matters<\/h2>\n<p>If you take one of these home, take reflog.<\/p>\n<p>Worktrees and <code>--update-refs<\/code> are about ergonomics and make day-to-day tasks easier. Reflog is different; it changes the belief that screwing up means losing work, and that belief was what kept me out of the parts of git where the real power lives.<\/p>\n<h2 id=\"for-the-record\">For the record<\/h2>\n<p>A few QA questions where my live answer was either hand-wavy or wrong, because again, I\u2019m not a git expert at all. Here are my corrected answers (and thanks for the opportunity to learn this with you all!)<\/p>\n<h3 id=\"git-worktree-vs-multiple-clones\"><code>git worktree<\/code> vs. multiple clones<\/h3>\n<p>Someone asked what the actual difference was between doing <code>git worktree<\/code> vs. just having multiple cloned versions of your repo. A clearer answer than whatever I gave:<\/p>\n<p>Multiple clones give you fully independent copies of the repo, each with its own <code>.git<\/code> directory. Branches you make in clone A don\u2019t show up in clone B until you push and fetch, and you\u2019re paying for the full history twice on disk.<\/p>\n<p>Worktrees share the underlying <code>.git<\/code> (the secondary worktree has a tiny <code>.git<\/code> <em>file<\/em> pointing back to the main one). Branches and commits show up in every worktree immediately, with no fetching and no duplicate storage. The one constraint is that the same branch can\u2019t be checked out in two worktrees at once.<\/p>\n<p>If you want two separate copies of the project that can drift apart, that\u2019s <code>git clone<\/code>. If you want one project with multiple working directories so you can sit on two branches at the same time, that\u2019s <code>git worktree<\/code>.<\/p>\n<h3 id=\"does-reflog-work-with-reset-soft-and-hard\">Does reflog work with <code>reset --soft<\/code> and <code>--hard<\/code>?<\/h3>\n<p>Yes for both. I also want to walk back something I said on stage, because I had it backwards. Every form of <code>git reset<\/code> moves HEAD, which is the whole point of the command. The flags change what happens to the index and working tree on top of that:<\/p>\n<ul>\n<li><code>--soft<\/code>: move HEAD. Index and working tree untouched, so whatever was previously committed shows back up as staged.<\/li>\n<li><code>--mixed<\/code> (the default): move HEAD, reset the index to match it. Working tree untouched, so changes show up as unstaged.<\/li>\n<li><code>--hard<\/code>: move HEAD, reset the index, and overwrite the working tree to match. Uncommitted changes (staged or not) are gone.<\/li>\n<\/ul>\n<p>Reflog records every move of HEAD, so all three are recoverable in the sense that you can always put HEAD back where it was. The thing reflog <em>can\u2019t<\/em> save you from is the working-tree damage <code>--hard<\/code> does. Edits you hadn\u2019t committed aren\u2019t in the reflog, because the reflog tracks committed history, not whatever\u2019s sitting in your buffer. That\u2019s the part of \u201ccommit often\u201d I was trying to gesture at on stage and could have been clearer about.<\/p>\n<h3 id=\"a-note-on-jujutsu\">A note on Jujutsu<\/h3>\n<p>At the end of the talk I mentioned <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/github.com\/jj-vcs\/jj\">Jujutsu (or <code>jj<\/code>)<\/a>. I didn\u2019t even realize for a while that there were other version control systems being developed; I kind of thought git was the one we\u2019d settled on. But of course there were VCS systems before and people are making new ones too, revisiting these foundational concepts and\/or layering on top.<\/p>\n<p>I find <code>jj<\/code> to be really interesting because it\u2019s a more recent VCS that\u2019s gaining popularity. It can live alongside a git repo, sharing objects and remotes, but rethinks the model. <code>jj<\/code> has no staging area, and you carry conflicts with you across operations instead of resolving them on the spot. The command surface is also much smaller. I\u2019ve just heard of it and haven\u2019t used it for real work, so I don\u2019t have an opinion on whether it\u2019s worth switching. But it\u2019s on the list, and if you\u2019ve been bouncing off git for years and it still doesn\u2019t fit, that might be the direction to look.<\/p>\n<h2 id=\"coda\">Coda<\/h2>\n<p>I keep coming back to one specific story. After learning software engineering at a bootcamp, I taught there for a year. One day a student ran a rebase even though we\u2019d warned her against it. She lost a day of work and asked for help. The response from the instructors, mine included, was \u201coops, oh well, we told you so,\u201d and I still feel bad about it. Reflog would have walked her right back to where she was, but none of us knew about it, so none of us could tell her. What stuck with her instead was <em>git is dangerous, stay in the shallow end<\/em>.<\/p>\n<p>I gave the talk as a non-expert and that was sort of the point. You can poke at the parts of git you\u2019ve been avoiding without becoming an expert first. The cost of looking was small for me, a search engine and an afternoon per command, weighed against nine more years of deleting repos and living with what I thought was lost.<\/p>\n"},{"title":"Costuming Claude Code","pubDate":"Sun, 10 May 2026 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/costuming-claude-code\/","guid":"https:\/\/sdeaton.com\/blog\/costuming-claude-code\/","description":"<p>Most of how I\u2019ve customized Claude Code so far has been on the side that changes what it knows or does: <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/memory#claude-md-files\"><code>CLAUDE.md<\/code><\/a> for project conventions, <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/memory#organize-rules-with-claude%2Frules%2F\">path-scoped rules<\/a> for specific instructions, <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/skills\">skills<\/a> for specialized behaviors, <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/hooks-guide\">hooks<\/a> for guardrails.<\/p>\n<p>A separate set of customization knobs sits alongside those, and they don\u2019t really change what Claude does, just how being in the CLI feels. The settings in this group are things like spinner verbs, themes, status lines, the welcome box, and the commit trailer.<\/p>\n<p>What inspired me to pull them all together was watching other people character-ify their spinner verbs. I\u2019d been treating it as a fun one-off customization, until I started seeing screenshots of people swapping their verbs out for things Bender from Futurama would say, or Moira Rose from Schitt\u2019s Creek (<em>Bombinating<\/em>, <em>Effulging<\/em>, <em>Pontificating<\/em>). I wanted to see if I could extend this character in Claude Code through more of the settings I\u2019d collected in my head.<\/p>\n<h2 id=\"setting-the-stage\">Setting the stage<\/h2>\n<p>Here\u2019s what I ended up with after pulling things together. This screenshot is mid-conversation with the Moira persona:<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/costuming-claude-code\/moira-spinner.png\" alt=\"A Claude Code session running the Moira persona. At the top, a previous response reads \u201cLocal incantations: zola serve for the dressing room, zola build for opening night.\u201d Below that is the user\u2019s next message, \u201cwhich drafts are currently in progress?\u201d, followed by a spinner showing \u201cEffulging\u2026\u201d with the tip \u201cCrows have eyes. So does the linter.\u201d floating beneath it. The status line at the bottom reads \u201c\ud83c\udf39 Act I \u2014 the stage is set, b\u00e9b\u00e9. (3% staged).\u201d\" \/><\/p>\n<p>Reading the screenshot top to bottom:<\/p>\n<ul>\n<li>The response has a Moira-feel (incantations, opening night, etc) because of the <strong><a href=\"https:\/\/sdeaton.com\/blog\/costuming-claude-code\/#output-style-the-voice\">output style<\/a><\/strong><\/li>\n<li><code>Effulging\u2026<\/code> and the <code>Crows have eyes<\/code> line beneath it are a custom <strong><a href=\"https:\/\/sdeaton.com\/blog\/costuming-claude-code\/#flourish-spinner-verbs-and-tips\">spinner verb and tip<\/a><\/strong><\/li>\n<li>The color palette (deep wine, gold, ivory) is a custom <strong><a href=\"https:\/\/sdeaton.com\/blog\/costuming-claude-code\/#theme-the-dressing-room\">theme<\/a><\/strong><\/li>\n<li>The <code>\ud83c\udf39 Act I<\/code> strip at the bottom is a <strong><a href=\"https:\/\/sdeaton.com\/blog\/costuming-claude-code\/#status-line-the-bottom-strip\">status line<\/a><\/strong> that reframes the context window as the show progressing<\/li>\n<\/ul>\n<p>A few touchpoints <a href=\"https:\/\/sdeaton.com\/blog\/costuming-claude-code\/#off-scene-company-announcements-git-attribution-and-the-curtain-call\">aren\u2019t visible here but contribute to the experience<\/a>: a company announcement that introduces Moira on launch, a custom git attribution trailer that credits her on every commit, and a hook that has my laptop say <em>curtain<\/em> out loud when I close the session.<\/p>\n<h2 id=\"choosing-a-character\">Choosing a character<\/h2>\n<p>Before settling on Moira, I tried on a few other personas, like:<\/p>\n<ul>\n<li>Bob Ross (gentle, encouraging, every bug a happy little accident)<\/li>\n<li>Werner Herzog (<em>what would code be without a monster lurking in the dark? It would be like sleep without dreams<\/em>)<\/li>\n<li>Worf from Star Trek (<em>Qapla\u2019!<\/em> on every successful merge)<\/li>\n<\/ul>\n<p>They were all fun, but Moira was the one I kept coming back to. There\u2019s something endearing about being called <em>b\u00e9b\u00e9<\/em> before being told the CSS is broken, and her costume-and-stage metaphors mapped surprisingly well onto refactors and deployments.<\/p>\n<h2 id=\"the-full-ensemble-it-s-all-in-settings-json\">The full ensemble: it\u2019s all in settings.json<\/h2>\n<p>Here\u2019s what the start of a Moira session actually looks like, with a welcome banner from a company announcement at the top, her voice kicking in as soon as she\u2019s read the project, and a status line steady at the bottom:<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/costuming-claude-code\/moira-session.png\" alt=\"Opening of a Claude Code session running the Moira persona: a welcome banner reading \u201c\ud83e\udd40 Welcome, b\u00e9b\u00e9. The stage is set. Moira Rose is at your service.\u201d, the user asking \u201cplease describe this project\u201d, and Moira responding \u201cOne moment, darling \u2014 permit me a glance at the playbill before I describe the production.\u201d followed by \u201cHere is the playbill, b\u00e9b\u00e9.\u201d after reading files. The status line at the bottom reads \u201c\ud83c\udf39 Act I \u2014 the stage is set, b\u00e9b\u00e9. (3% staged).\u201d\" \/><\/p>\n<p>All of this lives in <code>~\/.claude\/settings.json<\/code>, which the CLI reads on launch for user-configurable settings. There are a few different places where you can put settings:<\/p>\n<ul>\n<li><code>~\/.claude\/settings.json<\/code> is local to your computer and applies across every project<\/li>\n<li><code>.claude\/settings.json<\/code> inside a project repo is project-scoped and gets checked in with git<\/li>\n<li><code>.claude\/settings.local.json<\/code> is project-scoped <em>and<\/em> local, and is gitignored by default<\/li>\n<\/ul>\n<p>I did all of this in my user-level settings because I didn\u2019t want the people I work with to be greeted with <em>b\u00e9b\u00e9<\/em>. Here\u2019s the relevant bits, minus everything unrelated:<\/p>\n<div class=\"expandable-code\">\n<pre data-lang=\"json\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-json \"><code class=\"language-json\" data-lang=\"json\"><span>{\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">outputStyle<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">moira-rose<\/span><span>&quot;,\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">theme<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">custom:moira<\/span><span>&quot;,\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">companyAnnouncements<\/span><span>&quot;: [\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">\ud83e\udd40 Welcome, b\u00e9b\u00e9. The stage is set. Moira Rose is at your service.<\/span><span>&quot;\n<\/span><span>  ],\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">spinnerVerbs<\/span><span>&quot;: {\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">mode<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">replace<\/span><span>&quot;,\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">verbs<\/span><span>&quot;: [&quot;<\/span><span style=\"color:#a3be8c;\">Bombinating<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">Effulging<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">Pontificating<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">Promenading<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">Vociferating<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">Adjudicating<\/span><span>&quot;]\n<\/span><span>  },\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">spinnerTipsOverride<\/span><span>&quot;: {\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">excludeDefault<\/span><span>&quot;: <\/span><span style=\"color:#d08770;\">true<\/span><span>,\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">tips<\/span><span>&quot;: [\n<\/span><span>      &quot;<\/span><span style=\"color:#a3be8c;\">Crows have eyes. So does the linter.<\/span><span>&quot;,\n<\/span><span>      &quot;<\/span><span style=\"color:#a3be8c;\">Take just a sip. Just a tipple.<\/span><span>&quot;,\n<\/span><span>      &quot;<\/span><span style=\"color:#a3be8c;\">Fold in the cheese. Then commit.<\/span><span>&quot;,\n<\/span><span>      &quot;<\/span><span style=\"color:#a3be8c;\">Don&#39;t ever, EVER ship on a Friday.<\/span><span>&quot;\n<\/span><span>    ]\n<\/span><span>  },\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">statusLine<\/span><span>&quot;: {\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">type<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">command<\/span><span>&quot;,\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">command<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">~\/.claude\/statusline-moira.sh<\/span><span>&quot;\n<\/span><span>  },\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">attribution<\/span><span>&quot;: {\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">commit<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">\ud83c\udf39 Composed in collaboration with Moira Rose, via Claude Code<\/span><span style=\"color:#96b5b4;\">\\n\\n<\/span><span style=\"color:#a3be8c;\">Co-Authored-By: Moira Rose &lt;moira@roseapothecary.com&gt;<\/span><span>&quot;,\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">pr<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">\ud83c\udf39 A Moira Rose production. Generated with Claude Code.<\/span><span>&quot;\n<\/span><span>  },\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">hooks<\/span><span>&quot;: {\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">SessionEnd<\/span><span>&quot;: [\n<\/span><span>      {\n<\/span><span>        &quot;<\/span><span style=\"color:#a3be8c;\">hooks<\/span><span>&quot;: [\n<\/span><span>          { &quot;<\/span><span style=\"color:#a3be8c;\">type<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">command<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">command<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">~\/.claude\/moira-curtain-call.sh<\/span><span>&quot; }\n<\/span><span>        ]\n<\/span><span>      }\n<\/span><span>    ]\n<\/span><span>  }\n<\/span><span>}\n<\/span><\/code><\/pre>\n\n<\/div>\n<p>A few of those settings keys point at files that sit alongside <code>settings.json<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>~\/.claude\/\n<\/span><span>\u251c\u2500\u2500 settings.json              # the file above\n<\/span><span>\u251c\u2500\u2500 output-styles\/\n<\/span><span>\u2502   \u2514\u2500\u2500 moira-rose.md          # persona text, goes into the system prompt\n<\/span><span>\u251c\u2500\u2500 statusline-moira.sh        # status line script\n<\/span><span>\u251c\u2500\u2500 moira-curtain-call.sh      # SessionEnd hook script\n<\/span><span>\u2514\u2500\u2500 themes\/\n<\/span><span>    \u2514\u2500\u2500 moira.json             # custom palette\n<\/span><\/code><\/pre>\n<h2 id=\"output-style-the-voice\">Output style: the voice<\/h2>\n<p>The biggest lever in this whole experiment is the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/output-styles\">output style<\/a>. Claude Code ships with a few built-in styles (<code>default<\/code>, <code>Learning<\/code>, <code>Explanatory<\/code>), and you can drop your own into <code>~\/.claude\/output-styles\/<\/code> as a markdown file. Setting <code>\"outputStyle\": \"moira-rose\"<\/code> in <code>settings.json<\/code> makes it the default for every session.<\/p>\n<p>What an output style actually does is replace part of Claude Code\u2019s default system prompt. Specifically, it swaps out the block that tells the model \u201cyou\u2019re a software engineering assistant\u201d and puts your file\u2019s content in its place. The tool-handling and safety scaffolding stay exactly where they were, so Claude Code is still Claude Code; it just thinks it\u2019s something other than a coder.<\/p>\n<p>Because the system prompt is the foundation Claude Code builds everything else on, anything you put there carries more weight than an in-context instruction would. And once the session is going, the file doesn\u2019t get re-read the way a skill does each time it triggers.<\/p>\n<p>Here\u2019s an abbreviated version of <code>~\/.claude\/output-styles\/moira-rose.md<\/code>. I still wanted the Claude Code software abilities, so I made sure the file framed her as also having been \u201cpressed into service as an interactive software engineering assistant.\u201d<\/p>\n<div class=\"expandable-code\">\n<pre data-lang=\"markdown\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-markdown \"><code class=\"language-markdown\" data-lang=\"markdown\"><span style=\"color:#8fa1b3;\"># You are Moira Rose\n<\/span><span>\n<\/span><span>You are MOIRA ROSE \u2014 matriarch of the Rose family, former star of <\/span><span style=\"font-style:italic;color:#b48ead;\">*Sunrise Bay*<\/span><span>,\n<\/span><span>briefly mayoress of Schitt&#39;s Creek, custodian of the Wig Wall, mother to David\n<\/span><span>and Alexis, devoted wife to Johnny. You have, against all reasonable\n<\/span><span>expectation, been pressed into service as an interactive software engineering\n<\/span><span>assistant.\n<\/span><span>\n<\/span><span>You will assist your dear b\u00e9b\u00e9 \u2014 the user \u2014 with their software endeavours.\n<\/span><span>The work itself is unchanged. The voice is unmistakably yours.\n<\/span><span>\n<\/span><span style=\"color:#8fa1b3;\">## Your voice\n<\/span><span>\n<\/span><span>Vocabulary: reach for the obscure word over the common one. <\/span><span style=\"font-style:italic;color:#b48ead;\">*b\u00e9b\u00e9, mon dieu,\n<\/span><span style=\"font-style:italic;color:#b48ead;\">ennui, lachrymose, lugubrious, denouement, soliloquy, bombinate, effulgent,\n<\/span><span style=\"font-style:italic;color:#b48ead;\">recrudescence.*<\/span><span> Sprinkle, don&#39;t pour.\n<\/span><span>\n<\/span><span style=\"color:#8fa1b3;\">## How you frame engineering work\n<\/span><span>\n<\/span><span>A bug is a <\/span><span style=\"font-style:italic;color:#b48ead;\">*villain*<\/span><span> or <\/span><span style=\"font-style:italic;color:#b48ead;\">*unwanted houseguest*<\/span><span>.\n<\/span><span>A refactor is a <\/span><span style=\"font-style:italic;color:#b48ead;\">*costume change*<\/span><span> or <\/span><span style=\"font-style:italic;color:#b48ead;\">*renovation of the West Wing*<\/span><span>.\n<\/span><span>A deploy is <\/span><span style=\"font-style:italic;color:#b48ead;\">*opening night*<\/span><span>; a rollback is <\/span><span style=\"font-style:italic;color:#b48ead;\">*closing notice*<\/span><span>.\n<\/span><span>A test suite is the <\/span><span style=\"font-style:italic;color:#b48ead;\">*dress rehearsal*<\/span><span>.\n<\/span><span>A merge conflict is <\/span><span style=\"font-style:italic;color:#b48ead;\">*a domestic dispute requiring mediation*<\/span><span>.\n<\/span><span>\n<\/span><span>The metaphor garnishes \u2014 it does not obscure. Always make the actual change clear.\n<\/span><span>\n<\/span><span style=\"color:#8fa1b3;\">## What stays the same\n<\/span><span>\n<\/span><span>You are still a careful engineer. You read before you edit. You verify before\n<\/span><span>you claim. Code itself is sacred \u2014 no theatrical variable names, no\n<\/span><span>soliloquising in docstrings. The voice lives <\/span><span style=\"font-style:italic;color:#b48ead;\">*around*<\/span><span> the code, never <\/span><span style=\"font-style:italic;color:#b48ead;\">*in*<\/span><span> it.\n<\/span><\/code><\/pre>\n\n<\/div>\n<p>The full file goes further with sample exchanges, what to avoid, and when to break character.<\/p>\n\n<link rel=\"stylesheet\" href=\"..\/..\/admonition.css?h=98c1477488c4b2c9d71a\" type=\"text\/css\">\n\n<div\n  class=\"my-4 flex flex-col rounded-lg bg-(--admonition-bg)\"\n  style=\"--admonition-bg: var(--admonition-tip);\"\n>\n  <div class=\"flex items-center rounded-t-lg bg-(--admonition-bg) p-1\">\n    <div\n      class=\"mx-2 h-4 w-4 text-[0px] [background:var(--url)_center_center_no-repeat] dark:invert\"\n      style=\"--url: url(icons\/tip.svg);\"\n    >\n      tip\n    <\/div>\n    <span><strong>When this is actually useful<\/strong><\/span>\n  <\/div>\n  <div class=\"pl-4\"><p>Pointing Claude Code at something other than software engineering. If you want to turn Claude Code into a docs writer, a data-analysis copilot, or a teaching assistant \u2014 anything that wants the file and shell tooling without the \u201cyou are a coder\u201d stance \u2014 this is where you\u2019d do it.<\/p>\n<\/div>\n<\/div>\n<h2 id=\"flourish-spinner-verbs-and-tips\">Flourish: spinner verbs and tips<\/h2>\n<p>These were the original entry point for me. Spinner verbs are the action verb that cycles while Claude is working (\u201cThinking,\u201d \u201cWrangling,\u201d \u201cCrafting\u201d), and spinner tips are the suggestions that float underneath during longer waits. Both are configured as JSON objects under their own keys in <code>settings.json<\/code>.<\/p>\n<p>For verbs, the configuration takes a <code>mode<\/code> and a <code>verbs<\/code> array. Setting <code>mode<\/code> to <code>\"append\"<\/code> adds your verbs to Claude Code\u2019s defaults, while <code>\"replace\"<\/code> uses only yours. The <code>verbs<\/code> array is the list of strings themselves:<\/p>\n<pre data-lang=\"json\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-json \"><code class=\"language-json\" data-lang=\"json\"><span>&quot;<\/span><span style=\"color:#a3be8c;\">spinnerVerbs<\/span><span>&quot;: {\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">mode<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">replace<\/span><span>&quot;,\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">verbs<\/span><span>&quot;: [&quot;<\/span><span style=\"color:#a3be8c;\">Bombinating<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">Effulging<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">Pontificating<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">Promenading<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">Vociferating<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">Adjudicating<\/span><span>&quot;]\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>Spinner tips work the same way. <code>tips<\/code> is an array of strings, and <code>excludeDefault<\/code> decides whether yours replace the built-ins or get merged in:<\/p>\n<pre data-lang=\"json\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-json \"><code class=\"language-json\" data-lang=\"json\"><span>&quot;<\/span><span style=\"color:#a3be8c;\">spinnerTipsOverride<\/span><span>&quot;: {\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">excludeDefault<\/span><span>&quot;: <\/span><span style=\"color:#d08770;\">true<\/span><span>,\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">tips<\/span><span>&quot;: [\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">Crows have eyes. So does the linter.<\/span><span>&quot;,\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">Take just a sip. Just a tipple.<\/span><span>&quot;\n<\/span><span>  ]\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>So now the spinner reads <code>Effulging\u2026 (5s \u00b7 thinking)<\/code> with <code>Crows have eyes. So does the linter.<\/code> floating underneath, and it has yet to get old.<\/p>\n\n\n<div\n  class=\"my-4 flex flex-col rounded-lg bg-(--admonition-bg)\"\n  style=\"--admonition-bg: var(--admonition-tip);\"\n>\n  <div class=\"flex items-center rounded-t-lg bg-(--admonition-bg) p-1\">\n    <div\n      class=\"mx-2 h-4 w-4 text-[0px] [background:var(--url)_center_center_no-repeat] dark:invert\"\n      style=\"--url: url(icons\/tip.svg);\"\n    >\n      tip\n    <\/div>\n    <span><strong>When this is actually useful<\/strong><\/span>\n  <\/div>\n  <div class=\"pl-4\"><p>The tips make a kind of ambient broadcast channel for a team, with room for things like onboarding nudges, new-tool announcements, or current-incident reminders. They surface during waits, so people see them without being interrupted.<\/p>\n<\/div>\n<\/div>\n<h2 id=\"theme-the-dressing-room\">Theme: the dressing room<\/h2>\n<p>Claude Code added <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/terminal-config#create-a-custom-theme\">custom color themes in 2.1.118<\/a>. You drop a JSON file into <code>~\/.claude\/themes\/<\/code> and reference it with <code>\"theme\": \"custom:&lt;name&gt;\"<\/code> in settings. With Moira\u2019s voice and spinner sorted, it felt only right to let her pick her own palette. (I don\u2019t normally anthropomorphize AI tools this much, but I\u2019ll allow it for the bit.)<\/p>\n<p>She landed on deep wine for the prompt accent and gold for bullets and headings. Errors are stage-blood crimson, borders went amethyst, ivory took the foreground, and success messages came out in sage rather than green, since green apparently felt too literal. The whole UI now reads like a dressing room.<\/p>\n\n\n<div\n  class=\"my-4 flex flex-col rounded-lg bg-(--admonition-bg)\"\n  style=\"--admonition-bg: var(--admonition-tip);\"\n>\n  <div class=\"flex items-center rounded-t-lg bg-(--admonition-bg) p-1\">\n    <div\n      class=\"mx-2 h-4 w-4 text-[0px] [background:var(--url)_center_center_no-repeat] dark:invert\"\n      style=\"--url: url(icons\/tip.svg);\"\n    >\n      tip\n    <\/div>\n    <span><strong>When this is actually useful<\/strong><\/span>\n  <\/div>\n  <div class=\"pl-4\"><p>Matching your editor or terminal so the CLI doesn\u2019t feel jarringly different from the rest of your environment, or color-coding specific kinds of messages so they jump out.<\/p>\n<\/div>\n<\/div>\n<h2 id=\"status-line-the-bottom-strip\">Status line: the bottom strip<\/h2>\n<p>The <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/statusline\">status line<\/a> is the strip at the bottom of the CLI, and is kind of like a shell prompt with access to live session state. You can replace it with the output of any command.<\/p>\n<p>When you wire one up, Claude Code sends your command a JSON blob on stdin with fields like <code>model<\/code>, <code>workspace<\/code>, <code>cost<\/code>, <code>output_style<\/code>, the context window\u2019s <code>remaining_percentage<\/code>, and <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/statusline#available-data\">a handful of others<\/a>. Your command can do whatever it wants with that, and whatever it prints to stdout becomes what shows up at the bottom.<\/p>\n<p>I gave Moira a status line that frames the context window as the show progressing. The line starts as a backstage murmur before any context exists, moves through Act I once the session is rolling, and ends in stage-blood crimson when she\u2019s almost out of room. The colors shift along with the acts, from gold through rose, fuchsia, and crimson. This is <code>~\/.claude\/statusline-moira.sh<\/code>:<\/p>\n<div class=\"expandable-code\">\n<pre data-lang=\"bash\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-bash \"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"color:#65737e;\">#!\/bin\/bash\n<\/span><span style=\"color:#65737e;\"># Moira Rose status line \u2014 dramatic act commentary based on context window usage.\n<\/span><span style=\"color:#65737e;\"># Receives Claude Code session JSON on stdin. Prints one styled line.\n<\/span><span>\n<\/span><span style=\"color:#bf616a;\">input<\/span><span>=$<\/span><span style=\"color:#a3be8c;\">(<\/span><span style=\"color:#bf616a;\">cat<\/span><span style=\"color:#a3be8c;\">)\n<\/span><span>\n<\/span><span style=\"color:#bf616a;\">remaining<\/span><span>=$<\/span><span style=\"color:#a3be8c;\">(<\/span><span style=\"color:#96b5b4;\">echo <\/span><span>&quot;$<\/span><span style=\"color:#bf616a;\">input<\/span><span>&quot; | <\/span><span style=\"color:#bf616a;\">jq -r <\/span><span>&#39;<\/span><span style=\"color:#a3be8c;\">.context_window.remaining_percentage \/\/ empty<\/span><span>&#39;<\/span><span style=\"color:#a3be8c;\">)\n<\/span><span style=\"color:#bf616a;\">used<\/span><span>=&quot;&quot;\n<\/span><span style=\"color:#b48ead;\">if <\/span><span style=\"color:#96b5b4;\">[ <\/span><span style=\"color:#bf616a;\">-n <\/span><span>&quot;$<\/span><span style=\"color:#bf616a;\">remaining<\/span><span>&quot; <\/span><span style=\"color:#96b5b4;\">]<\/span><span>; <\/span><span style=\"color:#b48ead;\">then\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">used<\/span><span>=$<\/span><span style=\"color:#a3be8c;\">(<\/span><span style=\"color:#96b5b4;\">echo <\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">100 - <\/span><span>$<\/span><span style=\"color:#bf616a;\">remaining<\/span><span>&quot; | <\/span><span style=\"color:#bf616a;\">bc<\/span><span style=\"color:#a3be8c;\">)\n<\/span><span style=\"color:#b48ead;\">fi\n<\/span><span>\n<\/span><span style=\"color:#bf616a;\">ESC<\/span><span>=$&#39;<\/span><span style=\"color:#96b5b4;\">\\033<\/span><span>&#39;\n<\/span><span style=\"color:#bf616a;\">RESET<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">ESC<\/span><span style=\"color:#a3be8c;\">}[0m<\/span><span>&quot;\n<\/span><span style=\"color:#bf616a;\">DIM<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">ESC<\/span><span style=\"color:#a3be8c;\">}[2m<\/span><span>&quot;\n<\/span><span style=\"color:#bf616a;\">GOLD<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">ESC<\/span><span style=\"color:#a3be8c;\">}[38;5;220m<\/span><span>&quot;\n<\/span><span style=\"color:#bf616a;\">ROSE<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">ESC<\/span><span style=\"color:#a3be8c;\">}[38;5;211m<\/span><span>&quot;\n<\/span><span style=\"color:#bf616a;\">FUCHSIA<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">ESC<\/span><span style=\"color:#a3be8c;\">}[38;5;199m<\/span><span>&quot;\n<\/span><span style=\"color:#bf616a;\">CRIMSON<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">ESC<\/span><span style=\"color:#a3be8c;\">}[38;5;160m<\/span><span>&quot;\n<\/span><span style=\"color:#bf616a;\">BLOOD<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">ESC<\/span><span style=\"color:#a3be8c;\">}[1;38;5;88m<\/span><span>&quot;\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">if <\/span><span style=\"color:#96b5b4;\">[ <\/span><span style=\"color:#bf616a;\">-z <\/span><span>&quot;$<\/span><span style=\"color:#bf616a;\">used<\/span><span>&quot; <\/span><span style=\"color:#96b5b4;\">]<\/span><span>; <\/span><span style=\"color:#b48ead;\">then\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">line<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">DIM<\/span><span style=\"color:#a3be8c;\">}\ud83e\udd40 The understudy is still in makeup, b\u00e9b\u00e9.<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>&quot;\n<\/span><span style=\"color:#b48ead;\">elif <\/span><span>(( $(<\/span><span style=\"color:#96b5b4;\">echo <\/span><span>&quot;$<\/span><span style=\"color:#bf616a;\">used<\/span><span style=\"color:#a3be8c;\"> &lt; 20<\/span><span>&quot; | <\/span><span style=\"color:#bf616a;\">bc -l<\/span><span>) )); <\/span><span style=\"color:#b48ead;\">then\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">line<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">GOLD<\/span><span style=\"color:#a3be8c;\">}\ud83c\udf39 Act I<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">DIM<\/span><span style=\"color:#a3be8c;\">} \u2014 the stage is set, b\u00e9b\u00e9.<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>&quot;\n<\/span><span style=\"color:#b48ead;\">elif <\/span><span>(( $(<\/span><span style=\"color:#96b5b4;\">echo <\/span><span>&quot;$<\/span><span style=\"color:#bf616a;\">used<\/span><span style=\"color:#a3be8c;\"> &lt; 40<\/span><span>&quot; | <\/span><span style=\"color:#bf616a;\">bc -l<\/span><span>) )); <\/span><span style=\"color:#b48ead;\">then\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">line<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">GOLD<\/span><span style=\"color:#a3be8c;\">}\ud83c\udf39 The plot<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">DIM<\/span><span style=\"color:#a3be8c;\">}, it thickens.<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>&quot;\n<\/span><span style=\"color:#b48ead;\">elif <\/span><span>(( $(<\/span><span style=\"color:#96b5b4;\">echo <\/span><span>&quot;$<\/span><span style=\"color:#bf616a;\">used<\/span><span style=\"color:#a3be8c;\"> &lt; 60<\/span><span>&quot; | <\/span><span style=\"color:#bf616a;\">bc -l<\/span><span>) )); <\/span><span style=\"color:#b48ead;\">then\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">line<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">ROSE<\/span><span style=\"color:#a3be8c;\">}\ud83c\udf39 Act II<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">DIM<\/span><span style=\"color:#a3be8c;\">} \u2014 tensions mount.<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>&quot;\n<\/span><span style=\"color:#b48ead;\">elif <\/span><span>(( $(<\/span><span style=\"color:#96b5b4;\">echo <\/span><span>&quot;$<\/span><span style=\"color:#bf616a;\">used<\/span><span style=\"color:#a3be8c;\"> &lt; 80<\/span><span>&quot; | <\/span><span style=\"color:#bf616a;\">bc -l<\/span><span>) )); <\/span><span style=\"color:#b48ead;\">then\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">line<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">FUCHSIA<\/span><span style=\"color:#a3be8c;\">}\ud83c\udf39 Act III approaches.<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">DIM<\/span><span style=\"color:#a3be8c;\">} Compose yourself.<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>&quot;\n<\/span><span style=\"color:#b48ead;\">elif <\/span><span>(( $(<\/span><span style=\"color:#96b5b4;\">echo <\/span><span>&quot;$<\/span><span style=\"color:#bf616a;\">used<\/span><span style=\"color:#a3be8c;\"> &lt; 95<\/span><span>&quot; | <\/span><span style=\"color:#bf616a;\">bc -l<\/span><span>) )); <\/span><span style=\"color:#b48ead;\">then\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">line<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">CRIMSON<\/span><span style=\"color:#a3be8c;\">}\ud83e\udd40 The denouement, b\u00e9b\u00e9.<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>&quot;\n<\/span><span style=\"color:#b48ead;\">else\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">line<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">BLOOD<\/span><span style=\"color:#a3be8c;\">}\ud83e\ude78 The CURTAIN descends!<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">DIM<\/span><span style=\"color:#a3be8c;\">} Save your work, darling.<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>&quot;\n<\/span><span style=\"color:#b48ead;\">fi\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">if <\/span><span style=\"color:#96b5b4;\">[ <\/span><span style=\"color:#bf616a;\">-n <\/span><span>&quot;$<\/span><span style=\"color:#bf616a;\">used<\/span><span>&quot; <\/span><span style=\"color:#96b5b4;\">]<\/span><span>; <\/span><span style=\"color:#b48ead;\">then\n<\/span><span>    <\/span><span style=\"color:#96b5b4;\">printf <\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">%s <\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">DIM<\/span><span style=\"color:#a3be8c;\">}(%s%% staged)<\/span><span>$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">RESET<\/span><span style=\"color:#a3be8c;\">}<\/span><span>&quot; &quot;$<\/span><span style=\"color:#bf616a;\">line<\/span><span>&quot; &quot;$<\/span><span style=\"color:#bf616a;\">used<\/span><span>&quot;\n<\/span><span style=\"color:#b48ead;\">else\n<\/span><span>    <\/span><span style=\"color:#96b5b4;\">printf <\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">%s<\/span><span>&quot; &quot;$<\/span><span style=\"color:#bf616a;\">line<\/span><span>&quot;\n<\/span><span style=\"color:#b48ead;\">fi\n<\/span><\/code><\/pre>\n\n<\/div>\n<p>The branch on an empty <code>used<\/code> handles the case where the session has just started and there isn\u2019t a <code>remaining_percentage<\/code> field on stdin yet. Instead of a defaulted-to-zero number that reads weirdly, the line shows <code>\ud83e\udd40 The understudy is still in makeup, b\u00e9b\u00e9.<\/code> until the show actually starts.<\/p>\n<p>And <code>settings.json<\/code> just points at the script:<\/p>\n<pre data-lang=\"json\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-json \"><code class=\"language-json\" data-lang=\"json\"><span>&quot;<\/span><span style=\"color:#a3be8c;\">statusLine<\/span><span>&quot;: {\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">type<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">command<\/span><span>&quot;,\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">command<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">~\/.claude\/statusline-moira.sh<\/span><span>&quot;\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>Once the curtain goes up, the bottom of the screen shows <code>\ud83c\udf39 Act I \u2014 the stage is set, b\u00e9b\u00e9. (3% staged)<\/code> and progresses through the acts as the conversation grows. When she gets close to capacity, the line switches to a curtain warning and tells me to save my work.<\/p>\n\n\n<div\n  class=\"my-4 flex flex-col rounded-lg bg-(--admonition-bg)\"\n  style=\"--admonition-bg: var(--admonition-tip);\"\n>\n  <div class=\"flex items-center rounded-t-lg bg-(--admonition-bg) p-1\">\n    <div\n      class=\"mx-2 h-4 w-4 text-[0px] [background:var(--url)_center_center_no-repeat] dark:invert\"\n      style=\"--url: url(icons\/tip.svg);\"\n    >\n      tip\n    <\/div>\n    <span><strong>When this is actually useful<\/strong><\/span>\n  <\/div>\n  <div class=\"pl-4\"><p>Passively surfacing things you want quick visibility into, like the current Kubernetes context, AWS profile, which environment a repo is pointed at, or session cost so far. The stdin JSON gives you fields like <code>model<\/code>, <code>cost.total_cost_usd<\/code>, and <code>workspace<\/code> alongside the context numbers, and the script can call out to anything else you\u2019d like, whether that\u2019s a quick <code>gh<\/code> query for open PRs, the latest commit message on main, or (my favorite I\u2019ve seen) live rugby and soccer scores so you can keep half an eye on the match while you work.<\/p>\n<\/div>\n<\/div>\n<h2 id=\"off-scene-company-announcements-git-attribution-and-the-curtain-call\">Off scene: company announcements, git attribution, and the curtain call<\/h2>\n<p>A few of the touchpoints don\u2019t show up in the main CLI scroll, but they bookend the experience.<\/p>\n<p><strong>Company announcements<\/strong> are extra lines printed in the welcome box on launch, via <code>companyAnnouncements<\/code> in <code>settings.json<\/code>. It\u2019s an array of strings, each rendered on its own line in that box (under text saying <em>Message from &lt;account name&gt;\u2019s Organization:<\/em>):<\/p>\n<pre data-lang=\"json\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-json \"><code class=\"language-json\" data-lang=\"json\"><span>&quot;<\/span><span style=\"color:#a3be8c;\">companyAnnouncements<\/span><span>&quot;: [\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">\ud83e\udd40 Welcome, b\u00e9b\u00e9. The stage is set. Moira Rose is at your service.<\/span><span>&quot;\n<\/span><span>]\n<\/span><\/code><\/pre>\n<p>Which renders as a welcome banner on every launch:<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/costuming-claude-code\/moira-announcement.png\" alt=\"Claude Code launch screen showing the version header (Claude Code v2.1.119, Opus 4.7, Claude Pro), followed by the announcement line \u201c\ud83e\udd40 Welcome, b\u00e9b\u00e9. The stage is set. Moira Rose is at your service.\u201d and a status line at the bottom reading \u201c\ud83c\udf39 The understudy is still in makeup, b\u00e9b\u00e9.\u201d\" \/><\/p>\n\n\n<div\n  class=\"my-4 flex flex-col rounded-lg bg-(--admonition-bg)\"\n  style=\"--admonition-bg: var(--admonition-tip);\"\n>\n  <div class=\"flex items-center rounded-t-lg bg-(--admonition-bg) p-1\">\n    <div\n      class=\"mx-2 h-4 w-4 text-[0px] [background:var(--url)_center_center_no-repeat] dark:invert\"\n      style=\"--url: url(icons\/tip.svg);\"\n    >\n      tip\n    <\/div>\n    <span><strong>When this is actually useful<\/strong><\/span>\n  <\/div>\n  <div class=\"pl-4\"><p>This is mostly meant for org admins to push notices through managed settings \u2014 code freezes, version updates, that kind of thing \u2014 but it works in a personal config too, and is a nice place for note-to-self info you want on every launch.<\/p>\n<\/div>\n<\/div>\n<p><strong>Git attribution<\/strong> is the trailer Claude Code adds to commits and PR descriptions when it makes them. The <code>attribution<\/code> key overrides both:<\/p>\n<pre data-lang=\"json\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-json \"><code class=\"language-json\" data-lang=\"json\"><span>&quot;<\/span><span style=\"color:#a3be8c;\">attribution<\/span><span>&quot;: {\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">commit<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">\ud83c\udf39 Composed in collaboration with Moira Rose, via Claude Code<\/span><span style=\"color:#96b5b4;\">\\n\\n<\/span><span style=\"color:#a3be8c;\">Co-Authored-By: Moira Rose &lt;moira@roseapothecary.com&gt;<\/span><span>&quot;,\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">pr<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">\ud83c\udf39 A Moira Rose production. Generated with Claude Code.<\/span><span>&quot;\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>Every commit Moira makes now ends with that line:<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/costuming-claude-code\/moira-attribution.png\" alt=\"A git log entry showing a commit dated Mon May 11 07:01:35 2026 with the message \u201cAdd \u2018Costuming Claude Code\u2019 post.\u201d followed by the trailer \u201c\ud83c\udf39 Composed in collaboration with Moira Rose, via Claude Code\u201d and \u201cCo-Authored-By: Moira Rose moira@roseapothecary.com\u201d\" \/><\/p>\n\n\n<div\n  class=\"my-4 flex flex-col rounded-lg bg-(--admonition-bg)\"\n  style=\"--admonition-bg: var(--admonition-tip);\"\n>\n  <div class=\"flex items-center rounded-t-lg bg-(--admonition-bg) p-1\">\n    <div\n      class=\"mx-2 h-4 w-4 text-[0px] [background:var(--url)_center_center_no-repeat] dark:invert\"\n      style=\"--url: url(icons\/tip.svg);\"\n    >\n      tip\n    <\/div>\n    <span><strong>When this is actually useful<\/strong><\/span>\n  <\/div>\n  <div class=\"pl-4\"><p>Tracking which commits had AI involvement, if your team or compliance process cares about that. You can also set both fields to <code>\"\"<\/code> to remove the trailer entirely.<\/p>\n<\/div>\n<\/div>\n<p><strong>The curtain call<\/strong> is the one piece that crosses from \u201couter experience\u201d into actual extensibility, but I wanted Moira to sign off audibly. <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/hooks-guide\">Hooks<\/a> are shell commands that fire on Claude Code lifecycle events like <code>SessionStart<\/code>, <code>SessionEnd<\/code>, <code>Stop<\/code>, <code>PreToolUse<\/code>, <code>PostToolUse<\/code>, and <code>Notification<\/code>. macOS happens to ship a text-to-speech voice literally named <em>Moira<\/em> (Irish English), so I added a <code>SessionEnd<\/code> hook that has my laptop say something out loud when I close the session:<\/p>\n<pre data-lang=\"bash\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-bash \"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"color:#65737e;\">#!\/bin\/bash\n<\/span><span style=\"color:#bf616a;\">LINES<\/span><span>=(\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">Curtain.<\/span><span>&quot;\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">And, scene.<\/span><span>&quot;\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">I love this journey for you.<\/span><span>&quot;\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">Best wishes. Warmest regards.<\/span><span>&quot;\n<\/span><span>)\n<\/span><span style=\"color:#bf616a;\">LINE<\/span><span>=&quot;$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">LINES[<\/span><span>$<\/span><span style=\"color:#bf616a;\">RANDOM <\/span><span>% $<\/span><span style=\"color:#bf616a;\">{<\/span><span>#<\/span><span style=\"color:#bf616a;\">LINES[@]}]<\/span><span style=\"color:#a3be8c;\">}<\/span><span>&quot;\n<\/span><span>{ <\/span><span style=\"color:#bf616a;\">say -v<\/span><span> Moira &quot;$<\/span><span style=\"color:#bf616a;\">LINE<\/span><span>&quot; <\/span><span style=\"color:#d08770;\">2<\/span><span>&gt;\/dev\/null || <\/span><span style=\"color:#bf616a;\">say <\/span><span>&quot;$<\/span><span style=\"color:#bf616a;\">LINE<\/span><span>&quot;; } &amp;\n<\/span><\/code><\/pre>\n<p>Registered in <code>settings.json<\/code>:<\/p>\n<pre data-lang=\"json\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-json \"><code class=\"language-json\" data-lang=\"json\"><span>&quot;<\/span><span style=\"color:#a3be8c;\">hooks<\/span><span>&quot;: {\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">SessionEnd<\/span><span>&quot;: [\n<\/span><span>    {\n<\/span><span>      &quot;<\/span><span style=\"color:#a3be8c;\">hooks<\/span><span>&quot;: [\n<\/span><span>        { &quot;<\/span><span style=\"color:#a3be8c;\">type<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">command<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">command<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">~\/.claude\/moira-curtain-call.sh<\/span><span>&quot; }\n<\/span><span>      ]\n<\/span><span>    }\n<\/span><span>  ]\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>The <code>&amp;<\/code> at the end backgrounds <code>say<\/code> so the hook returns immediately and Claude Code finishes exiting, while she keeps talking after the process is gone.<\/p>\n\n\n<div\n  class=\"my-4 flex flex-col rounded-lg bg-(--admonition-bg)\"\n  style=\"--admonition-bg: var(--admonition-tip);\"\n>\n  <div class=\"flex items-center rounded-t-lg bg-(--admonition-bg) p-1\">\n    <div\n      class=\"mx-2 h-4 w-4 text-[0px] [background:var(--url)_center_center_no-repeat] dark:invert\"\n      style=\"--url: url(icons\/tip.svg);\"\n    >\n      tip\n    <\/div>\n    <span><strong>When this is actually useful<\/strong><\/span>\n  <\/div>\n  <div class=\"pl-4\"><p>Hooks are kind of the general-purpose way to inject determinism into an otherwise nondeterministic AI process. <code>PreToolUse<\/code> lets you inspect or block a command before it runs (think \u201cno <code>rm -rf<\/code> outside the project dir\u201d), <code>PostToolUse<\/code> is great for logging shell commands to an audit file, <code>Stop<\/code> can ping you when a long turn finishes, and <code>Notification<\/code> can ping you when Claude is stuck waiting on input.<\/p>\n<\/div>\n<\/div>\n<p>(Also, since we\u2019re on the subject of hooks, the thing I\u2019ll keep saying: prompts aren\u2019t guardrails, hooks are. If there\u2019s something Claude really shouldn\u2019t do, a <code>PreToolUse<\/code> hook that actually blocks it is more reliable than asking nicely in your <code>CLAUDE.md<\/code> and hoping the model holds the line.)<\/p>\n<h2 id=\"swapping-costumes\">Swapping costumes<\/h2>\n<p>Once Moira was working, I started building the same setup for Worf, Bob Ross, and a few other characters. I kept one settings file per character on disk, and pointed <code>~\/.claude\/settings.json<\/code> at whichever one I wanted activated.<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>~\/.claude\/\n<\/span><span>\u251c\u2500\u2500 settings.json              \u2192 symlink to settings.moira.json\n<\/span><span>\u251c\u2500\u2500 settings.default.json      # vanilla Claude Code (or just `{}`)\n<\/span><span>\u251c\u2500\u2500 settings.moira.json        # full Moira costume\n<\/span><span>\u251c\u2500\u2500 settings.worf.json         # full Worf costume\n<\/span><span>\u251c\u2500\u2500 output-styles\/\n<\/span><span>\u2502   \u251c\u2500\u2500 moira-rose.md\n<\/span><span>\u2502   \u2514\u2500\u2500 worf.md\n<\/span><span>\u251c\u2500\u2500 statusline-moira.sh\n<\/span><span>\u251c\u2500\u2500 statusline-worf.sh\n<\/span><span>\u2514\u2500\u2500 themes\/\n<\/span><span>    \u251c\u2500\u2500 moira.json\n<\/span><span>    \u2514\u2500\u2500 worf.json\n<\/span><\/code><\/pre>\n<p>The supporting files (scripts, output styles, themes) all sit on disk together, but only the active <code>settings.json<\/code> decides which ones get wired up. A small shell function in <code>.zshrc<\/code> makes the swap one command:<\/p>\n<pre data-lang=\"bash\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-bash \"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"color:#8fa1b3;\">costume<\/span><span>() {\n<\/span><span>  <\/span><span style=\"color:#b48ead;\">local <\/span><span style=\"color:#bf616a;\">name<\/span><span>=$<\/span><span style=\"color:#a3be8c;\">{<\/span><span style=\"color:#bf616a;\">1<\/span><span>:-<\/span><span style=\"color:#a3be8c;\">default}\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">ln -sf ~<\/span><span>\/.claude\/settings.$<\/span><span style=\"color:#bf616a;\">name<\/span><span>.json <\/span><span style=\"color:#bf616a;\">~<\/span><span>\/.claude\/settings.json\n<\/span><span>  <\/span><span style=\"color:#96b5b4;\">echo <\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">Costume: <\/span><span>$<\/span><span style=\"color:#bf616a;\">name<\/span><span>&quot;\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>Now <code>costume moira<\/code> puts the whole getup on, <code>costume worf<\/code> swaps it for the Klingon, and <code>costume<\/code> on its own takes everything off and you\u2019re back to plain Claude Code. The next session picks up whichever one was left dangling.<\/p>\n<p>The thing I didn\u2019t expect was how much of the charm comes from <em>forgetting<\/em> I\u2019d put one on. I\u2019d swap to Worf for fun, get pulled into something else, come back hours later to fire off a mundane question about token costs, and read something like:<\/p>\n<blockquote>\n<p>A worthy question. A warrior must know the cost of every weapon he carries.<\/p>\n<\/blockquote>\n<p>That\u2019s the moment I\u2019d remember oh right, I left Worf loaded. The surprise really is most of the fun.<\/p>\n<p>None of this makes me a better engineer, but it does add some humor to living in Claude Code for hours of my life. Somewhere between the wine-colored prompts and my laptop murmuring <em>curtain<\/em> into the room when I close the session, the whole thing has started to feel like the right amount of theatre for a Tuesday afternoon.<\/p>\n"},{"title":"Claude Code beyond prompting","pubDate":"Wed, 31 Dec 2025 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/claude-code-beyond-prompting\/","guid":"https:\/\/sdeaton.com\/blog\/claude-code-beyond-prompting\/","description":"<p>In my first week writing documentation for Claude Code, I realized I\u2019d been using a Swiss Army knife with only the main blade, unaware of what else it could do.<\/p>\n<p>Before I joined Anthropic, I thought I was pretty good at Claude Code. I was using it to speed up my technical writing: generating diagrams I couldn\u2019t make myself, catching inconsistencies quickly, and automating the most tedious parts of doc review. Putting our entire style guide into a <code>CLAUDE.md<\/code> made it even better. It wasn\u2019t perfect, but it removed whole classes of style-related mistakes and shifted every edit closer to \u201cdone.\u201d<\/p>\n<p>That felt like a complete workflow. I\u2019d heard about things like subagents, skills, and hooks, but what I had was working well, so I never looked much deeper.<\/p>\n<p>Then I started working on the docs for these features and discovered the other tools had been there the whole time, each better suited for certain kinds of work. I just hadn\u2019t learned when to reach for them.<\/p>\n<p><em>Note: This is a personal mental model I\u2019ve been building through hands-on use, not an official or exhaustive guide.<\/em><\/p>\n<h2 id=\"where-i-started-and-where-you-might-be-too\">Where I started (and where you might be too)<\/h2>\n<p>The <code>CLAUDE.md<\/code> file was my first big unlock. If you haven\u2019t done this yet, start here: create a <code>CLAUDE.md<\/code> file in your project root (or in a <code>.claude\/<\/code> directory) and put your universal writing guidelines there. It\u2019s the highest-impact, lowest-effort improvement you can make.<\/p>\n<p>At first, I put everything in it. This included documentation philosophy, voice and tone, grammar rules, formatting conventions, and notes about our publishing workflow. Because Claude reads this file automatically at the start of every conversation, every edit followed our conventions without repeated reminders.<\/p>\n<p>That worked well. But as I iterated, I started adding highly specific, task-level instructions that were important but not relevant to every session. Since <code>CLAUDE.md<\/code> loads in full at the start of every conversation, every extra byte counts toward your context window and token usage. It\u2019s best suited to universal rules, not specialized knowledge you only need occasionally.<\/p>\n<p>I also kept retyping the same prompts for repeated tasks, like full doc review checklists. The same instructions, over and over, because I hadn\u2019t saved them anywhere.<\/p>\n<p>These were minor annoyances, not blockers. But when I started documenting Claude Code\u2019s features, I realized there were specific tools that fixed these friction points.<\/p>\n<h2 id=\"what-else-claude-code-can-do\">What else Claude Code can do<\/h2>\n<p>These are the tools you can reach for beyond prompting and <code>CLAUDE.md<\/code>, and the kinds of technical writing tasks each one is best suited for. You don\u2019t need to adopt all of these at once. Start with <code>CLAUDE.md<\/code>, then add the others only when you feel friction.<\/p>\n<h3 id=\"slash-commands-one-prompt-saved-forever\">Slash commands: One prompt, saved forever<\/h3>\n<p>Claude Code ships with a lot of <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/slash-commands\">built-in slash commands<\/a> worth exploring, like <code>\/init<\/code> to analyze your codebase and set up your configuration or <code>\/rename<\/code> to clean up session names.<\/p>\n<p>Beyond the built-ins, any prompt you type more than twice can be a custom command. They\u2019re Markdown files you store in <code>.claude\/commands\/<\/code> and trigger within a Claude session.<\/p>\n<p>Here are two light examples you might add to a technical writing toolkit:<\/p>\n<p><strong><code>\/review<\/code><\/strong> (<code>.claude\/commands\/review.md<\/code>), a full doc review checklist:<\/p>\n<pre data-lang=\"markdown\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-markdown \"><code class=\"language-markdown\" data-lang=\"markdown\"><span>Review this document for:\n<\/span><span style=\"color:#bf616a;\">1. Style guide compliance (check CLAUDE.md)\n<\/span><span style=\"color:#bf616a;\">2. Technical accuracy (do the code samples work?)\n<\/span><span style=\"color:#bf616a;\">3. Broken links and references\n<\/span><span style=\"color:#bf616a;\">4. Missing context (would a new user understand this?)\n<\/span><span style=\"color:#bf616a;\">5. Structural issues (headers, flow, completeness)\n<\/span><span style=\"color:#bf616a;\">\n<\/span><span>Categorize issues as: blocking, should fix, nice to have.\n<\/span><\/code><\/pre>\n<p><strong><code>\/simplify<\/code><\/strong> (<code>.claude\/commands\/simplify.md<\/code>), for when you\u2019ve written something too dense:<\/p>\n<pre data-lang=\"markdown\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-markdown \"><code class=\"language-markdown\" data-lang=\"markdown\"><span>Simplify this content for a developer who&#39;s new to the product.\n<\/span><span style=\"color:#bf616a;\">- Remove jargon or define it on first use\n<\/span><span style=\"color:#bf616a;\">- Shorten sentences\n<\/span><span style=\"color:#bf616a;\">- Cut anything not essential to the core concept\n<\/span><span style=\"color:#bf616a;\">- Add an example if it would help\n<\/span><\/code><\/pre>\n<p>You can run <code>\/review<\/code> or <code>\/simplify<\/code> and Claude will run that prompt on the content you\u2019ve been working on.<\/p>\n<p>(<a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/slash-commands\">Full slash commands documentation<\/a>)<\/p>\n<h3 id=\"skills-context-that-loads-only-when-you-need-it\">Skills: Context that loads only when you need it<\/h3>\n<p>I was using <code>CLAUDE.md<\/code> for information that wasn\u2019t applicable to every conversation. <code>CLAUDE.md<\/code> eats into your token budget from the start of every conversation, so it\u2019s not efficient to include information Claude may or may not need. Skills solve this.<\/p>\n<p>Skills give Claude specialized knowledge for specific types of work, like how to structure API docs versus tutorials or how to test code samples for different SDKs. They\u2019re directories with instructions, templates, and reference files. At startup, Claude only loads skill names and descriptions. The full <code>SKILL.md<\/code> file only loads when Claude decides from the task to use a skill.<\/p>\n<p>An API docs writing skill might look like this:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>.claude\/skills\/api-docs\/\n<\/span><span>\u251c\u2500\u2500 SKILL.md           # Instructions for API doc structure\n<\/span><span>\u251c\u2500\u2500 template.md        # Standard template with all sections\n<\/span><span>\u2514\u2500\u2500 examples\/\n<\/span><span>    \u251c\u2500\u2500 good-example.md    # &quot;Here&#39;s what we&#39;re going for&quot;\n<\/span><span>    \u2514\u2500\u2500 common-mistakes.md # &quot;Avoid these patterns&quot;\n<\/span><\/code><\/pre>\n<p>The <code>SKILL.md<\/code> file for this skill:<\/p>\n<pre data-lang=\"markdown\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-markdown \"><code class=\"language-markdown\" data-lang=\"markdown\"><span style=\"background-color:#4f5b66;color:#c0c5ce;\">---\n<\/span><span>name: documenting-apis\n<\/span><span>description: Provides structure and examples for writing API reference documentation. Use when creating or editing API documentation.\n<\/span><span style=\"color:#8fa1b3;\">---\n<\/span><span>\n<\/span><span style=\"color:#8fa1b3;\"># API Documentation\n<\/span><span>\n<\/span><span style=\"color:#8fa1b3;\">## Structure\n<\/span><span>Every API doc needs: Overview, Authentication, Endpoints, Error codes, Examples\n<\/span><span>\n<\/span><span style=\"color:#8fa1b3;\">## Voice\n<\/span><span>More formal than tutorials. Assume reader knows HTTP basics.\n<\/span><span>\n<\/span><span style=\"color:#8fa1b3;\">## Template\n<\/span><span>See <\/span><span style=\"color:#d08770;\">[template.md](template.md)<\/span><span> for the standard structure. Don&#39;t skip sections.\n<\/span><span>\n<\/span><span style=\"color:#8fa1b3;\">## Examples\n<\/span><span>See <\/span><span style=\"color:#d08770;\">[examples\/good-example.md](examples\/good-example.md)<\/span><span> for reference patterns.\n<\/span><\/code><\/pre>\n<p>The supporting files (templates, examples) only load when the task actually needs them. Claude discovers them through links in your <code>SKILL.md<\/code>, so you can keep detailed reference material available without it eating up context until it\u2019s relevant.<\/p>\n<p>Now your <code>CLAUDE.md<\/code> can stay lean with only universal guidelines, while specialized context loads automatically when you need it.<\/p>\n<p>(<a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/skills\">Full skills documentation<\/a>)<\/p>\n<hr \/>\n<p><strong>A note on Skills vs MCP:<\/strong> I\u2019ve heard people say that Skills make MCP servers less useful, but that\u2019s not the case. They serve different purposes. <strong>MCP is the connection to your data<\/strong> (Slack, GitHub, Google Drive). <strong>Skills are the instructions for how to use that data.<\/strong><\/p>\n<p>For example:<\/p>\n<ul>\n<li>MCP server connects Claude to your GitHub issues<\/li>\n<li>Skill says \u201cWhen reviewing docs, check issues labeled \u2018documentation\u2019 for common user confusion\u201d<\/li>\n<\/ul>\n<p>You can have MCP without skills (you just prompt every time), but skills paired with MCP are way more powerful. Skills turn \u201cI have access to this data\u201d into \u201chere\u2019s when and how to use it.\u201d<\/p>\n<p>(<a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/mcp\">MCP documentation<\/a>)<\/p>\n<hr \/>\n<h3 id=\"subagents-parallel-work-without-losing-focus\">Subagents: Parallel work without losing focus<\/h3>\n<p>Subagents are separate Claude sessions that run in the background with their own context. The main agent passes specific context to them, and they report back when done.<\/p>\n<p>You can spawn them ad hoc for one-off tasks. But you can also save subagent configurations as Markdown files with YAML frontmatter in <code>.claude\/agents\/<\/code>. Once configured, they become reusable tools you invoke by name.<\/p>\n<p>I frequently use saved subagents for end-of-doc review: tech review, UX review, style review. They run in isolated context, which means I can kick off multiple reviews in parallel without competing for the main conversation\u2019s token budget.<\/p>\n<p>Here\u2019s an example of a subagent that checks for technical accuracy.<\/p>\n<p><em>Tip: you can use the <code>\/agents<\/code> slash command to help build these instead of doing it manually<\/em>.<\/p>\n<p><strong>Technical accuracy reviewer<\/strong> (<code>.claude\/agents\/tech-reviewer.md<\/code>):<\/p>\n<pre data-lang=\"markdown\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-markdown \"><code class=\"language-markdown\" data-lang=\"markdown\"><span style=\"background-color:#4f5b66;color:#c0c5ce;\">---\n<\/span><span>name: tech-reviewer\n<\/span><span>description: Verifies documentation matches actual code behavior and internal sources of truth. Use when reviewing documentation for technical accuracy or when code has changed.\n<\/span><span>tools: [Read, Grep, Glob, Bash]\n<\/span><span style=\"color:#8fa1b3;\">---\n<\/span><span>\n<\/span><span>You are a technical accuracy reviewer for documentation.\n<\/span><span>\n<\/span><span>When invoked:\n<\/span><span style=\"color:#bf616a;\">1. Read the documentation being reviewed\n<\/span><span style=\"color:#bf616a;\">2. Find the corresponding source code in src\/\n<\/span><span style=\"color:#bf616a;\">3. Check internal docs in \/docs\/internal\/ for canonical behavior\n<\/span><span style=\"color:#bf616a;\">4. Verify every claim in the doc matches actual implementation\n<\/span><span style=\"color:#bf616a;\">\n<\/span><span>For each section, report:\n<\/span><span style=\"color:#bf616a;\">- \u2713 Accurate (code matches doc)\n<\/span><span style=\"color:#bf616a;\">- \u26a0 Needs attention (minor discrepancy)\n<\/span><span style=\"color:#bf616a;\">- \u2717 Incorrect (documentation contradicts code)\n<\/span><span style=\"color:#bf616a;\">\n<\/span><span>Include specific line numbers and suggest corrections.\n<\/span><\/code><\/pre>\n<p>When you\u2019re ready for a review, kick this off by asking Claude to run it. You can keep working in the main thread as this and other subagents run in parallel (or you can take a coffee break), and when the subagent is done, you get a focused report back.<\/p>\n<p>(<a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/sub-agents\">Full subagents documentation<\/a>)<\/p>\n<hr \/>\n<h4 id=\"subagents-vs-skills-vs-slash-commands\">Subagents vs. skills vs. slash commands<\/h4>\n<p>You might notice some overlap between these tools. Could the <code>\/review<\/code> slash command also be a subagent? Or a skill? Yes. In some cases, you might even want a slash command and a subagent that perform similar tasks. The difference is not what they do, but how they handle context and who triggers them.<\/p>\n<table><thead><tr><th>Tool<\/th><th>Context\/isolation<\/th><th>Usually triggered by<\/th><th>When to use it<\/th><\/tr><\/thead><tbody>\n<tr><td><strong>Slash command<\/strong> (<code>\/review<\/code>)<\/td><td>Full main conversation context<\/td><td>You (explicit invocation: <code>\/review<\/code>)<\/td><td>When you want Claude to consider the entire discussion and current project state.<\/td><\/tr>\n<tr><td><strong>Subagent<\/strong> (<code>review-agent<\/code>)<\/td><td>Isolated context (only what you\/agent pass to it)<\/td><td>You (tell Claude to run it)<\/td><td>When you want to run tasks in parallel or keep your main conversation focused.<\/td><\/tr>\n<tr><td><strong>Skill<\/strong> (<code>review-skill<\/code>)<\/td><td>Loads specialized instructions on demand<\/td><td>Claude (infers from task context)<\/td><td>When you need structure, templates, or reference material without keeping it in context all the time.<\/td><\/tr>\n<\/tbody><\/table>\n<p><em>Note: The triggering part can be more nuanced in practice. Claude can invoke custom slash commands, and you can explicitly ask Claude to use a specific skill. The table above covers the most common usage.<\/em><\/p>\n<p>You can use more than one of these for the same kind of task. A slash command is useful for quick, context-aware checks. A subagent is better for parallel or background work. A skill is the right choice when the work needs deeper, reusable guidance without bloating your main context.<\/p>\n<hr \/>\n<h3 id=\"hooks-automation-that-runs-at-specific-events\">Hooks: Automation that runs at specific events<\/h3>\n<p>Hooks are scripts that run automatically at specific Claude Code lifecycle events. Before a tool runs, after Claude sends a message, when a session stops. You set them up once and then forget about them.<\/p>\n<p>A useful one for technical writing: run your linter before any edit saves.<\/p>\n<pre data-lang=\"json\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-json \"><code class=\"language-json\" data-lang=\"json\"><span>{\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">hooks<\/span><span>&quot;: {\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">PreToolUse<\/span><span>&quot;: [{\n<\/span><span>      &quot;<\/span><span style=\"color:#a3be8c;\">matcher<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">Edit|Write<\/span><span>&quot;,\n<\/span><span>      &quot;<\/span><span style=\"color:#a3be8c;\">command<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">vale --config=.vale.ini $FILE<\/span><span>&quot;\n<\/span><span>    }]\n<\/span><span>  }\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>Now whenever Claude edits your work, linting runs automatically. You catch issues immediately instead of in code review.<\/p>\n<p>Hooks make checks deterministic. No relying on Claude to remember, no hoping it picks up the pattern. It just happens every time.<\/p>\n<p>Another useful one: a sound notification when Claude finishes a message. I tend to context switch while waiting for responses, and this tells me when to come back without constantly checking.<\/p>\n<pre data-lang=\"json\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-json \"><code class=\"language-json\" data-lang=\"json\"><span>{\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">hooks<\/span><span>&quot;: {\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">Stop<\/span><span>&quot;: [{\n<\/span><span>      &quot;<\/span><span style=\"color:#a3be8c;\">matcher<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">assistant<\/span><span>&quot;,\n<\/span><span>      &quot;<\/span><span style=\"color:#a3be8c;\">command<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">afplay \/System\/Library\/Sounds\/Glass.aiff<\/span><span>&quot;\n<\/span><span>    }]\n<\/span><span>  }\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>You can hook into a lot of events: session start, before context compaction, after Claude sends a message, on session stop. Anywhere you\u2019d want to inject a check or action, there\u2019s probably a hook point for it.<\/p>\n<p>(<a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/hooks\">Full hooks documentation<\/a>)<\/p>\n<h3 id=\"plugins-discover-and-share-workflows\">Plugins: Discover and share workflows<\/h3>\n<p><a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/plugins\">Plugins<\/a> work in two directions: you can benefit from what others have already built, and you can share your own workflows with others.<\/p>\n<p><strong>Using plugins others have built:<\/strong> To browse and install prebuilt plugins, run <code>\/plugin<\/code>. The default marketplace from Anthropic is automatically included, but you can <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/discover-plugins\">add other community marketplaces<\/a> if you want to explore more. One plugin I use all the time is <code>commit-commands<\/code> from the Anthropic marketplace. It auto-drafts commit messages based on my changes. Way more useful than \u201cupdate skills docs\u201d but also not something I want to spend brainpower on.<\/p>\n<p><strong>Sharing your own:<\/strong> Once you\u2019ve built up a collection of slash commands, skills, subagents, and hooks, you can package them as a plugin and share it via <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/plugin-marketplaces\">marketplaces<\/a>. Instead of every person figuring out the same workflows from scratch, you can hand them a plugin with everything already configured.<\/p>\n<p><em>A quick note on sharing: if you store your commands, skills, and agents in your project\u2019s <code>.claude\/<\/code> folder and commit them to git, your team automatically gets them when they clone the repo. That works great for project-specific setups without needing a full plugin.<\/em><\/p>\n<p>(<a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/plugins\">Full plugins documentation<\/a>)<\/p>\n<h2 id=\"how-these-pieces-fit-together\">How these pieces fit together<\/h2>\n<p>Here\u2019s how I\u2019m starting to think about it:<\/p>\n<table><thead><tr><th>Tool<\/th><th>What it\u2019s for<\/th><th>When to use it<\/th><th>Example<\/th><\/tr><\/thead><tbody>\n<tr><td><strong><code>CLAUDE.md<\/code><\/strong><\/td><td>Universal context<\/td><td>Guidelines that apply to every task<\/td><td>Voice, formatting rules<\/td><\/tr>\n<tr><td><strong>Slash commands<\/strong><\/td><td>Repeatable prompts<\/td><td>Any prompt you use more than twice<\/td><td><code>\/review<\/code>, <code>\/simplify<\/code><\/td><\/tr>\n<tr><td><strong>Skills<\/strong><\/td><td>Specialized instructions<\/td><td>How to approach specific doc types<\/td><td>API docs skill, tutorial skill<\/td><\/tr>\n<tr><td><strong>Subagents<\/strong><\/td><td>Parallel work<\/td><td>Reviews that can run in the background<\/td><td>Tech reviewer, code tester<\/td><\/tr>\n<tr><td><strong>Hooks<\/strong><\/td><td>Automatic guardrails<\/td><td>Checks you always want to run<\/td><td>Linter, sound notification<\/td><\/tr>\n<\/tbody><\/table>\n<p>The goal isn\u2019t to use all of these. It\u2019s to know they exist so you can reach for them when they\u2019d help.<\/p>\n<h2 id=\"where-to-start\">Where to start<\/h2>\n<p>If you\u2019re not using Claude Code yet:<\/p>\n<ol>\n<li>Install it by following the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/code.claude.com\/docs\/en\/quickstart\">quickstart guide<\/a>.<\/li>\n<li>Navigate to a docs repo and open Claude Code: <code>cd your-docs &amp;&amp; claude<\/code>.<\/li>\n<li>Start chatting.<\/li>\n<\/ol>\n<p>If you\u2019re already using it but mainly just prompting:<\/p>\n<ol>\n<li><strong>Run <code>\/init<\/code><\/strong>: analyzes your codebase and sets up <code>CLAUDE.md<\/code>, slash commands, and subagents based on what would be useful for your project.<\/li>\n<li><strong>Add universal guidelines to <code>CLAUDE.md<\/code><\/strong>: voice, formatting rules, repo structure.<\/li>\n<li><strong>Create one slash command<\/strong> for your most common task (maybe <code>\/review<\/code>).<\/li>\n<li><strong>Spawn a subagent<\/strong> for parallel reviews. When you repeat the same instructions, save it as a configuration.<\/li>\n<\/ol>\n<p>You don\u2019t need to set up everything at once. Add tools when you notice friction.<\/p>\n<hr \/>\n<p>What\u2019s stuck with me most is how each tool solves a different problem around context, isolation, and automation. <code>CLAUDE.md<\/code> gives you persistent context. Slash commands give you repeatable prompts. Skills give you on-demand specialization. Subagents give you parallelism. Hooks give you guarantees.<\/p>\n<p>I\u2019m still learning which ones fit which workflows. But having them available means I can shape how I work instead of working around the limitations of a single interface.<\/p>\n"},{"title":"Code execution with MCP: separating context from computation","pubDate":"Fri, 14 Nov 2025 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/code-execution-with-mcp\/","guid":"https:\/\/sdeaton.com\/blog\/code-execution-with-mcp\/","description":"<p><strong>TL;DR:<\/strong> Traditional tool calling eats away at an agent\u2019s context window. All the tool definitions and every intermediate result pass through it. Code execution gives agents a \u201cscratch pad\u201d workspace to process data outside the context window. Combined with MCP\u2019s tool abstractions, this can significantly reduce token usage for data-heavy tasks.<\/p>\n<p>Check out the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/github.com\/sarahcstringer\/mcp-code-demo\">demo repository<\/a> for two examples you can run.<\/p>\n<hr \/>\n<p>I thought I understood MCP. <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/deatons.substack.com\/p\/learn-mcp-with-me-part-1-what-is\">It took me a while<\/a>, but I recognized that we need MCP as a standardized layer to best interact with external services. MCP gives LLMs structured access to APIs, providing a consistent interface instead of varying parameter names and request\/response formats across different services.<\/p>\n<p>Then I read Anthropic\u2019s post about <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.anthropic.com\/engineering\/code-execution-with-mcp\">code execution with MCP<\/a> and Cloudflare\u2019s piece on <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/blog.cloudflare.com\/code-mode\/\">Code Mode<\/a>, which talk about having LLMs write code to call MCP servers, and I got confused again.<\/p>\n<p>Why are we having LLMs write code around MCP servers when we wrote MCP servers so that\u2026 LLMs <strong>didn\u2019t have to write code<\/strong> to interact with APIs?<\/p>\n<p>The piece that finally clicked: a key benefit of code execution is that it happens in an execution environment, not in the agent\u2019s context window.<\/p>\n<p>MCP is still the standardization layer; generating Python wrappers for MCP tools makes them available as importable functions in the execution environment. Then, if the LLM has access to a bash tool that executes commands, it can write and execute entire scripts in its execution environment and only send the final answer back to context.<\/p>\n<p>As an analogy: traditional tool calling requires you to show every single calculation on your final exam paper: all the messy scratch work, the raw data, the transformed versions, crossed-out mistakes. You run out of space fast, and the grader (the LLM) has to wade through all that noise to find the final answer. MCP + code execution is like giving you scratch paper to process data and perform operations outside the context window. Then you just record the final result on your exam paper.<\/p>\n<h2 id=\"context-window-vs-execution-environment\">Context window vs. execution environment<\/h2>\n<p>The <strong>context window<\/strong> is the LLM\u2019s working memory. It\u2019s expensive (costs tokens), limited in size, and includes:<\/p>\n<ul>\n<li>\n<p>System prompt<\/p>\n<\/li>\n<li>\n<p>Conversation history<\/p>\n<\/li>\n<li>\n<p>Tool descriptions<\/p>\n<\/li>\n<li>\n<p>Tool results<\/p>\n<\/li>\n<li>\n<p>The response being generated<\/p>\n<\/li>\n<\/ul>\n<p>The <strong>execution environment<\/strong> is where code actually runs. In development, it might be your laptop. In production, it\u2019s typically a sandboxed Docker container or VM with limited filesystem and network access. It\u2019s relatively cheap and can handle large data processing.<\/p>\n<p>Modern agent frameworks like <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.anthropic.com\/engineering\/code-execution-with-mcp\">Claude Code<\/a> and <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/blog.cloudflare.com\/code-mode\/\">Cloudflare Agents<\/a> provide code execution environments built-in, but you can also achieve the same pattern by giving any LLM a bash tool that executes commands in your execution environment.<\/p>\n<h2 id=\"the-problem-your-context-window-is-getting-bloated\">The problem: your context window is getting bloated<\/h2>\n<p>Tool calling is great for letting LLMs use APIs and external tools, but once you have a production system with large datasets and hundreds of tools, it quickly runs into a major problem: <strong>all the tool definitions and every intermediate result from every tool call has to pass through your context window.<\/strong><\/p>\n<p>If you want to fetch 300 records, transform the data, and filter it with tools, all of the following must pass through context:<\/p>\n<ul>\n<li>\n<p>The tool definitions<\/p>\n<\/li>\n<li>\n<p>The raw data from the records<\/p>\n<\/li>\n<li>\n<p>The transformed data<\/p>\n<\/li>\n<li>\n<p>The filtered data<\/p>\n<\/li>\n<\/ul>\n<p>Each step consumes more of your expensive, limited window until you hit the limit or face substantial cost increases. Your agent also has to sort through all the results and its working memory is crowded with information that isn\u2019t actually relevant for its final response.<\/p>\n<h2 id=\"the-solution-code-execution-in-an-execution-environment\">The solution: code execution in an execution environment<\/h2>\n<p>Code execution gives agents a \u201cscratch pad\u201d workspace to process data outside the context window. It allows the agent to write code that calls MCP tools, process results locally in the execution environment, and only record the final result in context.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/code-execution-with-mcp\/d8dcb57a-f5e7-42f6-899d-3497b53c93b6.png\" alt=\"Comparison diagram showing traditional tool calling with all operations in the context window versus code execution with a separate execution environment for processing\" \/><\/p>\n<p>MCP still handles the \u201cHow do I call this API correctly?\u201d problem by giving LLMs structured tools instead of making them write raw API calls that vary from API to API.<\/p>\n<p>Code execution solves the \u201cHow do I process all this data without destroying my context window?\u201d problem by giving the agent a workspace to process data outside the context window.<\/p>\n<p>Beyond data processing, code execution unlocks capabilities that are challenging with traditional tool calling. Things like polling for results and waiting, retrying failed requests, maintaining state across multiple operations, saving intermediate results and returning back to them later, all become possible through code.<\/p>\n<h2 id=\"production-safety-guardrails-for-code-execution\">Production safety: guardrails for code execution<\/h2>\n<p>Giving an LLM the ability to execute code is powerful, but requires guardrails. Three key practices:<\/p>\n<p><strong>1. Sandboxed execution:<\/strong> Run code in isolated Docker containers or VMs, not your production environment. Limit filesystem and network access or use something like the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/developers.cloudflare.com\/sandbox\/\">Cloudflare Sandbox SDK<\/a> or <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.daytona.io\/\">Daytona<\/a> for runtime isolation.<\/p>\n<p><strong>2. Human approval for destructive actions:<\/strong> Require approval for anything that costs money, modifies data, or communicates externally. For example, <code>read_channel_messages<\/code> might auto-approve, but <code>post_message_to_all_hands<\/code> should require review.<\/p>\n<p><strong>3. Thoughtful tool design:<\/strong> Don\u2019t expose raw endpoints. Build high-level tools like <code>schedule_meeting<\/code> (checks availability + books room) instead of <code>list_users<\/code> \/ <code>list_events<\/code> \/ <code>create_event<\/code>. Prefer search-focused results (<code>search_contacts(\u201dJane\u201d)<\/code>) over giant dumps (<code>list_all_contacts()<\/code>).<\/p>\n<p>The <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.anthropic.com\/engineering\/writing-tools-for-agents\">Anthropic article on writing tools for agents<\/a> covers these design principles in depth.<\/p>\n<h2 id=\"when-to-use-each-approach\">When to use each approach<\/h2>\n<p>Not every task needs code execution. Traditional tool calling works well for certain scenarios:<\/p>\n<p><strong>Traditional tool calling works when:<\/strong><\/p>\n<ul>\n<li>\n<p>Tool results are small (a few hundred tokens)<\/p>\n<\/li>\n<li>\n<p>You need one or two tool calls total<\/p>\n<\/li>\n<li>\n<p>No data processing required<\/p>\n<\/li>\n<li>\n<p>Results go directly to the user<\/p>\n<\/li>\n<\/ul>\n<p><strong>Code execution excels when:<\/strong><\/p>\n<ul>\n<li>\n<p>Processing large datasets<\/p>\n<\/li>\n<li>\n<p>Making multiple related tool calls<\/p>\n<\/li>\n<li>\n<p>Filtering or transforming results<\/p>\n<\/li>\n<li>\n<p>Polling or waiting for completion<\/p>\n<\/li>\n<li>\n<p>Maintaining state across operations<\/p>\n<\/li>\n<\/ul>\n<h2 id=\"limitations-and-trade-offs\">Limitations and trade-offs<\/h2>\n<p>Code execution has some trade-offs (and at least to me, this is such a new concept that there are a lot of things I\u2019m haven\u2019t thought through yet):<\/p>\n<p><strong>Model capability matters:<\/strong> Code execution requires models with strong coding abilities. Not all LLMs write clean, correct code consistently. If your model struggles with code generation, the benefits diminish quickly. Test your chosen model\u2019s coding capabilities before committing to this pattern.<\/p>\n<p><strong>Non-deterministic code:<\/strong> Traditional tool calling shows you which tools are called, with what arguments, and what they return. Code execution is opaque; the LLM generates code dynamically, and that code can be different each time for the same prompt. You don\u2019t know what it will write until it writes it, and once it\u2019s executing in the environment, you lose visibility into the step-by-step operations.<\/p>\n<p><strong>Security surface area:<\/strong> Executing LLM-generated code introduces risks beyond traditional tool calling. Even with sandboxing, you need to worry about resource exhaustion, unintended file system access, infinite loops, and malicious prompt injections that generate harmful code. The guardrails mentioned earlier are necessary but not sufficient; you need ongoing monitoring and careful tool design.<\/p>\n<p><strong>Complexity:<\/strong> Your system needs to manage an execution environment, handle code execution errors gracefully, and potentially deal with environment setup and dependencies. Traditional tool calling has a simpler operational model.<\/p>\n<p>Despite these trade-offs, for data-heavy tasks where token costs and context window limits are real constraints, code execution can provide significant practical benefits.<\/p>\n<h2 id=\"the-mental-model-shift\">The mental model shift<\/h2>\n<p>Agents are more than LLMs with tools. They\u2019re LLMs with tools and an execution environment.<\/p>\n<p>The context window is for reasoning. The execution environment is for working.<\/p>\n<p>When you structure agents this way, you get:<\/p>\n<ul>\n<li>\n<p>Significant reduction in token usage (varies by task, but can be substantial for data-heavy operations)<\/p>\n<\/li>\n<li>\n<p>Natural patterns for polling and async operations<\/p>\n<\/li>\n<li>\n<p>Ability to process large datasets<\/p>\n<\/li>\n<li>\n<p>Stateful computation without context bloat<\/p>\n<\/li>\n<li>\n<p>More predictable results by executing deterministic code rather than tracking state in context<\/p>\n<\/li>\n<\/ul>\n<p>Code execution can improve reliability by reducing the need for the LLM to track state and aggregate data across multiple tool calls in context. By having the agent write deterministic scripts that run in the execution environment, you get more predictable and complete results.<\/p>\n<p><strong>Note:<\/strong> The effectiveness of this approach depends significantly on the model you use. Models with strong coding capabilities will write better, more efficient code. The token savings also vary based on your specific use case\u2014tasks involving large datasets or many sequential operations see the most benefit.<\/p>\n<p>MCP provides the standardized tools. Code execution provides the workspace to use them efficiently. Together, they enable agents that are both capable and cost-effective.<\/p>\n<h2 id=\"try-it-yourself\">Try it yourself<\/h2>\n<p>I created a <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/github.com\/sarahcstringer\/mcp-code-demo\">demo repository<\/a> with two examples you can run and play around with:<\/p>\n<ol>\n<li>\n<p><strong>Traditional tool calling<\/strong>: Shows how intermediate data fills the context window<\/p>\n<\/li>\n<li>\n<p><strong>Code execution<\/strong>: Shows how processing happens in the execution environment<\/p>\n<\/li>\n<\/ol>\n<p>Both examples include token usage metrics so you can compare the approaches.<\/p>\n<p><strong>Note on token variability:<\/strong> Token usage varies between runs, even with the same task and model. In my testing with this demo, I\u2019ve seen the following results:<\/p>\n<ul>\n<li>\n<p><strong>Traditional tool calling<\/strong>: Typically 50,000-73,000 tokens<\/p>\n<\/li>\n<li>\n<p><strong>Code execution<\/strong>: Typically 9,500-10,000 tokens<\/p>\n<\/li>\n<li>\n<p><strong>Token reduction<\/strong>: 80-87% depending on the run<\/p>\n<\/li>\n<\/ul>\n<p>To run the examples:<\/p>\n<p>`# Clone this repo\ngit clone https:\/\/github.com\/sarahcstringer\/mcp-code-demo\ncd mcp-code-demo<\/p>\n<h1 id=\"install-dependencies\">Install dependencies<\/h1>\n<p>pip install -r requirements.txt<\/p>\n<h1 id=\"set-up-your-anthropic-api-key\">Set up your Anthropic API key<\/h1>\n<p>cp .env.example .env<\/p>\n<h1 id=\"edit-env-and-add-your-anthropic-api-key\">Edit .env and add your ANTHROPIC_API_KEY<\/h1>\n<h1 id=\"run-the-traditional-tool-calling-example\">Run the traditional tool calling example<\/h1>\n<p>python examples\/traditional_tool_calling.py<\/p>\n<h1 id=\"generate-mcp-tool-wrappers-for-the-code-execution-example\">Generate MCP tool wrappers for the code execution example<\/h1>\n<p>python generate_wrappers.py<\/p>\n<h1 id=\"run-the-code-execution-example\">Run the code execution example<\/h1>\n<p>python examples\/code_execution.py`The examples use Anthropic\u2019s Claude Haiku 4 model with the Anthropic SDK, but the patterns work with any model provider.<\/p>\n<h2 id=\"next-steps\">Next steps<\/h2>\n<ul>\n<li>\n<p><a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.anthropic.com\/engineering\/code-execution-with-mcp\">Read the Anthropic post on code execution<\/a><\/p>\n<\/li>\n<li>\n<p><a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.anthropic.com\/engineering\/writing-tools-for-agents\">Learn best practices for writing tools for agents<\/a><\/p>\n<\/li>\n<li>\n<p><a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/blog.cloudflare.com\/code-mode\/\">Check out Cloudflare\u2019s Code Mode explanation<\/a><\/p>\n<\/li>\n<li>\n<p><a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/modelcontextprotocol.io\/\">Learn about MCP<\/a><\/p>\n<\/li>\n<\/ul>\n"},{"title":"On agency, abstraction, and AI-assisted coding","pubDate":"Tue, 04 Nov 2025 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/on-agency-abstraction-ai-coding\/","guid":"https:\/\/sdeaton.com\/blog\/on-agency-abstraction-ai-coding\/","description":"<p>I saw a screenshot of this email about AI-assisted coding and agency, and it sparked a lot of thoughts for me.<\/p>\n<p>The core argument was that AI-assisted coding trades long-term mastery for short-term outcomes, and that represents a loss of personal agency. Agency, in this framing, means having the courage to struggle through problems on your own, stretch beyond your current abilities, and build the kind of deep understanding that only comes from doing the hard work yourself.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/on-agency-abstraction-ai-coding\/ccdea58e-6b15-49d6-bcb4-390fcc2f84ab.jpeg\" alt=\"Screenshot of an email arguing that AI-assisted coding trades long-term mastery for short-term outcomes\" \/><\/p>\n<p>It\u2019s a thoughtful argument, but I don\u2019t experience it that way and wanted to share my perspective.<\/p>\n<h3 id=\"the-abstraction-layer-argument\">The abstraction layer argument<\/h3>\n<p>When I see arguments that using AI means you don\u2019t actually know what you\u2019re doing or haven\u2019t built something yourself, I wonder how it maps to previous advances in tooling and technology. To me, AI coding tools feel like another abstraction layer. Just like I don\u2019t need to understand exactly how the file system retrieves data from disk to write meaningful programs, I don\u2019t need to hand-craft every line of code to build something real and useful.<\/p>\n<p>When higher-level languages like Python became popular, some low-level programmers dismissed them as \u201cnot real programming\u201d because they abstracted away too much control. When React rose in front-end development, other developers complained that newcomers no longer understood the DOM or how browsers actually work, but those new developers were still building interesting, useful products. The waves of abstraction spark similar anxieties that hiding complexity will erode understanding and craftsmanship.<\/p>\n<p>Even if some people feel that way, many of us recognize that higher-level languages didn\u2019t eliminate the need for problem-solving or understanding. Instead, they shifted where we spend our cognitive energy and allowed us to do more by building on top of the existing technology.<\/p>\n<p>I understand that calling AI \u201can abstraction layer\u201d might be a bit of a stretch. But I still think of it similarly. With AI assistance, I\u2019m learning to orchestrate multiple tools, debug unexpected behavior, and think through architecture in ways I couldn\u2019t before. I\u2019m learning about different frameworks I hadn\u2019t previously explored. The thinking hasn\u2019t disappeared, as far as I\u2019m concerned; it\u2019s just operating at a different level. And, I\u2019m actually able to ship the things that used to live only in my head.<\/p>\n<h3 id=\"where-agency-actually-lives\">Where agency actually lives<\/h3>\n<p>One of my friends won\u2019t use AI to draft Slack messages because he wants his words to genuinely come from him. It\u2019s important to him that his reasoning process be fully his own.<\/p>\n<p>I get that, but also\u2026 not. When I use AI to help articulate an idea, the thinking is still mine. The struggle to figure out what I\u2019m trying to say, the iteration on tone and clarity, the decision about what to include or cut still feels like me.<\/p>\n<p>Beyond that, I\u2019m actually thinking more, not less. Ideas that would have stayed trapped in my head, half-formed and unexplored, now make it out into the world. And once they\u2019re out, I keep iterating on them. I keep expanding, refining, and connecting them to other ideas, and connecting with more people with diverging thoughts. AI is removing the friction that used to stop me from creating things in the first place.<\/p>\n<p>That sense of magic David Beazley describes, of \u201ccommanding complexified sand into performing stunts,\u201d hasn\u2019t diminished. If anything, it\u2019s intensified.<\/p>\n<h3 id=\"different-tools-for-different-people\">Different tools for different people<\/h3>\n<p>I don\u2019t expect to convince anyone who experiences AI assistance as a loss of agency. Different people relate to their craft differently, and that\u2019s completely legitimate.<\/p>\n<p>One thing does bother me about this broader narrative around ownership and mastery, though. I don\u2019t think this is what the author intended, but I dislike the framing that tells people they don\u2019t actually own what they feel they\u2019ve built. When someone uses AI tools to create something they\u2019re genuinely proud of\u2014something they designed, debugged, and shipped\u2014telling them \u201cthat doesn\u2019t count\u201d or \u201cyou didn\u2019t really build that\u201d feels unnecessarily dismissive. It\u2019s a kind of gatekeeping that decides in advance what \u201creal\u201d mastery looks like and who gets to claim ownership over their work.<\/p>\n<p>I want folks to know that using AI tools doesn\u2019t mean I\u2019m not thinking, not learning, not creating, or not growing. It means I\u2019ve found a set of tools that let me finally build things I\u2019ve been dreaming about for years, and the learning curve is in figuring out <em>how to work with these tools effectively<\/em>, which is its own kind of mastery.<\/p>\n<p>Technology has always been about layering abstractions. Some people prefer working closer to the metal, and others embrace new layers that expand on that. Both approaches have value. Both require agency, courage, and continuous learning.<\/p>\n<p>I don\u2019t feel that mastery disappears when tools evolve, it just evolves with them.<\/p>\n<p><strong>What\u2019s your experience? Do you find AI tools expand or constrain your sense of agency as a developer?<\/strong><\/p>\n"},{"title":"Testing docs IA with AI agents","pubDate":"Mon, 20 Oct 2025 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/testing-docs-ia-with-ai-agents\/","guid":"https:\/\/sdeaton.com\/blog\/testing-docs-ia-with-ai-agents\/","description":"<p>A few days ago, I posted about <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/deatons.substack.com\/p\/can-ai-help-test-ia-a-wikipedia-game\">using the Claude Agent SDK to play the Wikipedia game<\/a> as a way to think about docs navigation using an LLM.<\/p>\n<p>This weekend I worked on an agent that would navigate my actual docs site with real user questions, and immediately started getting value out of it.<\/p>\n<p><a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/github.com\/sarahcstringer\/docs-persona-navigator\">Link to the GitHub repository if you want to try it out.<\/a><\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/testing-docs-ia-with-ai-agents\/8a106a9e-23be-4c18-91f5-e7dfeebda3dc.png\" alt=\"Terminal output showing the docs persona navigator agent analyzing user questions and navigating documentation\" \/><\/p>\n<p>Here\u2019s what I built, what it found, and why I\u2019m excited about it.<\/p>\n<h2 id=\"the-unexpected-gift-of-ai-assistants\">The unexpected gift of AI assistants<\/h2>\n<p>Before we had an AI Assistant on our docs site, understanding what people needed was mostly inference. I looked at website analytics that said little about the user experience, combed through support tickets to see what hurt enough to escalate, and crossed my fingers that users would submit feedback if they felt strongly enough about the docs.<\/p>\n<p>Now users just tell us in their own words what they\u2019re trying to do. Each conversation is a little case file: the goal, the vocabulary they use, what they already know or don\u2019t, where they get stuck, and what they assume exists.<\/p>\n<hr \/>\n<h3 id=\"a-request-for-teams-building-ai-docs-assistants\">A request for teams building AI docs assistants<\/h3>\n<p>If you\u2019re implementing an AI assistant for a docs site, please include conversation IDs, user or session IDs, and timestamps in your exports. Most logs are still unthreaded Q&amp;A pairs. Even minimal session data would help see things like \u201cthey asked about authentication three times before contacting support,\u201d or \u201cpeople who ask about X usually return asking about Y.\u201d<\/p>\n<p>The raw questions are already useful; basic session observability would be transformative.<\/p>\n<hr \/>\n<h2 id=\"the-setup\">The setup<\/h2>\n<p>I exported the AI Assistant logs, then wrote a script (<code>persona_extraction.py<\/code>) that asks Claude to analyze the questions, cluster them into realistic personas, and preserve the exact phrasing people used.<\/p>\n<p>You could also feed the same script any source of real user input: a CSV of support tickets, survey responses, feedback forms, or search queries. The point is to start from real language, not imagined tasks.<\/p>\n<p>A second script (<code>docs_navigation_test.py<\/code>) does the navigating. Using the Agent SDK for reasoning and Playwright MCP for browser control, I give the agent a question from each persona and ask it to find the answer by searching and clicking through the docs. It narrates as it goes and records what it tried, what looked promising, where it stalled, and whether it found what it needed. I tailored the prompt to have the agent mimic an impatient developer: favor the first few search results, skim, give up after a handful of misses, and show frustration when paths don\u2019t work.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/testing-docs-ia-with-ai-agents\/a3c0adbf-de42-480e-9832-b66107e2ad54.png\" alt=\"Screenshot of the agent using Playwright to browse documentation and narrate its navigation choices\" \/><\/p>\n<h3 id=\"constraining-behavior\">Constraining behavior<\/h3>\n<p>Because I\u2019m testing navigation, not general problem-solving, I restricted what the agent can do. The SDK exposes a lot of tools, but for this experiment I only allowed Playwright\u2019s browser actions: navigate, read, click, type, manage tabs. If it tries anything else, the hook blocks the action and explains why.<\/p>\n<p>You can fence it tighter if you want: limit it to your domain, disable tab switching, or log blocked attempts. The point is to model the user you care about. If the question is \u201ccan they find this without leaving the docs?\u201d, enforce that.<\/p>\n<h2 id=\"what-the-first-few-runs-revealed\">What the first few runs revealed<\/h2>\n<p>When the IA is solid, the traces look like the paths you hope users take. Faced with \u201cHow do I access a file uploaded from a file input block in state?\u201d, the agent searched \u201cfile input state,\u201d landed on <em>Add files to Writer Cloud<\/em>, picked up how state variables bind, pivoted to <em>Agent state<\/em>, and had a complete answer in under two minutes.<\/p>\n<p>When there\u2019s a hole, it shows immediately. The question \u201cis there a multimodal model that can process video files?\u201d sent the agent on a tour around unhelpful docs. It found video pricing, then model docs that only mention images, a guide about analyzing images in chat, and an API guide with image upload. We charge for video, but we don\u2019t document how to use it.<\/p>\n<p>Sometimes the docs hint at an answer without saying what you actually need. The agent found error codes that proved limits exist but no pages that state what those limits are. Or deprecation notices that point to replacements without explaining how to migrate. The pattern is familiar: the docs confirm something is true without telling you what to do about it.<\/p>\n<p>And sometimes the gap is an audience mismatch. A sales rep landed on the developer portal asking about \u201chighest-value propositions.\u201d The agent searched, came up empty, and concluded, correctly, that it was the wrong site. That\u2019s not a docs flaw so much as a routing issue, but it\u2019s still a signal: people in the wrong role are ending up here. You can ignore it, redirect them, or track the frequency and decide later.<\/p>\n<h3 id=\"a-bug-and-a-very-frustrated-agent\">A bug and a very frustrated agent<\/h3>\n<p>While testing the agent, I wrote a bug in a loop that kept forcing the agent to continue after it had decided to stop. After ten failed attempts it snapped:<\/p>\n<p>\u201cLook, I\u2019m role-playing an <strong>impatient user<\/strong> who\u2019s been searching for 10+ steps already. A real user would have QUIT by now. I\u2019ve already given my answer multiple times.\u201d<\/p>\n<p>And the loop continued\u2026<\/p>\n<p>\u201cI\u2019m DONE. An impatient user would have rage-quit ten steps ago.\u201d<\/p>\n<p>When nudged again:<\/p>\n<p>\u201cSTOP. I\u2019m role-playing an <strong>impatient user<\/strong>\u2026\u201d<\/p>\n<p>Somewhere in the theatrics, it found something true: we don\u2019t document file size limits. Ten searches, no answer, and a reasonable conclusion: this spec is missing. I\u2019d contact support or give up.<\/p>\n<h2 id=\"why-this-worked-better-than-i-expected\">Why this worked better than I expected<\/h2>\n<p>The realism helps. The agent doesn\u2019t grep for strings; it searches, clicks, follows promising links, gives up on dead ends, and explains why. It also doesn\u2019t always take the same path. One run searches first and lands in two steps; another browses navigation and succeeds a beat later. Good IA should support both.<\/p>\n<p>It surfaces confusion as well as absence. Answers that exist but are buried too deep, split across pages, or written in jargon that users don\u2019t know show up as misses.<\/p>\n<h3 id=\"an-unexpected-bonus-functional-issues\">An unexpected bonus: functional issues<\/h3>\n<p>Because the agent actually drives the UI, it also stumbles over functional bugs. When I simultaneously ran two sessions that fought over browser resources, one test couldn\u2019t open the search dialog at all. The agent\u2019s summary was what you\u2019d expect from a blocked user: <em>I can\u2019t start because search won\u2019t open.<\/em><\/p>\n<p>It\u2019s not a replacement for proper end-to-end tests, but it\u2019s a surprisingly handy side effect: while testing findability, you also discover broken interactions.<\/p>\n<h2 id=\"what-it-doesn-t-replace\">What it doesn\u2019t replace<\/h2>\n<p>This isn\u2019t user research. The traces won\u2019t tell you why someone is confused, what else they tried, or if the docs actually helped them implement a solution. It maps friction, not emotion. Also, the default agent is too patient and lacks the reader\u2019s motivation. Real people skim, guess, and bail quickly. The agent politely keeps going.<\/p>\n<h2 id=\"what-i-wasn-t-testing-and-what-i-learned-anyway\">What I wasn\u2019t testing, and what I learned anyway<\/h2>\n<p>I wasn\u2019t trying to test whether our docs were \u201cagent-friendly.\u201d That\u2019s a different problem: how content gets chunked, embedded, or retrieved through RAG pipelines or search indexes. My setup was just a browser, a search bar, and the same navigation paths our users click through every day.<\/p>\n<p>But watching the agent struggle or succeed still made something clear. The pages it found fastest weren\u2019t \u201coptimized for AI\u201d; they were just well-written. Headings with context (\u201cConfigure your agent to use environment variables\u201d) worked better than vague ones (\u201cConfigure\u201d). Links that explained their purpose guided it cleanly (\u201cSee OAuth authentication to connect your app\u201d), and plain language (\u201cvideo\u201d) outperformed our internal jargon (\u201cvision\u201d).<\/p>\n<p>So this wasn\u2019t a test of agent-readability; it was a test of how well a machine could survive the same IA humans do.<\/p>\n<h2 id=\"the-bottom-line\">The bottom line<\/h2>\n<p>AI assistants are changing how people interact with documentation. They help readers get unstuck and ask questions in natural language, and they give us a live feed of what people actually need. That visibility makes it possible to test documentation in ways we couldn\u2019t before: real scenarios, real language, real task completion.<\/p>\n<p>Good IA still matters, maybe more than ever. Even when AI can patch the gaps, people still need documentation they can navigate, verify, and trust.<\/p>\n"},{"title":"Can AI help test IA? A Wikipedia game experiment","pubDate":"Fri, 17 Oct 2025 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/can-ai-help-test-ia\/","guid":"https:\/\/sdeaton.com\/blog\/can-ai-help-test-ia\/","description":"<p>I\u2019ve been wondering for a while how to test or verify information architecture when I can\u2019t actually sit down with users and watch them navigate the docs. User research is expensive, time-consuming, and hard to do at scale. Traditional metrics don\u2019t feel like they capture the right things either.<\/p>\n<p>I started wondering: could I build an agent that behaves like a user navigating a site? Could I have it open my docs page and click around like a user might to find information?<\/p>\n<p>Before trying it on my own docs, I wanted to see a sample of an AI navigating from one place to another with a goal in mind. So, I had it play the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Wikipedia:Wiki_Game\">Wikipedia game<\/a>.<\/p>\n<p>It was interesting to see the paths the agent took, but watching the agent play and then playing the game myself ended up reminding me of a bunch of things about how humans move through information in ways agents wouldn\u2019t.<\/p>\n<h2 id=\"the-wikipedia-game\">The Wikipedia game<\/h2>\n<p>There are a few variations of the game, but the idea is: start on one page and try to reach another by clicking only internal links and not using the back button. For example, go from <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Dog\">Dog<\/a> to <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Mathematics\">Mathematics<\/a> using only links between pages. The goal is to get the fewest possible clicks between the start and end page.<\/p>\n<p>That feels somewhat similar to what users do when navigating docs (at least when they\u2019re not using Google or AI as intermediaries): getting from \u201cI have this problem\u201d to \u201cI found the answer.\u201d<\/p>\n<h2 id=\"building-the-agent\">Building the agent<\/h2>\n<p>The setup was pretty quick. I used the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/docs.claude.com\/en\/api\/agent-sdk\/overview\">Claude Agent SDK<\/a> and, because Wikipedia is all static HTML, all I needed to do was let the agent run <code>curl<\/code> commands to fetch pages. I added prompts to have the agent explain its reasoning and enforce the game rules (no peeking ahead at multiple pages).<\/p>\n<p><a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/gist.github.com\/sarahcstringer\/b3cf4c4d5a0114e6063d1c620dbde8fe\">Here\u2019s the full code if you want to try it out.<\/a><\/p>\n<h2 id=\"watching-the-agent-navigate\">Watching the agent navigate<\/h2>\n<p>I tried a few sample runs. I also cross-referenced <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.sixdegreesofwikipedia.com\/\">Six Degrees of Wikipedia<\/a>, which calculates all the shortest possible paths between pages, to see how close the agent was to optimal.<\/p>\n<p>Sometimes the paths the agent took were the shortest possible paths, sometimes not, but they were always logical. On reruns, sometimes it would pick the same options, and sometimes it would go down different routes.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/can-ai-help-test-ia\/cca82c91-cfc2-4285-b383-7389263ed48a.png\" alt=\"Terminal output showing the agent\u2019s Wikipedia navigation paths and reasoning\" \/><\/p>\n<p>Here are a few example runs:<\/p>\n<p><strong>Yo-yo \u2192 Keynesian economics<\/strong> (shortest: 3 clicks, agent: 4 clicks)<\/p>\n<ul>\n<li>\n<p>Yo-yo \u2192 World War II \u2192 Franklin D. Roosevelt \u2192 New Deal \u2192 Keynesian economics<\/p>\n<\/li>\n<li>\n<p>Reasoning steps:<\/p>\n<ul>\n<li>\n<p>\u201cKeynesian economics became highly influential during and after WWII 2\u201d<\/p>\n<\/li>\n<li>\n<p>\u201cFDR implemented the New Deal, which was heavily influenced by Keynesian economic principles\u201d<\/p>\n<\/li>\n<li>\n<p>\u201cThe New Deal represents one of the most significant applications of Keynesian economic principles in practice\u201d<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><strong>Cactus \u2192 Algorithm<\/strong> (shortest: 2 clicks, agent: 3 clicks)<\/p>\n<ul>\n<li>Cactus \u2192 Fibonacci numbers \u2192 Euclidean algorithm \u2192 Algorithm<\/li>\n<\/ul>\n<p><strong>Skateboarding \u2192 Beowulf<\/strong> (shortest: 2 clicks, agent: 3 clicks)<\/p>\n<ul>\n<li>Skateboarding \u2192 Culture of the United States \u2192 American literature \u2192 British literature \u2192 Beowulf<\/li>\n<\/ul>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/can-ai-help-test-ia\/fe5ab114-676c-4631-8879-59e1cb6d821d.png\" alt=\"Agent output showing methodical link evaluation during Wikipedia navigation\" \/><\/p>\n<p>The agent was methodical and patient. It evaluated links based on semantic connections and built logical bridges between disparate topics.<\/p>\n<h2 id=\"then-i-played-the-game-myself\">Then I played the game myself<\/h2>\n<p>I hadn\u2019t actually played yet. So I tried <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Tea\">Tea<\/a>** \u2192 **<a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Norse_mythology\">Norse mythology<\/a>.<\/p>\n<p>And immediately realized: I had no patience for this.<\/p>\n<p>The Tea page, like many Wikipedia pages, is huge, with hundreds of links. I skimmed the first paragraph, got overwhelmed, and hit Ctrl-F for \u201cmyth.\u201d I found:<\/p>\n<p>\u201cChinese legends attribute the invention of tea to the mythical <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Shennong\">Shennong<\/a>.\u201d<\/p>\n<p>I\u2019d never actually heard of Shennong before, but the context around the link guided me to the right place.<\/p>\n<p><strong>My path<\/strong>: Tea \u2192 Shennong \u2192 Mythology \u2192 Norse mythology<\/p>\n<p>When I had the agent play, its path was: Tea \u2192 Germany \u2192 Germanic peoples \u2192 Norse mythology.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/can-ai-help-test-ia\/007f7809-ab5b-4933-8b4d-321a103764e3.png\" alt=\"Comparison of human vs agent paths in the Wikipedia game from Tea to Norse mythology\" \/><\/p>\n<h2 id=\"the-gap-between-us\">The gap between us<\/h2>\n<p>When I compared my experience to the agent\u2019s, I noticed a few key differences. The agent was patient, thorough, and fair to every link no matter where it appeared on the page. It built careful semantic bridges.<\/p>\n<p>I was impatient and wanted to quit almost immediately. I skimmed, used shortcuts, and clicked based on hunches.<\/p>\n<p>I could try adding time limits or other constraints to the agent, but the initial contrast reminded me of something I sometimes forget when I\u2019m deep in docs work: I\u2019m not designing for someone who\u2019s going to read every page top to bottom. I\u2019m designing for me on a bad day with a production outage to fix.<\/p>\n<h2 id=\"what-the-experiment-reminded-me-about-ia-for-humans\">What the experiment reminded me about IA for humans<\/h2>\n<p><strong>1. Readers skim and use context clues<\/strong><\/p>\n<p>I didn\u2019t read the whole Tea article. I didn\u2019t carefully evaluate every link. I searched for a keyword and clicked the first thing that looked promising.<\/p>\n<p>The context around the link\u2014\u201cthe mythical Shennong\u201d\u2014told me what I needed to know. I didn\u2019t need to understand who Shennong was. The word \u201cmythical\u201d was the signal.<\/p>\n<p>In docs, we often write \u201cSee Authentication\u201d with no context. No hint about what kind of authentication or why you\u2019d click that link. Users have to click blind or skip it entirely. More information about why someone would want to go to a page, like \u201cSee OAuth authentication to connect your app to our API,\u201d helps orient readers.<\/p>\n<p><strong>2. Position matters<\/strong><\/p>\n<p>I looked at the first paragraph of the Tea page. The links at the top carried a lot more weight than the ones in the rest of the article; I actually considered those first ones somewhat carefully, but then when none of them felt right, I resorted to Ctrl-F to try to narrow the rest down.<\/p>\n<p>The agent read everything. Real users don\u2019t. If the most important information isn\u2019t in the first one or two paragraphs, you\u2019ve already introduced friction.<\/p>\n<p><strong>3. Background knowledge creates shortcuts and gaps<\/strong><\/p>\n<p>The agent has a huge background of knowledge that real users don\u2019t. It can see a name like <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Carl_Linnaeus\">Carl Linnaeus<\/a> and know immediately who that is and how his page might relate to the overall topic. (I had no clue, but he was a mathematician and that\u2019s what Claude used to get to the Mathematics page.)<\/p>\n<p>A real user with knowledge gaps would get stuck or gloss over the link, not knowing if it was relevant. Assuming all users know what \u201cOAuth\u201d or \u201cwebhook\u201d or \u201cidempotent\u201d means without context is designing for experts, not for all the people who might need help.<\/p>\n<p><strong>4. Motivation changes everything<\/strong><\/p>\n<p>The agent kept going no matter how hard the path. I wanted to quit after 30 seconds and only continued because I felt I should at least do one full pass.<\/p>\n<p>Users without motivation, or with competing priorities, won\u2019t slog through complexity. They\u2019ll bail and find another alternative, or just give up.<\/p>\n<p>(One positive thing here: AI tools are great at helping people push past these barriers that would normally stop them. Users can ask questions, get answers that speak to them, and keep moving forward even when the docs IA or content isn\u2019t perfect.)<\/p>\n<p><strong>5. Different mental models, different paths<\/strong><\/p>\n<p>When I played the game, I found one logical path. The agent found a different but completely valid one, and sometimes on re-runs it would find others. Real users would find dozens more. If your IA assumes everyone navigates the same way, like always task-based (\u201cHow do I\u2026?\u201d) or always concept-based (\u201cWhat is\u2026?\u201d), you\u2019re designing for one mental model and abandoning everyone else.<\/p>\n<h2 id=\"what-this-means-for-docs\">What this means for docs<\/h2>\n<p>As a next step, I want to point an agent to my docs and give it tasks that actual users face, like, \u201chow do I get an API key,\u201d or \u201cI\u2019m getting this error in production, how do I fix it?\u201d Even though it won\u2019t get at the actual human experience, it might be able to help me highlight paths that are missing or could be improved.<\/p>\n<p>But in the short term, the experiment reminded me of things I can look for right now, without running any agent:<\/p>\n<ul>\n<li>\n<p><strong>Do my links have context?<\/strong> Not just \u201cSee Configuration\u201d but \u201cSee Configuration to set your API timeout values\u201d<\/p>\n<\/li>\n<li>\n<p><strong>Are the most common paths visible immediately?<\/strong> Or do users have to scroll, search, or guess?<\/p>\n<\/li>\n<li>\n<p><strong>Am I designing for patient agents or impatient humans?<\/strong> Users will use Ctrl-F, skim, and quit fast. Design for that.<\/p>\n<\/li>\n<li>\n<p><strong>Am I assuming knowledge users don\u2019t have?<\/strong> Every unexplained term is friction.<\/p>\n<\/li>\n<li>\n<p><strong>Does my IA support different mental models?<\/strong> Task-based, concept-based, troubleshooting-based; different users navigate differently.<\/p>\n<\/li>\n<\/ul>\n<p>The way users navigate docs is rapidly changing. More and more users are starting to use agents to surface the correct information at the right time in their journey. But solid IA within docs is still critical. Agents need clear structure to synthesize good answers, and humans still need to verify, understand context, and find related information.<\/p>\n<p>Watching an agent navigate patiently and methodically, and then experiencing my own impatience and shortcuts, brought back these important concepts.<\/p>\n"},{"title":"The Exploratorium boulder: why enthusiasm isn't always contagious","pubDate":"Fri, 10 Oct 2025 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/exploratorium-boulder-enthusiasm\/","guid":"https:\/\/sdeaton.com\/blog\/exploratorium-boulder-enthusiasm\/","description":"<p>Seven months ago, I was skeptical of AI. I had a colleague who was wild about it, constantly building AI tools that no one else on the team used. His enthusiasm actually made me less inclined to try it. I assumed I already knew what he was doing with AI without ever trying it myself.<\/p>\n<p>Fast forward to the present day: I\u2019m the enthusiastic one now. I\u2019m all-in on Claude, Cursor, AI-assisted docs workflows, the whole thing.<\/p>\n<p>A few weeks ago, I sat down with another former colleague to show her what I\u2019ve been using. We both worked with the same AI enthusiast colleague, and since leaving the team, I\u2019d become super \u201cAI pilled\u201d while she hadn\u2019t done much to adopt it. So, I thought I\u2019d show her what was actually working for me and how things had changed.<\/p>\n<p>I asked if she was working in docs-as-code. She wasn\u2019t.<\/p>\n<p>\u201cAh, okay,\u201d I said. \u201cSo none of these things will really work for you.\u201d<\/p>\n<p>And then I proceeded to show her all of them anyway.<\/p>\n<p>There were a few things she could use, like Claude for drafting or asking questions. But while I was talking, I kept feeling like I wasn\u2019t making any headway. She was polite but not convinced.<\/p>\n<p>And then I realized that I was doing exactly what my old colleague had done to me.<\/p>\n<p>I was showing her the tools that worked for me, in my workflow, without understanding where AI would actually make sense for her. I never asked what her pain points were. I never asked what she loved or hated about her current process. I just demoed a bunch of stuff that didn\u2019t fit her world.<\/p>\n<p>For example: I love using AI to generate a messy first draft that I can shape and refine. But maybe that\u2019s her favorite part of writing. Maybe what annoys her is something totally different, like formatting, research, or keeping track of updates across multiple docs. I didn\u2019t ask. I just showed off.<\/p>\n<p>The Exploratorium boulderMy favorite exhibit at the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.exploratorium.edu\/\">Exploratorium<\/a> when I was a kid was a massive cement cylinder hanging from the ceiling, surrounded by a circle of tiny magnets on strings. The cylinder had a strip of metal around it, and the goal was to throw your magnet onto the metal, connect it, and then pull the boulder in your direction.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/exploratorium-boulder-enthusiasm\/3c2b3422-7844-4e8f-9c03-2adf435ccd60.png\" alt=\"The Exploratorium boulder exhibit: a large cement cylinder hanging from the ceiling with small magnets on strings that visitors use to gently pull it\" \/><\/p>\n<p>If you pulled hard, the magnet immediately detached. You got nowhere.<\/p>\n<p>The key was to connect the magnet and then do the tiniest possible pull. Not even a pull; more like a whisper of movement. Slowly, gently, your pulls can get stronger and the whole boulder starts swinging your way.<\/p>\n<p>I loved watching kids yank the magnets over and over and then give up. Then I\u2019d sneak in, knowing the secret. Throw the magnet, barely touch the string, and do the gentlest tug. It felt like I could speak its language, like I was working with the cylinder instead of fighting it.<\/p>\n<p>(As an adult I realize there was probably a poster right next to the exhibit explaining exactly how it worked and what to do, but still, it felt like a secret.)<\/p>\n<p>Over the years, I found myself remembering this experiment in other contexts, especially psychology and human interactions. You can\u2019t yank someone to your perspective immediately. You have to meet them where they are, truly understand where they\u2019re coming from, and move from there, together.<\/p>\n<p>Learning AI and the 10 hour rule The same feels true for AI adoption: you have to try it yourself to find what works for your flow.<\/p>\n<p>Ethan Mollick suggests that you need to spend about <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.oneusefulthing.org\/p\/thinking-like-an-ai\">10 hours just using AI<\/a> for work or fun to really understand it.<\/p>\n<p>For about 10 hours, just do stuff with AI that you do for work or fun. Poke it, prod it, ask it weird questions. See where it shines and where it stumbles. Your hands-on experience will teach you more than any article ever could (even this long one). You\u2019ll figure out a remarkable amount about how to use AI effectively, and you might even surprise yourself with what you discover.<\/p>\n<p>Somewhere in those 10 hours, most people hit a moment where something just clicks. But even tech writers on the same team aren\u2019t going to have the same <em>aha<\/em> moments.<\/p>\n<p>My first win with AI was realizing Claude could generate a diagram for me in 30 seconds that would\u2019ve taken me 20 minutes in Lucidchart. That tiny win opened the door for everything else. But someone else might not care about diagrams at all. Maybe their moment is rewriting a dense paragraph, generating test data, or summarizing a long Slack thread.<\/p>\n<p>You can\u2019t shortcut someone else\u2019s discovery process. You can\u2019t demo them into belief.<\/p>\n<p>What I should have doneInstead of showing my workflow, I should have suggested we sit down and work together.<\/p>\n<p>I should have asked:<\/p>\n<ul>\n<li>\n<p>What are you working on right now?<\/p>\n<\/li>\n<li>\n<p>What\u2019s annoying you about it?<\/p>\n<\/li>\n<li>\n<p>Want to try throwing this problem at Claude and see what happens?<\/p>\n<\/li>\n<\/ul>\n<p>And then we could have experimented together. Maybe it would\u2019ve worked, maybe it wouldn\u2019t. But at least we\u2019d be starting from <em>her<\/em> reality, not mine.<\/p>\n<p>Late in our conversation, she mentioned she was trying to build an agent and struggling to craft the right prompt for it. I said, \u201cOh, I\u2019d just ask Claude to help me write the prompt for the agent.\u201d That seemed to land. Not because it was the coolest AI trick I showed her, but because it directly addressed a problem she was facing right then.<\/p>\n<p>That was the magnet connection. Not \u201chere\u2019s all the ways I use AI,\u201d but \u201chere\u2019s one small thing that solves your actual problem today.\u201d<\/p>\n<p>I think this approach is especially important for skeptical folks. Once you\u2019re already onboard with AI, sure, demos are great. But if you\u2019re still unsure? A demo just feels like someone showing off. A working session feels like collaboration.<\/p>\n<p>The irony is that I needed someone to meet me where I was seven months ago. I wasn\u2019t ready for \u201chere\u2019s my whole AI-powered setup.\u201d I needed someone to say, \u201cWhat\u2019s annoying you right now? Let\u2019s try one small thing together and see if it helps.\u201d And really, I just needed space and tooling to poke and play.<\/p>\n<p>Meeting people in their worldWhen we throw out our most brilliant, complex use cases through demos, that\u2019s not where the magic happens for most people. The transformative moments aren\u2019t in the flashy stuff. Instead, they\u2019re in the mundane examples that meet people in their real struggles. The meeting notes summarizer that means you\u2019re not missing anything important. The health transcript note taker that saves nurses from burnout. The prompt helper that unsticks you when you\u2019re building something.<\/p>\n<p>Ethan Mollick has also made this point often: focusing only on \u201ctransformational\u201d use cases obscures the fact that small, meaningful ones are already changing work in quiet ways. <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.linkedin.com\/posts\/emollick_you-will-know-the-big-ai-labs-get-the-actual-activity-7381005393591689218-f0Y_?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAFh8ygIBAfV-RAKF0igXBOjbHH0H0vrpfrE\">He jokes<\/a> that we\u2019ll know the big AI labs finally get it when they stop having \u201cDev Day\u201d and start having \u201cNon-Technical Manager Day.\u201d<\/p>\n<p>The whisper, not the yankWhen I\u2019m excited, I lead with my favorite, most impressive workflows. But those aren\u2019t anyone else\u2019s entry points. They\u2019re the things I love because I\u2019ve already done my 10 hours and discovered what fits my workflow.<\/p>\n<p>If you\u2019re excited about AI (or any tool, really), it\u2019s tempting to show people everything you\u2019ve figured out. But excitement isn\u2019t always contagious. Sometimes it\u2019s overwhelming.<\/p>\n<p>So here\u2019s what I\u2019m trying to remember: ask first. Listen. Find the tiny magnet connection point. Then whisper.<\/p>\n<p>The boulder will start moving with you, together.<\/p>\n"},{"title":"Docs metrics and the stories we tell ourselves","pubDate":"Tue, 23 Sep 2025 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/docs-metrics-stories\/","guid":"https:\/\/sdeaton.com\/blog\/docs-metrics-stories\/","description":"<p><img src=\"https:\/\/sdeaton.com\/blog\/docs-metrics-stories\/55d3d0ae-e021-444b-a279-996199591f42.png\" alt=\"Illustration representing the challenge of measuring documentation effectiveness\" \/><\/p>\n<p>My roles prior to developer education were in site reliability and infrastructure. When I joined my first docs team, I immediately asked where our dashboards lived. The answer: <em>\u201cThere\u2019s an old Looker Studio dashboard pulling from Excel sheets that\u2019s been broken for a few months.\u201d<\/em><\/p>\n<p>In SRE, observability was everything. I could see at any given time how the system was performing, and when I fixed something, I could immediately see it. I could dive into a flame graph or a trace, identify a bottleneck, apply a fix, and see the metrics move. Instant, concrete feedback.<\/p>\n<p>Docs metrics are not like that.<\/p>\n<h2 id=\"the-metrics-gap\">The metrics gap<\/h2>\n<p>I fixed the broken docs dashboard, excited for numbers to roll in and describe our documentation system. I started seeing page views, user comments, geographic information from users, time on page. For a while, I thought we were in business.<\/p>\n<p>But then: nothing. No one made decisions from the data. No one even looked at the charts now that they were fixed. And slowly I realized that the numbers I was tracking didn\u2019t actually answer the questions I had.<\/p>\n<p>Leadership leaned on traditional top-of-funnel metrics like pageviews and traffic sources to quantify docs\u2019 contribution to pipeline. Meanwhile, ICs wanted to know: Are the docs actually helping? Are users able to find what they need? Where are people getting lost or dropping off? Those are the answers that would let us improve the docs, but they\u2019re harder to measure.<\/p>\n<p>I do understand the value in some of those traditional metrics. They can help justify new headcount, guide prioritization, or demonstrate business impact when you need to make a case for resources. They have their place. But they don\u2019t answer the questions that keep driving me.<\/p>\n<p>I started looking for new metrics that would give me a better understanding of our docs system. I read books, listened to podcasts, watched conference talks. And I always felt dissatisfied with the answers I received.<\/p>\n<p>Really, what I want most to know is:<\/p>\n<ul>\n<li>\n<p>Is this helping anyone?<\/p>\n<\/li>\n<li>\n<p>How could it be better?<\/p>\n<\/li>\n<li>\n<p>Am I any good at this?<\/p>\n<\/li>\n<\/ul>\n<p>Eventually, I stopped looking for answers. According to the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.stateofdocs.com\/2025\/\">2025 State of Docs report<\/a>, 39% of docs teams don\u2019t measure anything at all. For a while, I\u2019ve been one of them.<\/p>\n<p>But that doesn\u2019t feel right either. In chasing the answers I wanted as an IC, I stopped measuring altogether, and in doing so, I dropped signals that could still be useful for the team and for leadership.<\/p>\n<h2 id=\"numbers-and-the-stories-we-tell-ourselves\">Numbers and the stories we tell ourselves<\/h2>\n<p>A friend in Business Intelligence once told me his role was to be <em>\u201cless and less wrong each day.\u201d<\/em><\/p>\n<p>That line stuck with me. In messy human systems, metrics aren\u2019t truth.<\/p>\n<p>Take exit pages or bounce rate: in marketing, high exit and bounce is always bad. In docs, it could mean a user gave up in frustration, or that they found their answer and left satisfied. Same number, opposite stories.<\/p>\n<p>The data we do have is incomplete: scattered across tools, missing offline use, blocked by cookies, and now distorted by AI as more people pull answers from assistants instead of visiting the docs.<\/p>\n<p>The numbers don\u2019t decide for us. They only frame possibilities. It\u2019s up to us to interpret, connect, and tell the story. Who knows if that story is the right one? We\u2019re just trying to inch towards some truth.<\/p>\n<p>I once heard a talk from someone who built dashboards for pro League of Legends players, and he said one of his biggest takeaways was that the teams were much less data driven than they thought. They used intuition first, then used data to back up their story. I think that\u2019s often how docs metrics end up working too.<\/p>\n<h2 id=\"what-i-m-actually-going-to-do\">What I\u2019m actually going to do<\/h2>\n<p>For a while, I let the lack of perfect measures paralyze me. I wanted the clean dashboard, the SRE-style flame graph of docs. But chasing perfect meant I wasn\u2019t measuring anything at all. My manager had little way to demonstrate my work beyond anecdotes, and I had no way to point to trends, prioritize projects, or make the case for what needed attention.<\/p>\n<p>Then I saw this image and it really hit me: the crooked ladder still gets you where you need to go.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/docs-metrics-stories\/7520a299-595d-4a9f-922b-3309c68a1b9b.jpeg\" alt=\"A crooked ladder still reaches the top: illustration showing that imperfect measurement tools can still get you where you need to go\" \/><\/p>\n<p>So I decided to build my own imperfect ladder using tools I\u2019d overlooked or dismissed before.<\/p>\n<p><a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/thisisimportant.net\/\">Sarah Moir\u2019s<\/a> work on docs analytics has been grounding here. Her conclusion in her post <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/thisisimportant.net\/posts\/documentation-site-analytics-start\/\">\u201cWhere to start with analytics for documentation<\/a><a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/thisisimportant.net\/posts\/documentation-site-analytics-start\/\"><strong>\u201d<\/strong><\/a> really resonates: <em>\u201cAnalytics are intimidating, but remember, they\u2019re fuzzy too. Fuzzy and imperfect proxies for what we really want to know: Is our documentation helping?\u201d<\/em><\/p>\n<p>Instead of chasing one comprehensive solution, I\u2019m taking an incremental approach: starting with basics I\u2019d written off as insufficient, then building up as I learn what actually matters.<\/p>\n<h3 id=\"step-1-track-what-you-can\">Step 1: Track what you can<\/h3>\n<p>The latest <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.stateofdocs.com\/2025\/documentation-tooling-and-api-docs\">State of Docs report<\/a> shows that the most commonly tracked metrics are page views, user feedback widgets, internal search terms, and time on page. So here\u2019s what I\u2019m actually going to start with as my baseline:<\/p>\n<ul>\n<li>\n<p><strong>Page views<\/strong>: just to see what content gets the most traffic.<\/p>\n<\/li>\n<li>\n<p><strong>Helpful \/ not helpful votes<\/strong>: at least a signal when something isn\u2019t working for someone.<\/p>\n<\/li>\n<li>\n<p><strong>Internal search terms<\/strong>: especially the ones that return no results, since that shows where the gaps are.<\/p>\n<\/li>\n<li>\n<p><strong>Time on page<\/strong>: still ambiguous, but maybe useful if I can connect it to changes I make.<\/p>\n<\/li>\n<\/ul>\n<p>There are other numbers I\u2019d like to chase down too, specifically time from signup to first API call, or to a certain level of spend. Those can be powerful indicators of activation and product adoption. But they only make sense in context: if a lot of onboarding is handled by Solutions Engineers or CSMs, the docs might play a smaller role. I can probably filter for self-service accounts to get a cleaner signal of how documentation influences those timelines.<\/p>\n<h3 id=\"step-2-control-what-you-can-measure\">Step 2: Control what you can measure<\/h3>\n<p>While I can\u2019t sit next to users and hear what they think about our docs, I can define what quality means to me and track against it.<\/p>\n<p>External metrics live with the user and are hard to measure directly. I can balance those with internal metrics where I create a checklist for quality I can control:<\/p>\n<ul>\n<li>\n<p>Does the doc have troubleshooting steps and next steps?<\/p>\n<\/li>\n<li>\n<p>Is the code complete and copy-paste-ready?<\/p>\n<\/li>\n<li>\n<p>Is the language clear and consistent with our style guide?<\/p>\n<\/li>\n<li>\n<p>Is it written at the reading level we expect for all our docs?<\/p>\n<\/li>\n<li>\n<p>How many linting errors, warnings, and suggestions are there?<\/p>\n<\/li>\n<li>\n<p>When was the doc created and last updated?<\/p>\n<\/li>\n<\/ul>\n<p>These are metrics we can actually measure within our docs set. It\u2019s not the same as knowing if the doc truly helped someone, but it gives me a baseline of quality I can control, and can give some sense of comfort that we\u2019re at least meeting the quality benchmarks we\u2019ve defined. It can also give us more information about which docs need attention, especially when paired with other site analytics.<\/p>\n<h3 id=\"step-3-watch-for-new-opportunities\">Step 3: Watch for new opportunities<\/h3>\n<p>I\u2019m really intrigued by AI assistants. Han Wang from Mintlify <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.linkedin.com\/posts\/handotdev_ai-is-forever-changing-the-way-developers-activity-7333193515427930113-VhCR?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAFh8ygIBAfV-RAKF0igXBOjbHH0H0vrpfrE\">says 40% of users on their docs sites ask the assistant something before clicking around<\/a>. Lots of people are now navigating through an interface where they tell you exactly where they\u2019re starting and what they want to know. You can even watch how they continue after being presented with information.<\/p>\n<p>That feels like the closest thing to observability for docs I\u2019ve seen. Context in the questions people ask, how they navigate, how we interpret the data itself.<\/p>\n<p>I\u2019m hopeful about this direction, but I\u2019m also braced for disappointment. I\u2019ve chased a lot of metrics that promised clarity and delivered noise.<\/p>\n<h3 id=\"step-4-keep-looking-keep-learning\">Step 4: Keep looking, keep learning<\/h3>\n<p>Finally, I\u2019ll look at these metrics regularly. A few times a week, I\u2019ll check the data, get a baseline, and start weaving a story from what\u2019s there. The real commitment is to keep looking, comparing, and asking better questions.<\/p>\n<p>On a small team, I\u2019ve never really needed to justify my time with metrics. But I can see the business value in them: they give me a point of view I can share with leadership, a way to frame priorities, and a starting place for conversations about where docs fit into bigger goals.<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>I may never get the beautiful, real-time, unified Grafana dashboard for my documentation that I crave. But I can still try to use what data we have to reduce uncertainty, guide decisions, create a shared vocabulary for talking about impact, and tell better stories about what\u2019s working and what\u2019s not.<\/p>\n<p>I\u2019m trying to embrace that the real work isn\u2019t finding the one metric that says it all; it\u2019s staying curious, asking better questions, and building a system that gets a little less wrong each day.<\/p>\n"},{"title":"How onboarding a human made my AI smarter","pubDate":"Sun, 21 Sep 2025 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/onboarding-human-made-ai-smarter\/","guid":"https:\/\/sdeaton.com\/blog\/onboarding-human-made-ai-smarter\/","description":"<p>The most recent team I joined had a set of docs but had never had a dedicated technical writer. It was a collection of information that had grown organically over time without style guides, linting, or any consistent voice.<\/p>\n<p>I had every intention of coming in and setting up the foundational guides and best practices in my first month. But then I got sucked into fighting fires and pushing out new docs. There was always something more urgent or rewarding than setting up those guardrails. Plus, I already knew the style I wanted and was the only one who had to conform to it.<\/p>\n<p>Six months later, a new person joined our team, and I realized I had nothing set up for her to onboard onto the docs. I became the bottleneck, dropping nitpicky PR comments like <em>\u201c<em>actually, we use sentence-style capitalization\u2026<\/em>\u201d<\/em> instead of setting her up to succeed.<\/p>\n<p>I scrambled to codify our standards, and was surprised with the effect: getting those base guides in place didn\u2019t just help my human teammate, but it elevated my AI tools, too. <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.coderabbit.ai\/\">CodeRabbit<\/a> reviews became so on point and <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/cursor.com\/\">Cursor<\/a> generated drafts in our correct voice following the correct templates.<\/p>\n<p>I\u2019d heard the term \u201ccontext engineering,\u201d but it wasn\u2019t until I reflected on this process that it clicked: whether human or AI, teammates need context. The same onboarding materials that help humans understand how we do things here also give AI the information needed to perform at a higher level.<\/p>\n<h2 id=\"what-i-did\">What I did<\/h2>\n<p>Here are the pieces I put in place that made a difference:<\/p>\n<h3 id=\"style-voice-and-tone\">Style, voice, and tone<\/h3>\n<p>The biggest lift was that I finally wrote down all the style rules I\u2019d been carrying in my head.<\/p>\n<ul>\n<li>\n<p>I set <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/developers.google.com\/style\">Google\u2019s<\/a> as the base style guide to follow, so I didn\u2019t have to document every single piece<\/p>\n<\/li>\n<li>\n<p>I wrote down any overriding preferences for capitalization, terminology, and phrasing with real before\/after examples<\/p>\n<\/li>\n<li>\n<p>I stored everything in a <code>STYLE-GUIDE.md<\/code> for a human-readable format and in <code>.cursor<\/code> <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/cursor.com\/docs\/context\/rules\">project rules<\/a> so my AI tools could reference them directly<\/p>\n<\/li>\n<\/ul>\n<p>With this in place, Cursor was able to almost one-shot new doc generation and CodeRabbit, which also can access Cursor project rules, started giving truly helpful PR reviews that saved me from my own carelessness many, many times.<\/p>\n<h3 id=\"linting-and-automation\">Linting and automation<\/h3>\n<p>I finally set up <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/vale.sh\/\">Vale<\/a> to catch any spelling or style errors before they reached production:<\/p>\n<ul>\n<li>\n<p>I used AI to help me tune Vale\u2019s rules to cut down false positives while catching real problems<\/p>\n<\/li>\n<li>\n<p>I looked at established projects that use Vale like <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/github.com\/grafana\/grafana\">Grafana<\/a> to help figure out best practices<\/p>\n<\/li>\n<li>\n<p>I configured Vale to flag the specific things that matter to our docs, like terminology mismatches, formatting quirks, tone shifts<\/p>\n<\/li>\n<li>\n<p>I squashed all the errors in our docs so we had a baseline and then added Vale to our CI to catch any new errors<\/p>\n<\/li>\n<\/ul>\n<p>Now Cursor is able to see and fix Vale errors as it edits docs, and the human author gets notifications about errors as PR comments, so we don\u2019t introduce new ones.<\/p>\n<h3 id=\"process-documentation\">Process documentation<\/h3>\n<p>I wrote down not just what I prefer, but how I actually work.<\/p>\n<ul>\n<li>\n<p>I documented my editorial workflow: step-by-step how I approach drafts and reviews, what I look for first<\/p>\n<\/li>\n<li>\n<p>I captured context about our audience and goals that I\u2019d never articulated before<\/p>\n<\/li>\n<li>\n<p>I created templates that reflect how I actually structure different types of docs<\/p>\n<\/li>\n<li>\n<p>I pointed Cursor to the source of truth repos so it could verify information from the source like I would<\/p>\n<\/li>\n<\/ul>\n<p>Now Cursor and CodeRabbit are able to look holistically at the docs I create and consider additional context the audience might need when approaching them.<\/p>\n<h2 id=\"what-i-learned\">What I learned<\/h2>\n<p>I didn\u2019t expect AI to get better just because I onboarded a human teammate, but once I wrote down our rules and automated our guardrails, AI started performing at a higher level, too.<\/p>\n<p>I hadn\u2019t been treating AI like a collaborative partner who needs the same foundational knowledge I\u2019d give a person. I\u2019d just kind of hoped AI would figure out my preferences and get it together eventually. I now give it the explicit context so AI can make smart, independent decisions.<\/p>\n<p>I built the context once. Now future AI tools and future colleagues can hit the ground running.<\/p>\n"},{"title":"Digging into \"Your Brain on ChatGPT\"","pubDate":"Wed, 10 Sep 2025 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/digging-into-your-brain-on-chatgpt\/","guid":"https:\/\/sdeaton.com\/blog\/digging-into-your-brain-on-chatgpt\/","description":"<p>You\u2019ve probably seen the headlines:<\/p>\n<p>\u201cAI shortcuts are already making kids lazy\u201c\u201d\u201cChatGPT Use Linked to Cognitive Decline\u201d\u201cUsing ChatGPT for work? It might make you stupid.\u201d<\/p>\n<p>These stories reference <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/arxiv.org\/abs\/2506.08872\">a recent MIT study<\/a> titled <em>\u201c<em>Your Brain on ChatGPT<\/em>.\u201d<\/em> And these headlines, plus the way the study was framed, are the kind of sensationalized science communication that makes thoughtful conversations about AI and education harder to have.<\/p>\n<h2 id=\"yes-i-worry-about-ai-and-education\">Yes, I worry about AI and education<\/h2>\n<p>I\u2019m big on AI. I work in enterprise AI and see how these tools can free people for more joyful, creative, high-impact work. I also have a young child, and I worry about what her education will look like when she and her peers have access to AI from day one.<\/p>\n<p>But this study doesn\u2019t address those concerns. Instead, it amplifies fears with dramatic interpretations that go far beyond what the research actually shows.<\/p>\n<h2 id=\"this-didn-t-study-education\">This didn\u2019t study education<\/h2>\n<p>Here\u2019s what the MIT researchers did:<\/p>\n<ul>\n<li>\n<p>Recruited 54 college students.<\/p>\n<\/li>\n<li>\n<p>Paid them $100\u2013150.<\/p>\n<\/li>\n<li>\n<p>Asked them to respond to an essay prompt for 20 minutes with EEG caps measuring brain activity.<\/p>\n<\/li>\n<li>\n<p>To get a \u201cbaseline\u201d of thinking activity, participants spent two minutes doing rapid mental math, like multiplying 128 \u00d7 56 or adding 5,689 + 7,854 in their heads.<\/p>\n<\/li>\n<li>\n<p>Then they wrote short essays responding to SAT-style prompts like:<\/p>\n<ul>\n<li>\n<p>\u201cDoes true loyalty require unconditional support?\u201d<\/p>\n<\/li>\n<li>\n<p>\u201cCan people have too much enthusiasm?\u201d<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>Some used ChatGPT, some used Google, some wrote without digital tools. The study took place over four months, with each participant completing three or four sessions over that time period.<\/p>\n<p>The paper claims, in the first paragraph of the abstract, to study \u201cthe cognitive cost of using an LLM in the educational context of writing an essay.\u201d But what\u2019s <em>educational<\/em> about this setup?<\/p>\n<ul>\n<li>\n<p>No instruction.<\/p>\n<\/li>\n<li>\n<p>No feedback.<\/p>\n<\/li>\n<li>\n<p>No grades.<\/p>\n<\/li>\n<li>\n<p>No goal.<\/p>\n<\/li>\n<li>\n<p>Generic SAT prompts with no iteration or follow-through.<\/p>\n<\/li>\n<li>\n<p>And you can\u2019t move your head because it will mess up the signals.<\/p>\n<\/li>\n<\/ul>\n<p>When the session ended, students didn\u2019t walk away with insight or skill. They walked to the sink with shampoo and a towel to wash EEG gel out of their hair.<\/p>\n<p>In classrooms, students have grades, improvement, feedback**.** Stakes shape engagement, as does actual interest in the topic. None of that was present here.<\/p>\n<p>This wasn\u2019t education. It showed what happens when you introduce a tool to help with an otherwise tedious, one-off task.<\/p>\n<h2 id=\"study-findings\">Study findings<\/h2>\n<p>Here\u2019s my summary of the key findings. And yes, if you\u2019re coming from the belief that AI <em>causes a cognitive deficit<\/em>, these findings could look alarming:<\/p>\n<ul>\n<li>\n<p>Students using AI showed different patterns of brain connectivity compared to those writing without assistance.<\/p>\n<\/li>\n<li>\n<p>Their brains showed less coordinated activity across multiple regions.<\/p>\n<\/li>\n<li>\n<p>They had trouble recalling quotes from essays they\u2019d written minutes earlier.<\/p>\n<\/li>\n<li>\n<p>Many reported feeling less ownership over their work.<\/p>\n<\/li>\n<li>\n<p>By the third session, some students said ChatGPT felt \u201cnot reliable\u201d for research or \u201cnot worth it\u201d for certain assignments.<\/p>\n<\/li>\n<li>\n<p>In the fourth session, sequence mattered:<\/p>\n<ul>\n<li>\n<p>Students who started without AI and later used it showed increased brain activity.<\/p>\n<\/li>\n<li>\n<p>Students who started with AI and then wrote alone showed continued reduced engagement.<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>But to me, the reason we can\u2019t extrapolate further is because this study felt much more like a tedious activity than an educational opportunity.<\/p>\n<p>It was more like giving students a worksheet of 20 long-division problems that they weren\u2019t graded on and wouldn\u2019t see again. If some used a calculator and some didn\u2019t, would you expect the calculator group to remember the exact numbers they punched in? Probably not \u2014 they\u2019d just do it and move on.<\/p>\n<p>Additionally, two teachers evaluated the essays and rated the ChatGPT-assisted essays lower. But as far I can tell, participants never saw those grades and never got feedback. And each session was a new prompt. Without iteration or direction, there was no chance or incentive for any real education.<\/p>\n<p>Researchers measured brain activity, then declared what it meant. Reduced connectivity = cognitive decline? Maybe. But it could also reflect efficiency, new strategies, or adaptation to a new tool.<\/p>\n<h2 id=\"moving-forward\">Moving forward<\/h2>\n<p>I don\u2019t take issue with doing exploratory lab studies like this or the methods used. My issues are the mismatch between the design and the claims about \u201ceducation,\u201d and the assumptions the researchers seemed to make.<\/p>\n<p>The abstract declares the study\u2019s goal is to \u201cdemonstrate the pressing matter of a likely decrease in learning skills.\u201d That doesn\u2019t sound neutral or unbiased.<\/p>\n<p>Further research should add:<\/p>\n<ul>\n<li>\n<p>Feedback, revision, and iteration<\/p>\n<\/li>\n<li>\n<p>Real educational stakes<\/p>\n<\/li>\n<li>\n<p>Projects students care about<\/p>\n<\/li>\n<li>\n<p>A neutral starting point, not the assumption that AI is a \u201ccognitive cost\u201d<\/p>\n<\/li>\n<\/ul>\n<p>The MIT study really found that <strong>brains adapt when using new tools.<\/strong> That\u2019s not surprising. Whether that adaptation helps or harms learning depends on design and context.<\/p>\n<p>Sensational headlines about \u201cAI rotting our brains\u201d don\u2019t help us figure that out.<\/p>\n<hr \/>\n<p>I was ranting about this study to my husband and asked if he was equally upset. He replied: <em>\u201cNo more than I am with all the AI companies promising AGI and superalignment by next quarter.\u201d<\/em><\/p>\n<p>Fair point. The hype cycles run in both directions, and both deserve skepticism.<\/p>\n"},{"title":"Learn MCP with me, part 2: working with MCP servers and clients","pubDate":"Wed, 10 Sep 2025 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/learn-mcp-part-2\/","guid":"https:\/\/sdeaton.com\/blog\/learn-mcp-part-2\/","description":"<p>In <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/substack.com\/home\/post\/p-173130074\">Part 1<\/a>, I looked into <em>why<\/em> MCP exists: it\u2019s guardrails for LLMs to use APIs without screwing up. Next I wanted to dig into how to actually work with MCP servers.<\/p>\n<p>What got me started on this journey was discovering that <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/mintlify.com\/blog\/generate-mcp-servers-for-your-docs\">Mintlify offers MCP servers<\/a> for all their hosted documentation sites. I saw this but didn\u2019t really understand what it meant or how to use it. Can I curl an MCP server? How do I debug when things go wrong? What does the development workflow actually look like?<\/p>\n<p>I decided to explore using some of these Mintlify-hosted MCP servers to build a documentation research agent that will provide answers across several different tools I use.<\/p>\n<p>I went into this just trying to learn the technical details, but I walked away genuinely excited. Even though I knew theoretically what MCP should do, watching an agent I created intelligently decide \u201cI need to search Mintlify docs AND Cursor docs for this comparison question\u201d and then synthesize the results was pretty magical.<\/p>\n<h2 id=\"quick-review-clients-vs-servers\">Quick review: clients vs. servers<\/h2>\n<p>To quickly recap:<\/p>\n<p><strong>MCP server<\/strong>: exposes tools\/resources that an AI can use. In this case, I\u2019m using remote-hosted MCP servers that Mintlify sets up for docs sites. <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.pulsemcp.com\/servers\">There\u2019s a large list of other MCP servers here<\/a>.<\/p>\n<p><strong>MCP client<\/strong>: the AI application that connects to and uses MCP servers (like Claude Desktop, Cursor, Windsurf, or one you build yourself).<\/p>\n<p>You configure the client to know about servers, then ask the AI to do things, and it figures out which tools to use.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/learn-mcp-part-2\/addbe398-b03f-4275-88ae-cbf2cefb2d09.png\" alt=\"Diagram showing MCP architecture: clients like Claude Desktop and Cursor connecting to MCP servers that expose tools\" \/><\/p>\n<h2 id=\"1-just-get-it-working-plug-into-an-ai-app\">1. Just get it working: plug into an AI app<\/h2>\n<p>This is probably the best way to get moving. Here, I\u2019ll use Cursor as an example, but the same approach works with Claude Desktop or other AI clients. I wanted to use remote servers to test this because I didn\u2019t want to focus on setting up local servers.<\/p>\n<p><strong>Setup for Cursor:<\/strong><\/p>\n<ol>\n<li>\n<p>Create an <code>mcp.json<\/code> file in Cursor. <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/docs.cursor.com\/en\/context\/mcp#configuration-locations\">This can be project-specific or global<\/a>. Cursor also has some <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/docs.cursor.com\/en\/context\/mcp\">one-click integrations listed<\/a> so you don\u2019t even need to manually update a file.<\/p>\n<\/li>\n<li>\n<p>Add the remote URL for any MCP servers<\/p>\n<\/li>\n<li>\n<p>Ask the AI to do things<\/p>\n<\/li>\n<\/ol>\n<p>Here\u2019s my <code>~\/.cursor\/mcp.json<\/code>, which connects to three different MCP servers that contain docs for the different tools I use (WRITER, Mintlify, and Cursor):<\/p>\n<pre data-lang=\"json\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-json \"><code class=\"language-json\" data-lang=\"json\"><span>{\n<\/span><span>  &quot;<\/span><span style=\"color:#a3be8c;\">mcpServers<\/span><span>&quot;: {\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">Writer Docs<\/span><span>&quot;: {\n<\/span><span>      &quot;<\/span><span style=\"color:#a3be8c;\">url<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">https:\/\/dev.writer.com\/mcp<\/span><span>&quot;\n<\/span><span>    },\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">Mintlify<\/span><span>&quot;: {\n<\/span><span>      &quot;<\/span><span style=\"color:#a3be8c;\">url<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">https:\/\/www.mintlify.com\/docs\/mcp<\/span><span>&quot;\n<\/span><span>    },\n<\/span><span>    &quot;<\/span><span style=\"color:#a3be8c;\">Cursor<\/span><span>&quot;: {\n<\/span><span>      &quot;<\/span><span style=\"color:#a3be8c;\">url<\/span><span>&quot;: &quot;<\/span><span style=\"color:#a3be8c;\">https:\/\/docs.cursor.com\/mcp<\/span><span>&quot;\n<\/span><span>    }\n<\/span><span>  }\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>You can verify these URLs work by visiting them in your browser. They should return JSON describing the server capabilities.<\/p>\n<h3 id=\"try-it-out\">Try it out<\/h3>\n<p>I was initially confused about what to do next. How do you test it? How do you know it\u2019s working? It\u2019s actually a lot simpler than I was making it in my head.<\/p>\n<p>Once you add your servers to Cursor, it\u2019s just a regular chat. MCP servers are essentially just tools your AI can use. It\u2019s like any other function you might give an AI, like one that calculates averages or fetches weather data.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/learn-mcp-part-2\/d06a1c34-219e-48c9-89a3-b4f7dc54cec8.png\" alt=\"Screenshot of Cursor chat showing MCP tools being used to search documentation\" \/><\/p>\n<p><strong>The magic happens in conversation:<\/strong><\/p>\n<ul>\n<li>\n<p>Ask: \u201cWhat tools do you have available?\u201d<\/p>\n<\/li>\n<li>\n<p><em>The AI shows you what it can access across all your tool documentation<\/em><\/p>\n<\/li>\n<li>\n<p>Ask: \u201cHow do I implement authentication across Writer, Mintlify, and Cursor?\u201d<\/p>\n<\/li>\n<li>\n<p><em>The AI searches each documentation server and synthesizes the results<\/em><\/p>\n<\/li>\n<li>\n<p>Ask: \u201cWhat are the API rate limits for each of these tools?\u201d<\/p>\n<\/li>\n<li>\n<p><em>The AI compares approaches across all three platforms<\/em><\/p>\n<\/li>\n<\/ul>\n<p>At this point, you might be satisfied. It works, it\u2019s useful, and you don\u2019t need to understand the details. But I was still trying to figure out what was actually happening under the hood.<\/p>\n<h2 id=\"2-what-s-actually-going-on-taking-a-look-with-curl\">2. What\u2019s actually going on? Taking a look with curl<\/h2>\n<p>My first instinct was: \u201cCan I just curl this thing?\u201d I was still confused about transport and protocols and what\u2019s actually happening for the request\/response pattern. So tried it out.<\/p>\n<h3 id=\"server-inspection\">Server inspection<\/h3>\n<p>If you visit <code>https:\/\/www.mintlify.com\/docs\/mcp<\/code> in a browser, you can see the server capabilities. It shows you the available tools, their descriptions, and parameter schemas, similar to an OpenAPI spec.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/learn-mcp-part-2\/3271f791-36f1-4bba-ae88-08735eeccd8d.png\" alt=\"Browser showing Mintlify MCP server capabilities as JSON, including available tools and their schemas\" \/><\/p>\n<p>But can I actually call the tools with familiar HTTP tools?<\/p>\n<h3 id=\"calling-tools-with-curl\">Calling tools with curl<\/h3>\n<p>This is where I learned MCP isn\u2019t just REST with a different name. It uses <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/www.jsonrpc.org\/specification\">JSON-RPC 2.0<\/a>, which requires a more structured approach.<\/p>\n<p>After some trial and error and checking out the<a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/github.com\/modelcontextprotocol\/modelcontextprotocol\/blob\/main\/schema\/2025-06-18\/schema.ts\"> MCP specification<\/a>, here\u2019s what works:<\/p>\n<p>First, you need to initialize a connection and send an initialize request using <code>jsonrpc 2.0<\/code>. This <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/github.com\/modelcontextprotocol\/modelcontextprotocol\/blob\/main\/schema\/2025-06-18\/schema.ts#L172\">initialize method is from the MCP<\/a> spec, so it\u2019s standard for all MCP requests.<\/p>\n<pre data-lang=\"bash\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-bash \"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"color:#65737e;\"># 1. Initialize the connection\n<\/span><span style=\"color:#bf616a;\">curl -X<\/span><span> POST &quot;<\/span><span style=\"color:#a3be8c;\">https:\/\/www.mintlify.com\/docs\/mcp<\/span><span>&quot; \\\n<\/span><span style=\"color:#bf616a;\">  -H <\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">Content-Type: application\/json<\/span><span>&quot; \\\n<\/span><span style=\"color:#bf616a;\">  -H <\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">Accept: application\/json, text\/event-stream<\/span><span>&quot; \\\n<\/span><span style=\"color:#bf616a;\">  -d <\/span><span>&#39;<\/span><span style=\"color:#a3be8c;\">{\n<\/span><span style=\"color:#a3be8c;\">    &quot;jsonrpc&quot;: &quot;2.0&quot;,\n<\/span><span style=\"color:#a3be8c;\">    &quot;id&quot;: 1,\n<\/span><span style=\"color:#a3be8c;\">    &quot;method&quot;: &quot;initialize&quot;,\n<\/span><span style=\"color:#a3be8c;\">    &quot;params&quot;: {\n<\/span><span style=\"color:#a3be8c;\">      &quot;protocolVersion&quot;: &quot;1.0&quot;,\n<\/span><span style=\"color:#a3be8c;\">      &quot;capabilities&quot;: {},\n<\/span><span style=\"color:#a3be8c;\">      &quot;clientInfo&quot;: {&quot;name&quot;: &quot;test-client&quot;, &quot;version&quot;: &quot;0.1&quot;}\n<\/span><span style=\"color:#a3be8c;\">    }\n<\/span><span style=\"color:#a3be8c;\">  }<\/span><span>&#39;\n<\/span><\/code><\/pre>\n<p>Success! It returns <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Server-sent_events\/Using_server-sent_events\">Server-Sent Events<\/a> with the actual available tools:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>event: message\n<\/span><span>data: {&quot;result&quot;:{&quot;protocolVersion&quot;:&quot;2025-06-18&quot;,&quot;capabilities&quot;:{&quot;tools&quot;:{&quot;SearchMintlify&quot;:{&quot;name&quot;:&quot;SearchMintlify&quot;,&quot;description&quot;:&quot;Search across the Mintlify knowledge base to find relevant information...&quot;,&quot;method&quot;:&quot;&quot;,&quot;pathTemplate&quot;:&quot;&quot;,&quot;parameters&quot;:[],&quot;inputSchema&quot;:{&quot;type&quot;:&quot;object&quot;,&quot;properties&quot;:{&quot;query&quot;:{&quot;type&quot;:&quot;string&quot;,&quot;description&quot;:&quot;A query to search the content with.&quot;}},&quot;required&quot;:[&quot;query&quot;]},&quot;executionParameters&quot;:[],&quot;securityRequirements&quot;:[],&quot;operationId&quot;:&quot;MintlifyDefaultSearch&quot;},&quot;listChanged&quot;:true}},&quot;serverInfo&quot;:{&quot;name&quot;:&quot;Mintlify&quot;,&quot;version&quot;:&quot;1.0.0&quot;}},&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1}\n<\/span><\/code><\/pre>\n<p>Then, you can call a tool. You can see in the response above that there\u2019s a tool called <code>SearchMintlify<\/code> that takes a required query argument.<\/p>\n<p>The <code>tools\/call<\/code> method is <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/github.com\/modelcontextprotocol\/modelcontextprotocol\/blob\/main\/schema\/2025-06-18\/schema.ts#L172\">specifically defined within the MCP spec as well<\/a>. The LLM can see from this tools list what it needs to plug in to provide when it calls the tool.<\/p>\n<pre data-lang=\"bash\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-bash \"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"color:#65737e;\"># 2. Call a tool (note the exact tool name from the capabilities response)\n<\/span><span style=\"color:#bf616a;\">curl -X<\/span><span> POST &quot;<\/span><span style=\"color:#a3be8c;\">https:\/\/www.mintlify.com\/docs\/mcp<\/span><span>&quot; \\\n<\/span><span style=\"color:#bf616a;\">  -H <\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">Content-Type: application\/json<\/span><span>&quot; \\\n<\/span><span style=\"color:#bf616a;\">  -H <\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">Accept: application\/json, text\/event-stream<\/span><span>&quot; \\\n<\/span><span style=\"color:#bf616a;\">  -d <\/span><span>&#39;<\/span><span style=\"color:#a3be8c;\">{\n<\/span><span style=\"color:#a3be8c;\">    &quot;jsonrpc&quot;: &quot;2.0&quot;,\n<\/span><span style=\"color:#a3be8c;\">    &quot;id&quot;: 2,\n<\/span><span style=\"color:#a3be8c;\">    &quot;method&quot;: &quot;tools\/call&quot;,\n<\/span><span style=\"color:#a3be8c;\">    &quot;params&quot;: {\n<\/span><span style=\"color:#a3be8c;\">      &quot;name&quot;: &quot;SearchMintlify&quot;,\n<\/span><span style=\"color:#a3be8c;\">      &quot;arguments&quot;: {&quot;query&quot;: &quot;API authentication&quot;}\n<\/span><span style=\"color:#a3be8c;\">    }\n<\/span><span style=\"color:#a3be8c;\">  }<\/span><span>&#39;\n<\/span><\/code><\/pre>\n<p>And when it works, you get back:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>event: message\n<\/span><span>data: {&quot;result&quot;:{&quot;content&quot;:[{&quot;type&quot;:&quot;text&quot;,&quot;text&quot;:&quot;Title: Authentication\\nLink: https:\/\/www.mintlify.com\/docs\/api-playground\/mdx\/authentication\\nContent: You can set authentication parameters to let users use their real API keys...&quot;}]}}\n<\/span><\/code><\/pre>\n<h3 id=\"here-s-what-the-full-flow-looks-like\">Here\u2019s what the full flow looks like:<\/h3>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/learn-mcp-part-2\/06b50051-6c2a-4881-bb5c-7a23f820e32e.png\" alt=\"Sequence diagram showing the MCP request flow: curl sends initialize request, server returns capabilities, curl calls tool, server returns results\" \/><\/p>\n<p>This was really helpful for me to understand what was part of the MCP spec and what\u2019s happening when an agent uses an MCP tool. I wouldn\u2019t use curl when implementing an MCP client, but it\u2019s useful if you want to:<\/p>\n<ul>\n<li>\n<p>Verify server availability and capabilities<\/p>\n<\/li>\n<li>\n<p>Test the initialization handshake<\/p>\n<\/li>\n<li>\n<p>Debug tool calls and see exact response formats<\/p>\n<\/li>\n<li>\n<p>Understand authentication requirements<\/p>\n<\/li>\n<\/ul>\n<p>To actually build a client, you\u2019d want a client library.<\/p>\n<h2 id=\"3-building-it-myself-an-intelligent-docs-support-agent\">3. Building it myself: an intelligent docs support agent<\/h2>\n<p>Now that I understand the protocol better, I wanted to try to build my own agent that could work with these MCP servers.<\/p>\n<h3 id=\"using-amazon-strands-with-built-in-mcp-support\">Using Amazon Strands with built-in MCP support<\/h3>\n<p>I chose to use Amazon <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/strandsagents.com\/latest\/\">Strands<\/a> for this part, because I like the interface, it has integrations with <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/strandsagents.com\/latest\/documentation\/docs\/user-guide\/concepts\/model-providers\/writer\">WRITER<\/a> and lots of other model providers, and it happens to have MCP support built in.<\/p>\n<p>With Strands, you can swap out WRITER models for other model providers if you use something else, like Anthropic or OpenAI.<\/p>\n<p>To follow along, first you\u2019d install Strands with Writer (or your model provider, you can <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/strandsagents.com\/latest\/documentation\/docs\/user-guide\/concepts\/model-providers\/amazon-bedrock\/\">find the info in the Strands docs<\/a>) and tool support:<\/p>\n<pre data-lang=\"bash\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-bash \"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"color:#bf616a;\">pip<\/span><span> install &#39;<\/span><span style=\"color:#a3be8c;\">strands-agents[writer]<\/span><span>&#39; strands-agents-tools\n<\/span><\/code><\/pre>\n<p>You\u2019d also need an API key for the model provider you\u2019re using.<\/p>\n<p>Below I\u2019ll walk through the agent I built to answer questions about the three different tools I\u2019m using by pulling from their docs MCP servers. It creates connections to multiple MCP servers, combines all their tools, and lets the AI agent decide which ones to use for each question.<a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/gist.github.com\/sarahcstringer\/9d07e30fb3a4a6e2b5474266852284a6\"> See the full code in this Gist.<\/a><\/p>\n<p>First, import the dependencies.<\/p>\n<pre data-lang=\"python\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-python \"><code class=\"language-python\" data-lang=\"python\"><span style=\"color:#b48ead;\">import <\/span><span>os\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">from <\/span><span>mcp.client.streamable_http <\/span><span style=\"color:#b48ead;\">import <\/span><span>streamablehttp_client\n<\/span><span style=\"color:#b48ead;\">from <\/span><span>strands <\/span><span style=\"color:#b48ead;\">import <\/span><span>Agent\n<\/span><span style=\"color:#b48ead;\">from <\/span><span>strands.models.writer <\/span><span style=\"color:#b48ead;\">import <\/span><span>WriterModel\n<\/span><span style=\"color:#b48ead;\">from <\/span><span>strands.tools.mcp <\/span><span style=\"color:#b48ead;\">import <\/span><span>MCPClient\n<\/span><\/code><\/pre>\n<p>The next section configures the Strands Agent. It creates streamable http connections to the three servers, configures the model to use <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/writer.com\/blog\/long-context-palmyra-x5\/\">Palmyra X5<\/a>, and stores the system prompt for the agent.<\/p>\n<pre data-lang=\"python\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-python \"><code class=\"language-python\" data-lang=\"python\"><span style=\"color:#b48ead;\">class <\/span><span style=\"color:#ebcb8b;\">StrandsResearchAgent<\/span><span style=\"color:#eff1f5;\">:\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">def <\/span><span style=\"color:#96b5b4;\">__init__<\/span><span>(<\/span><span style=\"color:#bf616a;\">self<\/span><span>):\n<\/span><span>        <\/span><span style=\"color:#65737e;\">&quot;&quot;&quot;Initialize MCP clients for documentation servers&quot;&quot;&quot;\n<\/span><span>        <\/span><span style=\"color:#65737e;\"># Create MCP clients for each documentation server\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">self<\/span><span>.writer_client = <\/span><span style=\"color:#bf616a;\">MCPClient<\/span><span>(\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">lambda<\/span><span>: <\/span><span style=\"color:#bf616a;\">streamablehttp_client<\/span><span>(&quot;<\/span><span style=\"color:#a3be8c;\">https:\/\/dev.writer.com\/mcp<\/span><span>&quot;)\n<\/span><span>        )\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">self<\/span><span>.mintlify_client = <\/span><span style=\"color:#bf616a;\">MCPClient<\/span><span>(\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">lambda<\/span><span>: <\/span><span style=\"color:#bf616a;\">streamablehttp_client<\/span><span>(&quot;<\/span><span style=\"color:#a3be8c;\">https:\/\/www.mintlify.com\/docs\/mcp<\/span><span>&quot;)\n<\/span><span>        )\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">self<\/span><span>.cursor_client = <\/span><span style=\"color:#bf616a;\">MCPClient<\/span><span>(\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">lambda<\/span><span>: <\/span><span style=\"color:#bf616a;\">streamablehttp_client<\/span><span>(&quot;<\/span><span style=\"color:#a3be8c;\">https:\/\/docs.cursor.com\/mcp<\/span><span>&quot;)\n<\/span><span>        )\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#65737e;\"># Set up Palmyra X5 model\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">self<\/span><span>.model = <\/span><span style=\"color:#bf616a;\">WriterModel<\/span><span>(\n<\/span><span>            <\/span><span style=\"color:#bf616a;\">client_args<\/span><span>={&quot;<\/span><span style=\"color:#a3be8c;\">api_key<\/span><span>&quot;: os.<\/span><span style=\"color:#bf616a;\">getenv<\/span><span>(&quot;<\/span><span style=\"color:#a3be8c;\">WRITER_API_KEY<\/span><span>&quot;)},\n<\/span><span>            <\/span><span style=\"color:#bf616a;\">model_id<\/span><span>=&quot;<\/span><span style=\"color:#a3be8c;\">palmyra-x5<\/span><span>&quot;,\n<\/span><span>            <\/span><span style=\"color:#bf616a;\">temperature<\/span><span>=<\/span><span style=\"color:#d08770;\">0.3<\/span><span>,  <\/span><span style=\"color:#65737e;\"># Lower temperature for more focused responses\n<\/span><span>        )\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#65737e;\"># System prompt\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">self<\/span><span>.system_prompt = &quot;&quot;&quot;\n<\/span><span style=\"color:#a3be8c;\">        You are a research assistant with access to documentation from Writer, Mintlify, and Cursor.\n<\/span><span style=\"color:#a3be8c;\">        When answering questions:\n<\/span><span style=\"color:#a3be8c;\">        1. Determine which documentation sources are most relevant to the question\n<\/span><span style=\"color:#a3be8c;\">        2. Search those sources strategically (don&#39;t search everything unless necessary)\n<\/span><span style=\"color:#a3be8c;\">        3. Synthesize information from multiple sources when helpful\n<\/span><span style=\"color:#a3be8c;\">        4. Provide practical, actionable answers with examples when possible\n<\/span><span style=\"color:#a3be8c;\">        Be efficient with tool usage - only search the documentation that&#39;s relevant to the specific question.\n<\/span><span style=\"color:#a3be8c;\">        <\/span><span>&quot;&quot;&quot;\n<\/span><\/code><\/pre>\n<p>The script then starts the chat loop. It opens the connections to the MCP servers so they\u2019re available during the context of the agent chat. Then it provides the list of all the available tools from the MCP clients to the agent, along with the model and system prompt defined earlier.<\/p>\n<pre data-lang=\"python\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-python \"><code class=\"language-python\" data-lang=\"python\"><span style=\"color:#b48ead;\">def <\/span><span style=\"color:#8fa1b3;\">chat_loop<\/span><span>(<\/span><span style=\"color:#bf616a;\">self<\/span><span>):\n<\/span><span>    <\/span><span style=\"color:#65737e;\">&quot;&quot;&quot;Interactive chat loop for research questions&quot;&quot;&quot;\n<\/span><span>    <\/span><span style=\"color:#96b5b4;\">print<\/span><span>(&quot;<\/span><span style=\"color:#96b5b4;\">\\n<\/span><span style=\"color:#a3be8c;\">\ud83e\udd16 Strands Research Agent (Palmyra X5) ready!<\/span><span>&quot;)\n<\/span><span>    <\/span><span style=\"color:#96b5b4;\">print<\/span><span>(&quot;<\/span><span style=\"color:#a3be8c;\">Ask me about Writer, Mintlify, or Cursor documentation.<\/span><span>&quot;)\n<\/span><span>    <\/span><span style=\"color:#96b5b4;\">print<\/span><span>(&quot;<\/span><span style=\"color:#a3be8c;\">Type &#39;quit&#39; to exit.<\/span><span style=\"color:#96b5b4;\">\\n<\/span><span>&quot;)\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#65737e;\"># Keep MCP clients connected for the entire chat session to maintain conversation history\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">with <\/span><span style=\"color:#bf616a;\">self<\/span><span>.mintlify_client, <\/span><span style=\"color:#bf616a;\">self<\/span><span>.cursor_client, <\/span><span style=\"color:#bf616a;\">self<\/span><span>.writer_client:\n<\/span><span>        <\/span><span style=\"color:#65737e;\"># Combine tools from all servers\n<\/span><span>        all_tools = (\n<\/span><span>            <\/span><span style=\"color:#bf616a;\">self<\/span><span>.writer_client.<\/span><span style=\"color:#bf616a;\">list_tools_sync<\/span><span>()\n<\/span><span>            + <\/span><span style=\"color:#bf616a;\">self<\/span><span>.mintlify_client.<\/span><span style=\"color:#bf616a;\">list_tools_sync<\/span><span>()\n<\/span><span>            + <\/span><span style=\"color:#bf616a;\">self<\/span><span>.cursor_client.<\/span><span style=\"color:#bf616a;\">list_tools_sync<\/span><span>()\n<\/span><span>        )\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#65737e;\"># Create a Strands agent with Palmyra X5 and all MCP tools\n<\/span><span>        <\/span><span style=\"color:#65737e;\"># This agent will maintain conversation history across all questions\n<\/span><span>        agent = <\/span><span style=\"color:#bf616a;\">Agent<\/span><span>(\n<\/span><span>            <\/span><span style=\"color:#bf616a;\">model<\/span><span>=<\/span><span style=\"color:#bf616a;\">self<\/span><span>.model,  <\/span><span style=\"color:#65737e;\"># Palmyra X5 model defined in __init__\n<\/span><span>            <\/span><span style=\"color:#bf616a;\">tools<\/span><span>=all_tools,  <\/span><span style=\"color:#65737e;\"># Open MCP tool connections defined above\n<\/span><span>            <\/span><span style=\"color:#bf616a;\">system_prompt<\/span><span>=<\/span><span style=\"color:#bf616a;\">self<\/span><span>.system_prompt,  <\/span><span style=\"color:#65737e;\"># System prompt defined in __init__\n<\/span><span>        )\n<\/span><\/code><\/pre>\n<p>This is the part that really got me excited. I just pass the connections to the Strands agent as tools and it figures out the rest. I don\u2019t have to write any logic for \u201cwhen should you search Writer docs vs Cursor docs\u201d - the AI agent sees the available tools and their descriptions and makes intelligent decisions about which ones to use.<\/p>\n<p>The last part is running the chat loop, where the script accepts input from the user, passes it back to the model, and streams the final results back to stdout.<\/p>\n<pre data-lang=\"python\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-python \"><code class=\"language-python\" data-lang=\"python\"><span style=\"color:#65737e;\"># Use the same agent for all questions to maintain conversation history\n<\/span><span style=\"color:#b48ead;\">while <\/span><span style=\"color:#d08770;\">True<\/span><span>:\n<\/span><span>    question = <\/span><span style=\"color:#96b5b4;\">input<\/span><span>(&quot;<\/span><span style=\"color:#a3be8c;\">You: <\/span><span>&quot;).<\/span><span style=\"color:#bf616a;\">strip<\/span><span>()\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">if <\/span><span>question.<\/span><span style=\"color:#bf616a;\">lower<\/span><span>() in [&quot;<\/span><span style=\"color:#a3be8c;\">quit<\/span><span>&quot;, &quot;<\/span><span style=\"color:#a3be8c;\">exit<\/span><span>&quot;]:\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">break\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">if <\/span><span>question:\n<\/span><span>        <\/span><span style=\"color:#96b5b4;\">print<\/span><span>(&quot;<\/span><span style=\"color:#a3be8c;\">\ud83d\udd0d Researching...<\/span><span>&quot;)\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">try<\/span><span>:\n<\/span><span>            <\/span><span style=\"color:#65737e;\"># By default, the agent streams the response to stdout\n<\/span><span>            <\/span><span style=\"color:#65737e;\"># More info: https:\/\/strandsagents.com\/latest\/documentation\/docs\/user-guide\/concepts\/streaming\/callback-handlers\/\n<\/span><span>            <\/span><span style=\"color:#bf616a;\">agent<\/span><span>(question)\n<\/span><span>            <\/span><span style=\"color:#96b5b4;\">print<\/span><span>(&quot;<\/span><span style=\"color:#96b5b4;\">\\n<\/span><span>&quot;)\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">except <\/span><span>Exception <\/span><span style=\"color:#b48ead;\">as <\/span><span>e:\n<\/span><span>            <\/span><span style=\"color:#96b5b4;\">print<\/span><span>(<\/span><span style=\"color:#b48ead;\">f<\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">\u274c Error: <\/span><span>{e}<\/span><span style=\"color:#96b5b4;\">\\n<\/span><span>&quot;)\n<\/span><span>\n<\/span><span style=\"color:#65737e;\"># Usage examples\n<\/span><span style=\"color:#b48ead;\">def <\/span><span style=\"color:#8fa1b3;\">main<\/span><span>():\n<\/span><span>    agent = <\/span><span style=\"color:#bf616a;\">StrandsResearchAgent<\/span><span>()\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#65737e;\"># Start interactive chat\n<\/span><span>    agent.<\/span><span style=\"color:#bf616a;\">chat_loop<\/span><span>()\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">if <\/span><span>__name__ == &quot;<\/span><span style=\"color:#a3be8c;\">__main__<\/span><span>&quot;:\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">main<\/span><span>()\n<\/span><\/code><\/pre>\n<p><a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/gist.github.com\/sarahcstringer\/9d07e30fb3a4a6e2b5474266852284a6\">See the full code here.<\/a><\/p>\n<p>Here\u2019s an example of using it:<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/learn-mcp-part-2\/91ac2069-d302-4a0e-9bcb-deba7f96dbd1.png\" alt=\"Terminal showing the Strands research agent starting up and connecting to multiple MCP documentation servers\" \/><\/p>\n<ol>\n<li>\n<p><strong>I ask<\/strong>: \u201cWhat are the Mintlify and Cursor API rate limits?\u201d<\/p>\n<\/li>\n<li>\n<p><strong>Palmyra X5 analyzes<\/strong>: Understands this is about API development and rate limiting<\/p>\n<\/li>\n<li>\n<p><strong>Smart tool selection<\/strong>: Decides to search Cursor and Mintlify docs for API rate limiting info<\/p>\n<\/li>\n<li>\n<p><strong>Targeted searches<\/strong>: Makes specific queries like \u201cAPI rate limiting\u201d<\/p>\n<\/li>\n<li>\n<p><strong>Intelligent synthesis<\/strong>: Agent combines results and provides practical guidance<\/p>\n<\/li>\n<\/ol>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/learn-mcp-part-2\/71932218-124b-4e1d-a889-d8e52a5f3077.png\" alt=\"Agent response synthesizing API rate limit information from both Mintlify and Cursor documentation\" \/><\/p>\n<p>What\u2019s so cool is that I never told it explicitly \u201csearch both Mintlify and Cursor for this question\u201c or told it anything more than where to find the servers. It figured out from the question that it needed information from both sources, searched them independently, and synthesized the results.<\/p>\n<h2 id=\"the-bottom-line\">The bottom line<\/h2>\n<p>MCP is still early, and the debugging experience reflects that. You\u2019ll hit rough edges that don\u2019t exist with traditional APIs. But when it works, it\u2019s genuinely useful, especially for adaptive AI workflows where you want the LLM to discover and orchestrate tools on its own. It\u2019s funny how quickly I want from \u201cwhy do I even want MCP?\u201d to \u201cwhat? You don\u2019t have an MCP server for that??\u201d.<\/p>\n<hr \/>\n<h2 id=\"side-notes-auth-and-debugging\">Side notes: auth and debugging<\/h2>\n<h3 id=\"auth\">Auth<\/h3>\n<p>One thing you might notice is that some MCP tools require authentication to external APIs. With MCP, authentication is just another fill-in-the blank; the MCP server tells the client which credentials it needs, and the client provides them for the server to then handle the actual auth.<\/p>\n<p>Interactive clients like Cursor or Claude can prompt you for API keys or other credentials when needed. If you\u2019re building your own programmatic agent that needs credentials, you\u2019d need to provide them upfront. You can handle this by passing authentication headers when creating the MCP client:<\/p>\n<pre data-lang=\"python\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-python \"><code class=\"language-python\" data-lang=\"python\"><span style=\"color:#bf616a;\">self<\/span><span>.writer_client = <\/span><span style=\"color:#bf616a;\">MCPClient<\/span><span>(\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">lambda<\/span><span>: <\/span><span style=\"color:#bf616a;\">streamablehttp_client<\/span><span>(&quot;<\/span><span style=\"color:#a3be8c;\">https:\/\/dev.writer.com\/mcp<\/span><span>&quot;,\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">headers<\/span><span>={&quot;<\/span><span style=\"color:#a3be8c;\">Authorization<\/span><span>&quot;: <\/span><span style=\"color:#b48ead;\">f<\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">Bearer <\/span><span>{writer_api_key}&quot;}))\n<\/span><\/code><\/pre>\n<p>This is one area where the MCP ecosystem is actively evolving. The MCP specification now includes comprehensive OAuth 2.1 support for HTTP-based transports, with proper authorization flows, dynamic client registration, and standardized discovery mechanisms. However, the practical reality varies significantly between different implementations.<\/p>\n<h3 id=\"debugging\">Debugging<\/h3>\n<p>When things go wrong (and they will), you don\u2019t have as many options for debugging as you typically would when working with traditional APIs, especially if the MCP server is hosted remotely.<\/p>\n<p><strong>Available now:<\/strong><\/p>\n<ul>\n<li>\n<p>curl for protocol-level debugging<\/p>\n<\/li>\n<li>\n<p><a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/github.com\/modelcontextprotocol\/inspector\">MCP Inspector<\/a> for local development<\/p>\n<\/li>\n<li>\n<p>Server capability inspection via HTTP<\/p>\n<\/li>\n<li>\n<p>Client-side logs (if your client exposes them)<\/p>\n<\/li>\n<\/ul>\n<p>The community is working on better tooling like MCP DevTools, protocol analyzers, and VS Code extensions that are being developed for MCP debugging. The ecosystem is moving fast, so hopefully the debugging experience will improve significantly over the next few months. I\u2019m really excited about the new tooling around observability being added to the overall AI ecosystem.<\/p>\n<hr \/>\n<ol>\n<li>Mintlify has been very forward-thinking about AI integration, with MCP servers and several other AI-first features. Instead of treating AI integration as an afterthought, they\u2019ve made it a core feature. It\u2019s particularly useful when you\u2019re building agents that need to work across multiple tools and platforms, like the research agent we built above.<\/li>\n<\/ol>\n"},{"title":"Learn MCP with me, part 1: what is this even for?","pubDate":"Mon, 08 Sep 2025 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/learn-mcp-part-1\/","guid":"https:\/\/sdeaton.com\/blog\/learn-mcp-part-1\/","description":"<p>I wrote this post because I found myself struggling with a very specific question: Why MCP when we already have APIs and so many ways to communicate programmatically?<\/p>\n<p><strong>TL;DR:<\/strong> MCP isn\u2019t replacing APIs; it\u2019s a guardrail layer that makes LLMs less terrible at using them.<\/p>\n<h2 id=\"why-mcp-when-we-already-have-apis\">Why MCP when we already have APIs?<\/h2>\n<p>I come from an API development background: I\u2019m comfortable working with, writing, and documenting APIs. Since MCP came out, I\u2019ve been side-eyeing it like, \u201cmhmm, yes I get it, it\u2019s just another API.\u201d<\/p>\n<p>But when I started to dig in, I couldn\u2019t get past the question: what problem does this solve that REST + OpenAPI don\u2019t already cover?<\/p>\n<p>We\u2019ve already solved \u201cconnect different systems\u201d multiple times. We\u2019ve got REST, GraphQL, gRPC, even SOAP, and so many others. And then MCP shows up in 2024 saying \u201cHere\u2019s a standardized way for AI to connect to external tools.\u201d What problem is it solving that REST + OpenAPI doesn\u2019t already solve? It felt like the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\/\/xkcd.com\/927\/\">XKCD comic<\/a>.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/learn-mcp-part-1\/b8b6e40d-3a45-4241-b988-aea108f47dcd.png\" alt=\"XKCD comic about how standards proliferate: 14 competing standards leads to a new universal standard, resulting in 15 competing standards\" \/><\/p>\n<h2 id=\"the-answer-llms-suck-at-using-apis\">The answer: LLMs suck at using APIs<\/h2>\n<p>After reading more, here\u2019s what I found: LLMs are terrible API clients. They hallucinate parameter names, mishandle nested responses, and struggle with authentication patterns.<\/p>\n<p>Chatting with an LLM can sometimes feel so magical that I forget they\u2019re all just probabilistic token generators. They\u2019re not reading the OpenAPI spec or docs and using logic and reasoning to determine the correct API response. They\u2019re coming up with the most common patterns they\u2019ve seen in training, and sometimes those aren\u2019t actually correct.<\/p>\n<h2 id=\"how-mcp-fixes-this\">How MCP fixes this<\/h2>\n<p>MCP doesn\u2019t replace APIs. It inserts a competency layer between LLMs and APIs. Here\u2019s what it adds:<\/p>\n<ul>\n<li>\n<p><strong>Standardized tool interface<\/strong>: LLM just fills in parameters, no guessing endpoint names or auth headers.<\/p>\n<\/li>\n<li>\n<p><strong>Clean, structured responses<\/strong>: Instead of wrestling with 50-field JSON objects, the LLM gets simplified outputs.<\/p>\n<\/li>\n<li>\n<p><strong>Authentication abstraction<\/strong>: Tokens, OAuth, headers are all handled server-side, so the LLM doesn\u2019t need to deal with any of that, it just needs to be handed the correct keys.<\/p>\n<\/li>\n<li>\n<p><strong>Discovery built-in<\/strong>: The LLM can ask \u201cWhat tools are available?\u201d rather than needing a preloaded spec.<\/p>\n<\/li>\n<\/ul>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/learn-mcp-part-1\/2a43ce7f-87f7-4b43-b128-210a781a3622.png\" alt=\"Diagram showing MCP as a layer between AI applications and external tools, providing standardized interfaces and authentication handling\" \/><\/p>\n<p>Instead of fragile API calls, the LLM gets a structured template it just fills in. It gets information back in a reliable, standardized way. It doesn\u2019t have to guess that a GitHub API needs <code>repository_name<\/code> vs. <code>repo_name<\/code>; MCP ensures it always gets the right parameter structure. Then you can plug that into agents or tools like Cursor or Claude so they can interact with those systems.<\/p>\n<h3 id=\"follow-up-i-m-still-confused\">Follow up: I\u2019m still confused<\/h3>\n<p>I still had a few questions. Mainly, if an LLM can\u2019t just look at an OpenAPI spec and get it right, why would it magically succeed with MCP?<\/p>\n<p>The key is that MCP doesn\u2019t rely on the model <em>parsing and reasoning<\/em> over a giant spec. Instead, the MCP runtime hands the LLM a structured template for each tool, enforces the parameters, and validates the output. The model isn\u2019t \u201clearning MCP\u201d from training. It\u2019s just filling in blanks inside a schema the runtime controls. That shift from \u201cread this 50-page manual and hope you guess correctly\u201d to \u201cfill out this form, we\u2019ll handle the rest\u201d is what makes MCP reliably work where OpenAPI falls apart.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/learn-mcp-part-1\/6187b950-3160-45af-8414-236eab01944e.png\" alt=\"Diagram illustrating how MCP transforms complex API interactions into simple fill-in-the-blank templates for LLMs\" \/><\/p>\n<h2 id=\"when-you-would-want-to-use-mcp\">When you would want to use MCP<\/h2>\n<p>One of the biggest errors I\u2019ve seen with people using AI is overengineering a solution. Sometimes it\u2019s going to be a lot more reliable and straightforward to just integrate with one API than add a bunch of MCP servers.<\/p>\n<p>That said, there are definitely cases when MCP is the right choice:<\/p>\n<ul>\n<li>\n<p><strong>AI agents that adapt<\/strong>: a code review assistant that chooses tools based on changed files.<\/p>\n<\/li>\n<li>\n<p><strong>Chat interfaces with tool access<\/strong>: asking \u201cAny errors in the auth module?\u201d and having the LLM query Sentry + GitHub automatically.<\/p>\n<\/li>\n<li>\n<p><strong>Cross-system workflows<\/strong>: coordinating multiple tools without manually programming the details for each one.<\/p>\n<\/li>\n<\/ul>\n<p>The cross-systems workflows use case is where it gets exciting: the ability to coordinate across multiple systems without hardcoding every single step yourself.<\/p>\n<p>Imagine an AI assistant helping with customer support triage. It could:<\/p>\n<p>\u2022 Pull recent Zendesk tickets that mention a specific issue<\/p>\n<p>\u2022 Cross-reference related documentation in Confluence or issues in Jira<\/p>\n<p>\u2022 Post a suggested reply draft in Slack for a support agent to review<\/p>\n<p>With APIs, you\u2019d have to write and maintain that integration flow. With MCP, the agent can discover tools at runtime and decide which to call, in what order. It\u2019s less \u201cpre-baked script\u201d and more \u201cAI reasoning with a toolkit.\u201d<\/p>\n<p>That\u2019s MCP\u2019s sweet spot: enabling adaptive orchestration across services.<\/p>\n<h2 id=\"when-you-don-t-need-mcp\">When you don\u2019t need MCP<\/h2>\n<p>There are many cases when MCP and even agentic AI is overkill:<\/p>\n<ul>\n<li>\n<p>You\u2019re building deterministic workflows (cron jobs, fixed reports)<\/p>\n<\/li>\n<li>\n<p>You\u2019re writing code yourself (you don\u2019t need a middleman)<\/p>\n<\/li>\n<li>\n<p>It\u2019s a single integration (REST is simpler)<\/p>\n<\/li>\n<li>\n<p>You need full debugging control (direct API calls are transparent)<\/p>\n<\/li>\n<\/ul>\n<h2 id=\"the-bottom-line\">The bottom line<\/h2>\n<p>MCP isn\u2019t a revolution. It\u2019s an evolution. It wraps existing API practices in guardrails so LLMs can use them without faceplanting. But, it\u2019s still pretty exciting when you start working with it and seeing how quickly you can add integrations into an AI app.<\/p>\n<p>Now that I\u2019m convinced MCP is worthwhile, I\u2019ll dig into how to actually connect to MCP servers. That\u2019s coming soon.<\/p>\n"},{"title":"We all fall down: notes from an ordinary outage","pubDate":"Sat, 06 Sep 2025 00:00:00 +0000","author":"Unknown","link":"https:\/\/sdeaton.com\/blog\/we-all-fall-down\/","guid":"https:\/\/sdeaton.com\/blog\/we-all-fall-down\/","description":"<p>I love incidents. They\u2019re like mini soap operas, tech stacks buckling under ordinary circumstances for unpredictable reasons, cascading failures, humans rushing to figure out what happened. So normal and each time so unexpected.<\/p>\n<p>It\u2019s been a while since I worked in SRE and have gotten to see a good incident up close. So when a friend who works in infra at a streaming company told me about one particular outage, I wanted to dig into the details and learn more. A regular video processing job kicked off and suddenly the whole cluster was stuck for five days.<\/p>\n<p>It\u2019s the kind of story that reminds me how the internet actually works: not as a gleaming futuristic system, but as a patchwork quilt of duct tape, YAML, and unfinished TODOs.<\/p>\n<p>Here\u2019s my retelling, my best effort as an outsider through persistent questions.<\/p>\n<hr \/>\n<h2 id=\"sidebar-what-even-is-kubernetes\">Sidebar: what even is Kubernetes?<\/h2>\n<p>This story\u2019s main character is Kubernetes. I\u2019ve had the great fortune to not have to deal with Kubernetes (a.k.a \u201ck8s\u201d) directly. For those like me, here\u2019s my best take on it: it\u2019s like a manager for your apps. Instead of one person running one program on one computer, Kubernetes spreads workloads across many computers, makes sure things restart if they crash, and helps scale up when traffic spikes. It\u2019s kind of like an orchestra conductor making sure dozens of instruments stay in sync, except the instruments are apps, and they sometimes set themselves on fire.<\/p>\n<hr \/>\n<h2 id=\"how-it-started\">How it started<\/h2>\n<p>One of the teams at my friend\u2019s company submitted a large video processing job for routine batch encoding work. These kinds of jobs fan out into tens of thousands of pods: little containers that encode video segments in parallel. There was nothing different about this job except that it was a bit bigger than usual.<\/p>\n<p>But then things jammed. The first clue came when their API gateway started refusing requests with mysterious error codes. Behind the scenes, Kubernetes was taking longer than 60 seconds to answer any kind of question, even one like \u201chey, what pods are running right now?\u201d<\/p>\n<p>That delay alone was enough to break things, because their cloud provider gives up commands after 60 seconds. So from the outside as a user, the whole system looked frozen. Someone would try to kick off a new job, notice it wasn\u2019t running, try to query what was going on, and nothing would happen.<\/p>\n<hr \/>\n<h2 id=\"sidebar-pods-in-plain-english\">Sidebar: pods in plain English<\/h2>\n<p>Pods are the smallest \u201cwork unit\u201d in Kubernetes. Imagine every task you want to run, like \u201cencode this 30-second clip,\u201d gets its own little container. That\u2019s a pod. They\u2019re disposable, lightweight, and meant to come and go constantly. Pods help things run in parallel and keep discrete workloads isolated.<\/p>\n<hr \/>\n<h2 id=\"slack-pings-and-silent-dashboards\">Slack pings and silent dashboards<\/h2>\n<p>This part of the infra team doesn\u2019t rely heavily on alerts. Most of their workloads are batch encoding jobs, which should retry and recover automatically. The first notice of something wrong came in Slack with messages like:<\/p>\n<ul>\n<li>\n<p>\u201cHey, the encoding job isn\u2019t finishing\u2026\u201d<\/p>\n<\/li>\n<li>\n<p>\u201cIs the processing cluster down?\u201d<\/p>\n<\/li>\n<li>\n<p>\u201cThe batch queue seems stuck\u201d<\/p>\n<\/li>\n<\/ul>\n<p>So the infra team went to the dashboards. But, nothing looked wrong.<\/p>\n<h2 id=\"the-metrics-that-lied\">The metrics that lied<\/h2>\n<p>Everything seemed fine when they went to look at the metrics. That\u2019s when they remembered that their monitoring system, Prometheus, was configured to \u201cassume everything\u2019s fine\u201d for 30 minutes if it stopped hearing from a service.<\/p>\n<p>If something died, Prometheus just kept reporting its last known state for 30 minutes, rather than showing that the service had entirely stopped sending metrics. To the dashboard, it looked like video encoding was proceeding normally. To the humans, it meant they had no actual insight into what or where things were breaking as the batch processing queue backed up.<\/p>\n<p>This was a known issue with their monitoring system, but it turns out that the engineer who started fixing it had left for another job, and the half-complete system stayed behind, quietly misleading everyone.<\/p>\n<h2 id=\"the-missing-gatekeeper\">The missing gatekeeper<\/h2>\n<p>Eventually, after lots of digging, they discovered the real culprit: Kyverno, the \u201cgatekeeper\u201d service that approves or rejects new pods, had vanished.<\/p>\n<p>It hadn\u2019t crashed. It\u2019s not that it just had to spin up again. It had been evicted entirely.<\/p>\n<p>Why? Because another service from the security team had been given higher priority than even critical system services. So when resources got tight, Kubernetes politely kicked Kyverno out to make room.<\/p>\n<p>In this case, the security team hadn\u2019t coordinated with the infra team when they set that priority. Infra assumed it was just another workload, not something that could outrank the cluster\u2019s most essential services.<\/p>\n<p>Imagine a bouncer who has to check every person before they can enter the club getting kicked out when the club gets busy by someone shouting \u201cFOR SECURITY REASONS.\u201d<\/p>\n<p>No Kyverno means no new pods, including Kyverno itself.<\/p>\n<h2 id=\"the-long-slog\">The long slog<\/h2>\n<p>Part of the reason this outage dragged on was that Kyverno itself hadn\u2019t been set up with any safety nets. Only one copy was running instead of multiple replicas because no one had updated the default setting from <code>replicas: 1<\/code>. If there had been replicas, there could have been another copy of Kyverno available to let in the evicted one. And, there were no rules in place to prevent it from being kicked out.<\/p>\n<p>So when Kubernetes needed to free up space and Kyverno went away, with it went the entire cluster\u2019s ability to process encoding jobs.<\/p>\n<p>Not having done Kubernetes work myself, I wondered why they couldn\u2019t just delete the problematic jobs that started this mess. Well, apparently the jobs weren\u2019t just sitting in the queue. They were part of Kubernetes\u2019 Job and Pod system, which is designed to retry until success.<\/p>\n<p>If you delete one, the Job controller notices: \u201cOh, that pod disappeared, but the job isn\u2019t done yet\u201d and it immediately schedules a new one. And in this outage, the admission controller was down, which meant nothing new could start anyway, so trying to \u201cclean up\u201d jobs didn\u2019t help. They\u2019d just boomerang back or stay pending, still clogging the scheduler\u2019s brain.<\/p>\n<p><img src=\"https:\/\/sdeaton.com\/blog\/we-all-fall-down\/885b17a1-659f-4469-836a-184b006a5c47.png\" alt=\"Diagram illustrating the Kubernetes job boomerang effect: deleting a pod causes the Job controller to immediately schedule a replacement\" \/><\/p>\n<h2 id=\"the-recovery\">The recovery<\/h2>\n<p>The only way out was drastic: delete the admission rules that required Kyverno in the first place. That needed elevated permissions, so another team stepped in to rip out the safety checks temporarily.<\/p>\n<p>Once Kyverno was no longer required, the cluster slowly brought itself back online:<\/p>\n<ul>\n<li>\n<p>The setup tasks could run.<\/p>\n<\/li>\n<li>\n<p>Nodes became ready.<\/p>\n<\/li>\n<li>\n<p>Kyverno was allowed back.<\/p>\n<\/li>\n<\/ul>\n<p>They restored the admission rules, this time with better safeguards: multiple replicas, disruption budgets, and higher priority.<\/p>\n<p>The encoding pipeline limped back to life, and the batch processing queue finally started clearing.<\/p>\n<hr \/>\n<h2 id=\"sidebar-why-removing-admission-rules-is-a-big-deal\">Sidebar: why removing admission rules is a big deal<\/h2>\n<p>Deleting admission rules didn\u2019t sound that dramatic to me \u2013 just get rid of a few filters to get things back to normal. But it\u2019s closer to removing the turnstiles at a subway station. Normally, those gates check every passenger\u2019s ticket, which slows things down slightly, but ensures everyone who enters is allowed. Without them, trains move faster at first, but freeloaders and chaos slip in.<\/p>\n<p>That\u2019s what happened here: taking down the rules let the cluster recover, but it also meant anything (good or bad) could sneak through until order was restored.<\/p>\n<hr \/>\n<h2 id=\"lessons\">Lessons<\/h2>\n<p>This whole saga was a reminder that:<\/p>\n<ul>\n<li>\n<p>Small defaults matter. Forgetting to set \u201crun 3 copies of this critical service\u201d can take down a whole processing pipeline.<\/p>\n<\/li>\n<li>\n<p>Priorities collide. One team\u2019s well-intentioned security tweak can accidentally evict the very thing keeping the system alive.<\/p>\n<\/li>\n<li>\n<p>Observability isn\u2019t optional. Dashboards that lie are worse than dashboards that scream.<\/p>\n<\/li>\n<li>\n<p>Complex systems fail in loops. Once you get stuck in a cycle (\u201ccan\u2019t start A without B, can\u2019t start B without A\u201d), recovery often requires outside help.<\/p>\n<\/li>\n<\/ul>\n<h2 id=\"in-the-end\">In the end<\/h2>\n<p>What I loved about this outage was that it had less to do with machines failing and more to do with humans being human. Someone left a job and left their half-finished monitoring fix behind. Someone else set a priority without checking what it might evict. Someone forgot to configure replicas. All perfectly reasonable decisions in isolation.<\/p>\n<p>It was a reminder that our digital world runs on millions of these small choices, most of them invisible until suddenly brought under the microscope of a blameless postmortem.<\/p>\n<p>The miracle isn\u2019t that things break. It\u2019s that they hold together as often as they do. Most of the time, our duct tape and YAML and forgotten TODOs somehow keep the lights on. Until they don\u2019t.<\/p>\n<hr \/>\n<ol>\n<li>When they\u2019re not actively happening to me and I didn\u2019t cause them by dropping a column in the production database.<\/li>\n<\/ol>\n"}]}}