<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Shimon</title>
    <description>The latest articles on DEV Community by David Shimon (@david_shimon).</description>
    <link>https://dev.to/david_shimon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1502835%2F08fa9db4-e979-4e6f-8b8d-9558d2e7748d.jpg</url>
      <title>DEV Community: David Shimon</title>
      <link>https://dev.to/david_shimon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/david_shimon"/>
    <language>en</language>
    <item>
      <title>Stop Manually Fixing Your Agent’s Output: How and Why We Built a Custom Skill for Monday.com</title>
      <dc:creator>David Shimon</dc:creator>
      <pubDate>Wed, 22 Apr 2026 05:53:01 +0000</pubDate>
      <link>https://dev.to/david_shimon/stop-manually-fixing-your-agents-output-how-we-built-a-custom-skill-for-mondaycom-2ilh</link>
      <guid>https://dev.to/david_shimon/stop-manually-fixing-your-agents-output-how-we-built-a-custom-skill-for-mondaycom-2ilh</guid>
      <description>&lt;p&gt;I'm using my AI agent to create Monday.com tasks. I ask Claude Code to create a task with a proper description, and it does - except when I open the task, the description field is empty. The text I wanted is sitting in the updates thread. Like a comment. Not a description.&lt;/p&gt;

&lt;p&gt;The reason I'm doing this from Claude Code at all: context. When you're mid-task - debugging something, writing a spec, reviewing code - and you realize something needs to go on the backlog, the last thing you want is to context-switch to a browser, find the right board, fill in the form, and try to reconstruct what you were thinking. With Claude Code, I just say "add this to the backlog" and it already knows what we've been working on. Nothing gets lost. Then we keep going.&lt;/p&gt;

&lt;p&gt;I assumed it was a bug.&lt;br&gt;
It wasn’t. It was worse: the system was working exactly as designed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://monday.com/w/mcp" rel="noopener noreferrer"&gt;Monday MCP&lt;/a&gt; server exposes a bunch of tools. When Claude looks for "how do I add content to a task", it scans the tool list and finds &lt;code&gt;create_update&lt;/code&gt;. That sounds like "update an item", but it actually creates an entry in the updates thread. Think comment section, not description field.&lt;/p&gt;

&lt;p&gt;It's a naming problem, but also a missing tool problem. There was simply no tool for setting an item's description.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the right mutation
&lt;/h2&gt;

&lt;p&gt;I wanted to understand what the &lt;em&gt;correct&lt;/em&gt; path was. So I let Claude read an existing task from my board first. That's when it noticed: "The description is stored as &lt;code&gt;item_description&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;Now we knew the field existed. But how do you set it? Claude's next move was to dump the full Monday GraphQL schema via &lt;a href="https://github.com/mondaycom/mcp#-dynamic-api-tools-beta" rel="noopener noreferrer"&gt;all_monday_api&lt;/a&gt; and grep through it. The schema came back as 51KB of JSON. We filtered it down to mutations with "description" in the name — and there it was:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;set_item_description_content&lt;/code&gt;: Sets an item description document's content with new markdown data.&lt;/p&gt;

&lt;p&gt;The right tool existed. It was just completely invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it was invisible
&lt;/h2&gt;

&lt;p&gt;No agent is going to figure this out on its own.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;set_item_description_content&lt;/code&gt; was added to the Monday API on &lt;strong&gt;January 26, 2026&lt;/strong&gt;. The MCP server hasn't caught up yet — so the only way to reach it is through &lt;code&gt;all_monday_api&lt;/code&gt;, a generic escape hatch that lets you run raw GraphQL. The mutation is in there, but you'd never know unless you already knew to look.&lt;/p&gt;

&lt;p&gt;Monday stores item descriptions separately from item metadata, which is why the correct flow requires two calls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;create_item&lt;/code&gt;: sets name, owner, status, etc.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;all_monday_api&lt;/code&gt; with &lt;code&gt;set_item_description_content&lt;/code&gt;: sets the description as markdown&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The fix: encode the right behavior
&lt;/h2&gt;

&lt;p&gt;I created a &lt;code&gt;/create-monday-task&lt;/code&gt; skill for Claude Code that wraps this two-step flow correctly. The skill does two things the agent wouldn't figure out alone: it explicitly forbids &lt;code&gt;create_update&lt;/code&gt; (with a comment explaining why), and it sequences the two API calls correctly: &lt;code&gt;create_item&lt;/code&gt; first, then &lt;code&gt;set_item_description_content&lt;/code&gt; via raw GraphQL.&lt;/p&gt;

&lt;p&gt;The "NEVER use &lt;code&gt;create_update&lt;/code&gt;" guardrail is the part that matters most. Without it, any future agent - or future me - would fall into the same trap.&lt;/p&gt;

&lt;p&gt;Now when I ask Claude to create a task, it uses the skill, and the description ends up in the right place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The upstream fix
&lt;/h2&gt;

&lt;p&gt;The real fix is simpler: &lt;code&gt;create_item&lt;/code&gt; should just accept a &lt;code&gt;description&lt;/code&gt; parameter. Internally it would call &lt;code&gt;set_item_description_content&lt;/code&gt; after creating the item. One call, no schema exploration needed.&lt;/p&gt;

&lt;p&gt;I filed it here: &lt;a href="https://github.com/mondaycom/mcp/issues/314" rel="noopener noreferrer"&gt;github.com/mondaycom/mcp/issues/314&lt;/a&gt; — no response yet, but the issue documents the expected behavior if you want to follow along.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broader lesson
&lt;/h2&gt;

&lt;p&gt;When something goes wrong, the instinct is to fix it manually and move on. Go into Monday, cut the text from the update, paste it into the description, done. I've done this more times than I'd like to admit.&lt;/p&gt;

&lt;p&gt;But that's the wrong response — and the leverage gets even bigger when you're on a team. A manual fix helps you once. A shared skill helps everyone, every time, and nobody else has to dig through 51KB of schema to understand why.&lt;/p&gt;

&lt;p&gt;The better question is: if I can figure out the right way to do this, why can't the agent? Usually, if there's a path — even a two-step, buried-in-raw-GraphQL path — the agent can follow it once you show it the way. The fix isn't to correct the output by hand. It's to invest a little time understanding &lt;em&gt;why&lt;/em&gt; it went wrong, and then encode that understanding: a skill, a guardrail, a better tool description.&lt;/p&gt;

&lt;p&gt;That investigation is rarely wasted either. You come out the other side with a working solution &lt;em&gt;and&lt;/em&gt; a clearer mental model of how agents actually select and use tools — which makes the next debugging session faster.&lt;/p&gt;

&lt;p&gt;Manual corrections disappear. A well-written skill sticks around.&lt;/p&gt;

&lt;p&gt;So next time your agent does something slightly off — before you reach for the manual fix — ask: is there a way to make the agent do this right? There probably is. It's almost always worth finding out.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>automation</category>
      <category>claude</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Skill That Helps Me Do Code Review</title>
      <dc:creator>David Shimon</dc:creator>
      <pubDate>Sun, 05 Apr 2026 08:41:07 +0000</pubDate>
      <link>https://dev.to/david_shimon/the-skill-that-helps-me-do-code-review-3ejc</link>
      <guid>https://dev.to/david_shimon/the-skill-that-helps-me-do-code-review-3ejc</guid>
      <description>&lt;p&gt;I was complaining. Again.&lt;/p&gt;

&lt;p&gt;Code review was eating my time, and the problem was only getting worse. We were shipping more features faster, which meant more code, much of it in domains I don't know deeply. Agentic development is great for output. It's not great for the person who still has to &lt;em&gt;understand&lt;/em&gt; what got built.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/amitaile"&gt;Amitai&lt;/a&gt;, a teammate, got tired of hearing about it. He sent me a link.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tip 26
&lt;/h2&gt;

&lt;p&gt;The article was &lt;em&gt;&lt;a href="https://github.com/ykdojo/claude-code-tips?tab=readme-ov-file#tip-26-interactive-pr-reviews" rel="noopener noreferrer"&gt;45 Claude Code Tips: From Basics to Advanced&lt;/a&gt;&lt;/em&gt; by &lt;a href="https://x.com/ykdojo" rel="noopener noreferrer"&gt;@ykdojo&lt;/a&gt;. Tip 26 was about interactive PR reviews:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Claude Code is great for PR reviews. The procedure is pretty simple: you ask it to retrieve PR information using the gh command, and then you can go through the review however you want.&lt;/p&gt;

&lt;p&gt;You can do a general review, or go file by file, step by step. You control the pace. You control how much detail you want to look into and the level of complexity you want to work at. Maybe you just want to understand the general structure, or maybe you want to have it run tests too.&lt;/p&gt;

&lt;p&gt;The key difference is that Claude Code acts as an interactive PR reviewer, not just a one-shot machine. Some AI tools are good at one-shot reviews (including the latest GPT models), but with Claude Code you can have a conversation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I tried it once. Then again. Then I turned it into a skill.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I still read the code
&lt;/h2&gt;

&lt;p&gt;Before getting into the workflow, I want to address the elephant in the room.&lt;/p&gt;

&lt;p&gt;Code review in the agentic era is genuinely contested. Some people think it's becoming obsolete. I don't.&lt;/p&gt;

&lt;p&gt;Agents write a lot of code. They also produce a lot of slop: suboptimal solutions, non-standard patterns, things that don't match your team's conventions, and sometimes just plain bugs. The responsibility is still ours. Even Boris Cherny, the creator of Claude Code, says he still reads the code he generates.&lt;/p&gt;

&lt;p&gt;There's also a practical reason: the person who wrote the code might be on vacation next week. I need to be able to own it, change it, and defend it. That only happens if I actually understood it during review.&lt;/p&gt;

&lt;p&gt;At PhaseV, CoPilot runs automatic review on every PR. On top of that, I manually run one-shot reviews using Codex and Claude Code's built-in skills. They catch real things. But automated review isn't the same as understanding the code. That still requires a human.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the flow works
&lt;/h2&gt;

&lt;p&gt;Our team writes commits that tell the story of a change. Each commit is atomic and focused.&lt;/p&gt;

&lt;p&gt;I type &lt;code&gt;/review-pr-interactive&lt;/code&gt; and the PR number. From there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude checks out the branch and summarizes the PR's goal&lt;/li&gt;
&lt;li&gt;It lists every commit with a one-line description&lt;/li&gt;
&lt;li&gt;It starts on the first commit: explains what's happening, flags what deserves attention, and asks for my take &lt;em&gt;before&lt;/em&gt; we move on&lt;/li&gt;
&lt;li&gt;I read the commit myself. If it's larger than expected, I ask Claude for a recommended reading order for the files before diving in&lt;/li&gt;
&lt;li&gt;We decide together what's worth commenting on&lt;/li&gt;
&lt;li&gt;Claude uses the GitHub CLI to post the comment on the exact relevant line&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then we move to the next commit.&lt;/p&gt;

&lt;p&gt;When I hit something I don't recognize, I ask Claude and learn something new, then usually post it on &lt;a href="https://x.com/davidshimon" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; under &lt;code&gt;#TIL&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a skill and not just a prompt
&lt;/h2&gt;

&lt;p&gt;At this point you might ask: why formalize this as a skill? You could just tell Claude what you want each time.&lt;/p&gt;

&lt;p&gt;The answer is consistency. Every time I start a review, Claude already knows our team's conventions, the tone we use for comments, what counts as a Nitpick, and that it should wait for my approval before posting anything. I don't re-explain. I don't forget to mention something. The skill is the accumulated knowledge of every session that didn't go quite right.&lt;/p&gt;

&lt;p&gt;One concrete example: before I wrote the skill, it took Claude around 3 attempts each session to figure out the right &lt;code&gt;gh&lt;/code&gt; command to add a comment on a specific line in a PR. Now it's written into the skill, and it gets it right the first time.&lt;/p&gt;

&lt;p&gt;It also lowers the activation energy. Typing &lt;code&gt;/review-pr-interactive 847&lt;/code&gt; is frictionless. Writing a detailed prompt from scratch is not.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we added along the way
&lt;/h2&gt;

&lt;p&gt;After using it daily, we refined the skill to fit how our team works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Define the focus&lt;/strong&gt; — set what matters most upfront for this type of PR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nitpick labeling&lt;/strong&gt; — it's fine to flag style issues, but mark them clearly as Nitpicks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boy Scout commits&lt;/strong&gt; — we allow unrelated cleanup in a separate commit within the PR. No separate PR required, just keep it isolated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No auto-commenting&lt;/strong&gt; — Claude waits for my explicit approval before posting anything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A separate thing we had to shape: how Claude behaves when approving a PR. Early on it would write a full essay summarizing everything the PR did as part of the approval message. Very strange. The PR creator knows what they built. We defined in the skill that a simple LGTM is enough, or a brief note on anything that genuinely stood out.&lt;/p&gt;




&lt;h2&gt;
  
  
  The thing that surprised me
&lt;/h2&gt;

&lt;p&gt;I expected this to save time. It did.&lt;/p&gt;

&lt;p&gt;What I didn't expect was that it would make me &lt;em&gt;more&lt;/em&gt; present in the review, not less. With one-shot automated tools, I read a list of findings and move on. With this flow, I'm actually in the code: reading, asking questions, making judgment calls. The AI handles the scaffolding. I bring the understanding.&lt;/p&gt;

&lt;p&gt;That feels like the right division of labor.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;How are you handling code review as AI writes more of your codebase?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codereview</category>
      <category>skills</category>
    </item>
    <item>
      <title>Prompting, Vague and Precise</title>
      <dc:creator>David Shimon</dc:creator>
      <pubDate>Sun, 22 Feb 2026 13:13:29 +0000</pubDate>
      <link>https://dev.to/david_shimon/prompting-vague-and-precise-1ngc</link>
      <guid>https://dev.to/david_shimon/prompting-vague-and-precise-1ngc</guid>
      <description>&lt;p&gt;I was in a session with Shiry, our sharp product manager, demoing &lt;a href="https://claude.ai/code" rel="noopener noreferrer"&gt;Claude Code Web&lt;/a&gt;'s capabilities. We chose a nice-to-have feature to work on: reflecting the active filter state in our data table.&lt;/p&gt;

&lt;p&gt;The product has a table with dozens of columns. Users can filter rows by values in one or more columns, and they can show or hide columns as they like.&lt;/p&gt;

&lt;p&gt;The problem: a user can end up with active filters and have no idea which columns they're filtering on. To find the filter indicator in a column header, they'd have to scroll the table horizontally, and maybe unhide columns that are currently hidden. A real game of hide and seek.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;First Attempt: Vague about the Problem, Precise About the Solution&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I asked Claude Code to add a list of filtered fields above the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add next to the text of "Showing 21 items", the fields we are filter 
the items by (if any). e.g. "Showing 21 items, filtered by Name and Country".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It wrote the code, committed, pushed. We waited for the build, checked the preview environment, and got exactly what I asked for: an ugly list of field names.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Second Attempt: Precise About the Problem, Vague About the Solution&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We started fresh.&lt;/p&gt;

&lt;p&gt;Shiry, who's a master at building prototypes using LLMs, suggested I try again. This time she dictated the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;On the items table, I want to help the user understand that there is an active filter and what it is.
(without having to look for it on the columns' headers, to scroll, or find  hidden column with filter, etc.)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time we were much vaguer about the solution, but we described the problem precisely, the same way I described it at the top of this post.&lt;/p&gt;

&lt;p&gt;The result impressed me.&lt;/p&gt;

&lt;p&gt;Instead of a plain text list, Claude Code rendered a row of chips. Each chip showed the field name and the selected filter values. If more than two values were selected, it showed the count instead. Each chip had a small X to remove that field's filter. And when multiple filters were active, a "clear all" button appeared.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff3o7xzpnx4adst4jbh2m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff3o7xzpnx4adst4jbh2m.png" alt=" " width="800" height="56"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Night and day difference. The second implementation was better by an order of magnitude. It looked familiar, it looked professional, far more useful. It looked like something a designer would design.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Lesson&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When I described the solution, the model built the solution I described.&lt;/p&gt;

&lt;p&gt;When I described the problem, the model solved the problem. And it knew a better solution than the one I thought of at first.&lt;/p&gt;

&lt;p&gt;Be precise about the problem. Be vague about the solution.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ux</category>
      <category>promptengineering</category>
    </item>
    <item>
      <title>One Word, One Bug, and the Future of AI-Assisted Development</title>
      <dc:creator>David Shimon</dc:creator>
      <pubDate>Sat, 14 Feb 2026 21:10:01 +0000</pubDate>
      <link>https://dev.to/david_shimon/one-word-one-bug-and-the-future-of-ai-assisted-development-2o06</link>
      <guid>https://dev.to/david_shimon/one-word-one-bug-and-the-future-of-ai-assisted-development-2o06</guid>
      <description>&lt;h2&gt;
  
  
  The Pain Point
&lt;/h2&gt;

&lt;p&gt;I was setting permissions for a list of users on our platform. Find a user, toggle the switch, repeat. One by one. With a list of 15 emails staring at me from a spreadsheet.&lt;/p&gt;

&lt;p&gt;Then it hit me - the search box is &lt;em&gt;right there&lt;/em&gt;. What if I could just paste all the emails at once, space-separated, and see all the matching users at once?&lt;/p&gt;

&lt;h2&gt;
  
  
  The One-Word Fix
&lt;/h2&gt;

&lt;p&gt;In the code-agentic world, it's probably faster to just try implementing the feature than to open a ticket and wait for prioritization. And honestly, even before AI assistants, I've always loved these low-hanging fruits - the small changes that with a little effort make life easier. Isn't that why we love this job? Especially when the life you're improving is your own.&lt;/p&gt;

&lt;p&gt;I described what I wanted to Claude Code. The solution was almost comically simple: change &lt;code&gt;.every()&lt;/code&gt; to &lt;code&gt;.some()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But first - why was it &lt;code&gt;.every()&lt;/code&gt; in the first place? Respecting Chesterton's Fence, I wanted to understand the reasoning before tearing it down. The original AND logic made sense for its original purpose: if you type "john smith", &lt;code&gt;.every()&lt;/code&gt; ensures &lt;em&gt;both&lt;/em&gt; "john" and "smith" match, narrowing results to that specific person. It treated the search box as a single query where spaces refine the search. A perfectly reasonable design for searching by name.&lt;/p&gt;

&lt;p&gt;But my use case was different. I wasn't searching for one person with a multi-word name - I was searching for &lt;em&gt;multiple people&lt;/em&gt; by their emails. And email addresses don't have spaces, so each space-separated token is a distinct person I'm looking for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: AND logic - all terms must match the same user (refines a single search)&lt;/span&gt;
&lt;span class="nx"&gt;searchTerms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// After: OR logic - any term can match (finds multiple users at once)&lt;/span&gt;
&lt;span class="nx"&gt;searchTerms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One word. That's it. And the trade-off? Even for the original "john smith" use case, the results are still useful - John Smith will still appear, just alongside other Johns and Smiths. A slightly broader result set is a small price for the ability to search for multiple users at once.&lt;/p&gt;

&lt;p&gt;I committed, pushed, opened a PR, and kicked off the build pipeline. No local testing - the local environment had some glitches, and with a change this small, I figured I'd validate on the preview environment once the build finished. A one-word change. What could go wrong?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vacuous Truth
&lt;/h2&gt;

&lt;p&gt;While I waited, GitHub Copilot reviewed the PR. At first glance, nothing stood out - the review looked clean. But then I noticed a collapsed comment: &lt;em&gt;"Comment suppressed due to low confidence."&lt;/em&gt; I almost scrolled past it. I clicked to expand, and there it was - a flag for something I completely missed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When the search box is empty, &lt;code&gt;searchTerms&lt;/code&gt; is an empty array. &lt;code&gt;[].some(...)&lt;/code&gt; always returns &lt;code&gt;false&lt;/code&gt;. This will hide every user when there's no search query.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a real computer science concept called &lt;strong&gt;vacuous truth&lt;/strong&gt;: a statement about &lt;em&gt;all&lt;/em&gt; members of an empty set is trivially true. &lt;code&gt;[].every(fn)&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; because "all zero elements satisfy the condition" is vacuously true. But &lt;code&gt;[].some(fn)&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; because "at least one of zero elements satisfies the condition" is false - there are no elements to satisfy anything.&lt;/p&gt;

&lt;p&gt;My one-word fix would have broken the entire permissions page. No search query → no users visible → confused admins pinging me on Slack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix Loop
&lt;/h2&gt;

&lt;p&gt;I asked Claude Code to look at the review comment. It immediately understood the severity, implemented the fix - a simple guard clause:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;searchTerms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
&lt;span class="nx"&gt;searchTerms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it amended the commit, pushed, and we waited for another build cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Lessons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Test as early as you can - it saves time and tokens.&lt;/strong&gt;&lt;br&gt;
Of course we'd test before merging. But a unit test written &lt;em&gt;before&lt;/em&gt; pushing would have caught this instantly, saving a full build cycle, a review round-trip, another fix, and another build. In the agentic world, tests aren't just for correctness - they're a feedback loop for your AI agents too. A single test case with an empty search string would have told Claude Code "you're not done yet" before it ever opened a PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. AI code review is a gift - but you have to unwrap it.&lt;/strong&gt;&lt;br&gt;
Copilot understood exactly what I was trying to build - I had a clear commit message explaining the motivation. It simply caught the edge case I missed. Without that review, I would have deployed to preview, seen an empty user list, and spent time debugging what went wrong. Instead, I got a precise comment pointing to the exact problem before I even opened the preview URL. That's time saved, frustration avoided, and a bug caught with a clear explanation - no debugging required on my end. But here's the thing: Copilot hid this critical catch behind a "low confidence" collapse. The bug-saving review was one click away from being invisible. AI review is only as good as your willingness to engage with it - even the parts it's not sure about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The "single-shot" dream isn't here yet - but the pieces are.&lt;/strong&gt;&lt;br&gt;
Here's what actually happened: Copilot found the bug → I read the comment → I asked Claude Code read it and fix it → Claude Code fixed, committed, and pushed → I waited for another review cycle.&lt;/p&gt;

&lt;p&gt;Here's what I &lt;em&gt;wish&lt;/em&gt; happened: Copilot finds the bug → Claude Code automatically reads the review, evaluates severity, implements the fix, pushes a new version → I review the final result.&lt;/p&gt;

&lt;p&gt;No human in the loop &lt;em&gt;between&lt;/em&gt; the AI reviewer and the AI implementer. Let them iterate and converge, then bring me in to approve the final version. Why am I the message bus between two AI systems that could talk to each other?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;The entire feature - from idea to shipped fix - involved changing about 10 lines of code. But it touched a real workflow pain point, introduced a subtle bug through a trivial change, and was caught and fixed by an AI pipeline where I was mostly orchestrating rather than coding.&lt;/p&gt;

&lt;p&gt;This is what AI-first development actually looks like today: not a single magical prompt that produces perfect code, but a tight loop of human intent → AI implementation → AI review → AI fix → human approval. The gap we need to close isn't in the AI's capability - it's in the connective tissue between the agents.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Pass Context to Pydantic Validators</title>
      <dc:creator>David Shimon</dc:creator>
      <pubDate>Sat, 14 Feb 2026 20:33:46 +0000</pubDate>
      <link>https://dev.to/david_shimon/how-to-pass-context-to-pydantic-validators-3jgo</link>
      <guid>https://dev.to/david_shimon/how-to-pass-context-to-pydantic-validators-3jgo</guid>
      <description>&lt;p&gt;I had a function that needed to behave differently depending on who was calling it. Not &lt;em&gt;what&lt;/em&gt; it does, just whether it should log errors or stay quiet.&lt;/p&gt;

&lt;p&gt;Sounds simple. It wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;We have a phone number normalization function. It takes messy phone numbers from various data sources and formats them consistently. Sometimes the numbers are garbage (missing digits, wrong format, random text). When that happens, the function returns the original value and moves on.&lt;/p&gt;

&lt;p&gt;The function is wired into our Pydantic models as a &lt;code&gt;BeforeValidator&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeforeValidator&lt;/span&gt;

&lt;span class="n"&gt;PhoneNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BeforeValidator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalize_phone_number&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means Pydantic calls it automatically whenever it constructs a model that has a phone number field. We don't control when or how it's called. Pydantic just passes the value and expects a value back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;This function runs in two very different contexts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;API requests&lt;/strong&gt; - serving data to users. Bad phone numbers are expected (the source data might be messy). Logging every invalid number here would flood our logs with noise on every request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data validation pipeline&lt;/strong&gt; - checking new data files before deploying them. Here we &lt;em&gt;want&lt;/em&gt; to see every invalid number so we can catch problems early.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same function. Same Pydantic model. Two different needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Obvious Ideas (and Why They Don't Work)
&lt;/h2&gt;

&lt;p&gt;My first thought was the simplest one: just add a parameter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_phone_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log_errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this function is a Pydantic &lt;code&gt;BeforeValidator&lt;/code&gt;. Pydantic calls it with &lt;code&gt;(value)&lt;/code&gt;. You can't pass extra arguments. The signature is fixed.&lt;/p&gt;

&lt;p&gt;OK, what about a global flag?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_log_errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enable_logging&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_log_errors&lt;/span&gt;
    &lt;span class="n"&gt;_log_errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works if you only handle one request at a time. But in FastAPI, multiple requests run concurrently. If Request A sets the flag to &lt;code&gt;True&lt;/code&gt;, Request B (running at the same time) will see it too - even though Request B never asked for logging.&lt;/p&gt;

&lt;p&gt;Global variables are shared across all concurrent requests. That's not what we want.&lt;/p&gt;

&lt;p&gt;What about two separate functions? One that logs, one that doesn't?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_phone_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# no logging
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_phone_number_with_logging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# with logging
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I'm duplicating the normalization logic. And since the function is bound to the Pydantic model through &lt;code&gt;BeforeValidator&lt;/code&gt;, I'd need a separate model just for the validation pipeline. That's a lot of ceremony for a logging flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter ContextVar
&lt;/h2&gt;

&lt;p&gt;Python has a module called &lt;code&gt;contextvars&lt;/code&gt; in the standard library (since 3.7). It gives you variables that are isolated per execution context. When you set a &lt;code&gt;ContextVar&lt;/code&gt; value in one async task, other tasks running concurrently don't see it. Each gets its own copy.&lt;/p&gt;

&lt;p&gt;Here's what I ended up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;contextmanager&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextvars&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;_validation_logging_enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validation_logging_enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enable_validation_logging&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_validation_logging_enabled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_validation_logging_enabled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_validation_error_if_enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_validation_logging_enabled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole module. No dependencies beyond the standard library.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Fits Together
&lt;/h2&gt;

&lt;p&gt;The normalization function calls the conditional logger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_phone_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;phonenumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;NumberParseException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;log_validation_error_if_enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid phone number format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data validation pipeline wraps its work in the context manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_incoming_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;enable_validation_logging&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Check a each data record through a Pydantic model
&lt;/span&gt;&lt;span class="gp"&gt;    ...&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That's it. Inside the &lt;code&gt;with&lt;/code&gt; block, errors get logged. Outside of it (like during API requests), they don't. No parameters to thread through the call chain, no global state to worry about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Token Pattern
&lt;/h2&gt;

&lt;p&gt;One detail worth understanding: the &lt;code&gt;set()&lt;/code&gt; / &lt;code&gt;reset(token)&lt;/code&gt; pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_validation_logging_enabled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ... do work ...
&lt;/span&gt;&lt;span class="n"&gt;_validation_logging_enabled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you call &lt;code&gt;set()&lt;/code&gt;, you get back a token. When you call &lt;code&gt;reset(token)&lt;/code&gt;, it restores the &lt;em&gt;previous&lt;/em&gt; value. Not &lt;code&gt;False&lt;/code&gt;. The previous value. This means if someone nests two &lt;code&gt;enable_validation_logging()&lt;/code&gt; calls, it still works correctly. The inner one resets to &lt;code&gt;True&lt;/code&gt; (what the outer one set), not to &lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Wrapping this in a &lt;code&gt;try/finally&lt;/code&gt; means cleanup happens even if an exception is thrown.&lt;/p&gt;

&lt;p&gt;Starting from Python 3.14, tokens can be used directly as context managers, so you can skip the wrapper entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_validation_logging_enabled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;with&lt;/code&gt; block calls &lt;code&gt;reset(token)&lt;/code&gt; for you on exit. If you're on 3.14+, this removes the need for a custom context manager.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing It
&lt;/h2&gt;

&lt;p&gt;The tests verify both sides of the behavior. We use pytest's &lt;code&gt;caplog&lt;/code&gt; fixture, which captures log records so we can assert whether a message was emitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestLogPhoneNumberErrors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_no_logging_outside_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caplog&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;normalize_phone_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not a phone number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;caplog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_logs_error_inside_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caplog&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;enable_validation_logging&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="nf"&gt;normalize_phone_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not a phone number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid phone number format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;caplog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_context_manager_resets_after_exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caplog&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;enable_validation_logging&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
        &lt;span class="nf"&gt;normalize_phone_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;another invalid number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;caplog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default is &lt;code&gt;False&lt;/code&gt; (silent). You opt in to logging. Existing code doesn't change behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trade-off
&lt;/h2&gt;

&lt;p&gt;I'll be honest about the downside: this is "action at a distance." The normalization function reads a flag it didn't receive as a parameter. If you're reading the module for the first time, you might not realize that logging is controlled by something outside the function.&lt;/p&gt;

&lt;p&gt;A good docstring helps. And for our use case (a simple boolean flag, two clear contexts), the trade-off is worth it. The alternative would be restructuring how Pydantic validation works just to pass a logging flag through.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Reach for ContextVar
&lt;/h2&gt;

&lt;p&gt;ContextVar is a good fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to pass context through layers you don't control (like Pydantic validators, middleware, or third-party callbacks)&lt;/li&gt;
&lt;li&gt;You're in an async environment where global state isn't safe&lt;/li&gt;
&lt;li&gt;The context is simple (a flag, a request ID, a correlation token)&lt;/li&gt;
&lt;li&gt;The context is for cross-cutting concerns (logging, tracing, monitoring), not business logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not the right tool for complex state management. If you need to collect errors into a list or build up rich context, there are better patterns. But for "should this code path behave slightly differently right now?" it's exactly right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The whole change was a small new module and a few test cases. No new dependencies. The data validation pipeline now catches bad phone numbers early, and API request logs stay clean.&lt;/p&gt;

&lt;p&gt;Sometimes the right solution isn't a new library or a clever architecture. Sometimes it's a stdlib module you haven't used before.&lt;/p&gt;

&lt;p&gt;Have you used ContextVar in your projects? I'd like to hear about your use cases in the comments.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>pydantic</category>
      <category>backend</category>
    </item>
    <item>
      <title>How AI Agents Changed What I Write About (And Why You Should Still Care About Git)</title>
      <dc:creator>David Shimon</dc:creator>
      <pubDate>Mon, 02 Feb 2026 11:29:33 +0000</pubDate>
      <link>https://dev.to/david_shimon/how-ai-agents-changed-what-i-write-about-and-why-you-should-still-care-about-git-5g72</link>
      <guid>https://dev.to/david_shimon/how-ai-agents-changed-what-i-write-about-and-why-you-should-still-care-about-git-5g72</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;The Moment Everything Changed&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Last week, I was working with Claude Code on a feature. We were in flow - the kind where you're solving problems faster than you can type. The code was good, the tests passed, and we had a commit ready to go.&lt;/p&gt;

&lt;p&gt;Then I looked at the commit message.&lt;/p&gt;

&lt;p&gt;It was organized, clear, and listed three distinct changes we'd made.&lt;/p&gt;

&lt;p&gt;Three separate things. In one commit.&lt;/p&gt;

&lt;p&gt;I paused. I'd just written a blog post about atomic commits. About how each commit should tell one story. About how good git history makes code reviews easier and future debugging possible.&lt;/p&gt;

&lt;p&gt;And here I was, about to commit the exact kind of change I'd been preaching against.&lt;/p&gt;

&lt;p&gt;So I asked Claude Code: "Can you split this into three atomic commits?"&lt;/p&gt;

&lt;p&gt;And it did. Perfectly. Exactly how I would have done it manually.&lt;/p&gt;

&lt;p&gt;That's when the doubt crept in.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;"So Why Did I Even Write This Post?"&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For the first time in 20 years as a developer, I'd been writing technical blog posts. The rise of LLMs gave me the confidence to write in English. I found myself explaining things I knew well, and that's how I discovered good topics.&lt;/p&gt;

&lt;p&gt;Three of my posts were about git. About how to structure commits so they have value. So they help whoever reviews the changes and whoever reads the code in the future.&lt;/p&gt;

&lt;p&gt;But now, staring at Claude Code's perfectly split commits, I felt my motivation drain away.&lt;/p&gt;

&lt;p&gt;Why does it matter that I know all the tools, shortcuts, and tricks, in a world where I can just ask the agent to do the work?&lt;/p&gt;

&lt;p&gt;There are already companies where nobody writes code anymore. People are shipping to production without even looking at what was generated.&lt;/p&gt;

&lt;p&gt;What's the value in writing posts that don't directly address how to work with LLMs?&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Answer Was Staring Me in the Face&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Then it hit me.&lt;/p&gt;

&lt;p&gt;Claude Code split those commits perfectly &lt;strong&gt;because I asked it to&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It didn't look at that three-part commit message and think "hmm, this should probably be three commits." It would have happily pushed that large commit if I hadn't intervened.&lt;/p&gt;

&lt;p&gt;I was the one who recognized there was a problem. I was the one who knew what good commits look like. I was the one who could evaluate whether the split was done well.&lt;/p&gt;

&lt;p&gt;The agent didn't replace my knowledge.It amplified it.&lt;/p&gt;

&lt;p&gt;All those posts I'd written about git? That's what gave me the ability to spot the issue and know how to fix it, even through a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;From Developer Instructions to Agent Instructions&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;After working with Claude Code a few more times, I did something interesting: I created a skill that encodes the principles of atomic commits directly. Now, every time the agent creates a commit, it automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asks itself whether it's atomic
&lt;/li&gt;
&lt;li&gt;Ensures it has a clear message
&lt;/li&gt;
&lt;li&gt;Splits it if needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But here's the thing: &lt;strong&gt;I couldn't have created that skill without understanding the principles&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The skill didn't invent the rules. It encodes what I already knew, what I wrote about in my post on &lt;a href="https://dev.to/david_shimon/the-madman-architect-carpenter-and-judge-turning-git-commits-into-stories-19b3"&gt;The Madman, Architect, Carpenter, and Judge&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is exactly what I mean when I say knowledge has shifted from "instructions for developers" to "instructions for agents." Same principles, same importance. Only the execution has changed.&lt;/p&gt;

&lt;p&gt;And even with the skill in place, I still need to check. The agent isn't perfect. Sometimes I need to correct or redirect it. &lt;strong&gt;That requires the same understanding&lt;/strong&gt; that would have been needed to write the code myself. I'm just using it for review and direction instead of manual implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Calculator Analogy&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We all have calculators in our pockets. But we still learn multiplication tables by heart. We still learn long division with paper and pencil. We understand the calculations.&lt;/p&gt;

&lt;p&gt;Not because we want to be faster than the calculator.&lt;/p&gt;

&lt;p&gt;We learn it because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It helps us understand what the calculator is doing
&lt;/li&gt;
&lt;li&gt;It gives us intuition for when a result looks wrong
&lt;/li&gt;
&lt;li&gt;It lets us estimate results mentally for simple cases
&lt;/li&gt;
&lt;li&gt;It helps us know the meaning of the calculation, and when we should use it
&lt;/li&gt;
&lt;li&gt;It's the foundation for understanding more advanced concepts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same is true for &lt;code&gt;git add --patch&lt;/code&gt;, or any other technical skill. You don't need to be faster than Claude Code. But you should know what it's doing under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Two Futures&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;There's both opportunity and risk here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Opportunity:&lt;/strong&gt; If you understand the principles, AI agents make you super-productive. You can ask "split this into atomic commits" and get a perfect result. You can focus on the &lt;em&gt;why&lt;/em&gt; and the &lt;em&gt;what&lt;/em&gt;, while the AI handles the &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Risk:&lt;/strong&gt; If you don't understand the principles, you'll be dependent on the AI's defaults. You'll get large commits because you didn't know to ask for something different. You'll get code that works but isn't readable. You'll ship to production without looking, and discover problems only when they explode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Reality:&lt;/strong&gt; To use these tools well, you need to understand what's happening under the hood. To know &lt;strong&gt;what to ask for&lt;/strong&gt;, &lt;strong&gt;when the result doesn't look right&lt;/strong&gt;, and &lt;strong&gt;how to fix it&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What This Means for Writing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This changes what technical blog posts should be.&lt;/p&gt;

&lt;p&gt;No more guides on "how to run &lt;code&gt;git rebase -i&lt;/code&gt; step by step." Instead, posts about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt; atomic commits matter (not how to create them)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt; makes code readable vs just working (not how to write it)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And yes, still some “&lt;strong&gt;how”&lt;/strong&gt;. A peek under the hood. Not a tutorial, but enough understanding so you know what's happening. Like learning long division even though you have a calculator.&lt;/p&gt;

&lt;p&gt;Because in a world where AI writes the code, our job is to know what to ask for, why it matters, and how to check that we got what we wanted.&lt;/p&gt;

&lt;p&gt;In the past, this knowledge was implicit in our technical skills. Now it needs to be explicit.&lt;/p&gt;

&lt;p&gt;And that's exactly what this blog will do going forward: show you what's possible, why it matters, and enough "how" so you understand what's happening.&lt;/p&gt;

&lt;p&gt;Then you can make sure it happens for you too - whether you do it yourself, or guide an AI agent to do it for you.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>git</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Use Git Shortcuts - Because Your Fingers Deserve Better</title>
      <dc:creator>David Shimon</dc:creator>
      <pubDate>Wed, 10 Dec 2025 17:09:20 +0000</pubDate>
      <link>https://dev.to/david_shimon/use-git-shortcuts-because-your-fingers-deserve-better-57ao</link>
      <guid>https://dev.to/david_shimon/use-git-shortcuts-because-your-fingers-deserve-better-57ao</guid>
      <description>&lt;p&gt;If you're running &lt;code&gt;git checkout -b feature/new-thing&lt;/code&gt; dozens of times a day, you're wasting precious seconds and mental energy. There's a better way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Command Line?
&lt;/h2&gt;

&lt;p&gt;I work with git from the command line. Not (only) because I'm old fashioned, but because once you know the shortcuts, it's faster than reaching for your mouse. My IDE has a great integrated terminal, and with the right aliases, I can navigate git in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Secret Weapon: Oh My Zsh Git Plugin
&lt;/h2&gt;

&lt;p&gt;I didn't invent most of the shortcuts I use. I use them from the git plugin of Oh My Zsh (a popular framework for managing your Zsh configuration), which provides a huge collection of aliases and functions that make working with git much faster.&lt;/p&gt;

&lt;p&gt;My team members also installed the plugin. Now we speak the same language - when I say "just &lt;code&gt;gpsup&lt;/code&gt; it," they know exactly what I mean. We can share tips, and when pair programming, the muscle memory transfers seamlessly between keyboards.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"But I'll Never Remember These Cryptic Shortcuts!"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I hear you. &lt;code&gt;gpsup&lt;/code&gt; looks like alphabet soup at first. But here's the thing: they're surprisingly logical once you see the pattern. Most shortcuts follow a simple formula: g (git) + first letters of the command. So gcb = git checkout -b, gco = git checkout, gcmsg = git commit -m (msg). Your brain picks up the pattern faster than you'd think.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Enable the Plugin
&lt;/h2&gt;

&lt;p&gt;The git plugin comes bundled with Oh My Zsh, but you need to enable it in your &lt;code&gt;.zshrc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;git&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Start Simple: The Essential Shortcuts
&lt;/h2&gt;

&lt;p&gt;Don't try to learn everything at once. Start with the commands you use most often. Like the legend about Harvard's paths that were paved in the ways students walked the most. Here's how to find them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See your most-used git commands&lt;/span&gt;
&lt;span class="nb"&gt;history&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;git | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2, $3}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check what you use most often and find a shortcut for that command, and get used to using it. &lt;/p&gt;

&lt;p&gt;For most developers, these basics cover 80% of daily work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gst&lt;/code&gt; - &lt;code&gt;git status&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gco&lt;/code&gt; - &lt;code&gt;git checkout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gcb&lt;/code&gt; - &lt;code&gt;git checkout -b&lt;/code&gt; create new branch&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gp&lt;/code&gt; - &lt;code&gt;git push&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gl&lt;/code&gt; - &lt;code&gt;git pull&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ga&lt;/code&gt; - &lt;code&gt;git add&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gcmsg&lt;/code&gt; - &lt;code&gt;git commit -m&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;glog&lt;/code&gt; - beautiful git log with graph&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Pro tip:&lt;/em&gt; Wondering what a shortcut actually does? Type &lt;code&gt;which &amp;lt;shortcut&amp;gt;&lt;/code&gt; in your terminal to see the full command.&lt;/p&gt;

&lt;p&gt;Once you get used to these shortcuts, they become muscle memory. You stop thinking about git syntax and stay focused on your code. That's the real win - maintaining flow instead of context-switching to remember command flags.&lt;/p&gt;

&lt;p&gt;Commands I mentioned in my previous posts about git:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gapa&lt;/code&gt; - &lt;code&gt;git add --patch&lt;/code&gt; interactively choose which changes to stage from each file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grbi&lt;/code&gt; - &lt;code&gt;git rebase -i&lt;/code&gt; interactive rebase&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gc!&lt;/code&gt; - &lt;code&gt;git commit -v --amend&lt;/code&gt;. Forgot to include a file in your last commit? Made a typo in the commit message? This fix your last commit without creating a new one. No more "oops, fix typo" commits cluttering your history&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Favorite Power Shortcuts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;gpsup&lt;/code&gt; - No more copying error messages when pushing a branch for the first time. This shortcut runs &lt;code&gt;git push --set-upstream origin $(current_branch)&lt;/code&gt; automatically. One command, done.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;gwip&lt;/code&gt; and &lt;code&gt;gunwip&lt;/code&gt; - Perfect for when you need to quickly switch contexts without losing work. &lt;code&gt;gwip&lt;/code&gt; creates a Work In Progress commit with all your changes, and &lt;code&gt;gunwip&lt;/code&gt; removes it when you're ready to make a proper commit. Great for handling urgent bugs mid-feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;gbda&lt;/code&gt; - Clean up 10 merged branches in one command instead of typing &lt;code&gt;git branch -d&lt;/code&gt; ten times. After a sprint with multiple feature branches merged, this is a lifesaver.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;gsta&lt;/code&gt; and &lt;code&gt;gstp&lt;/code&gt; - Stash shortcuts for when you need to quickly save work and come back to it. Much faster than typing out &lt;code&gt;git stash&lt;/code&gt; and &lt;code&gt;git stash pop&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Real Workflow Example
&lt;/h2&gt;

&lt;p&gt;Here's my typical feature branch workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcb new-feature          &lt;span class="c"&gt;# Create and switch to new branch&lt;/span&gt;
&lt;span class="c"&gt;# ... do some work ...&lt;/span&gt;
gapa                     &lt;span class="c"&gt;# Interactively stage changes&lt;/span&gt;
gcmsg &lt;span class="s2"&gt;"Add new feature"&lt;/span&gt;  &lt;span class="c"&gt;# Commit with message&lt;/span&gt;
gpsup                    &lt;span class="c"&gt;# Push and set upstream&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to typing it all out manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; new-feature
&lt;span class="c"&gt;# ... do some work ...&lt;/span&gt;
git add &lt;span class="nt"&gt;--patch&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add new feature"&lt;/span&gt;
git push &lt;span class="nt"&gt;--set-upstream&lt;/span&gt; origin new-feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shortcuts aren't just faster - they let you stay in the zone instead of wrestling with git syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning Hidden Git Gems
&lt;/h2&gt;

&lt;p&gt;A bonus of using the plugin: you'll discover git commands and flags you didn't know about. The plugin authors have made thoughtful choices about safer alternatives and useful flags.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;gpf&lt;/code&gt; isn't just &lt;code&gt;git push --force&lt;/code&gt; - it's actually &lt;code&gt;git push --force-with-lease&lt;/code&gt; (or &lt;code&gt;git push --force-with-lease --force-if-includes&lt;/code&gt; on newer Git versions), which is a safer alternative that prevents you from accidentally overwriting someone else's work. If you really need the dangerous &lt;code&gt;--force&lt;/code&gt;, that's &lt;code&gt;gpf!&lt;/code&gt; with an exclamation mark.&lt;/p&gt;

&lt;p&gt;Another example: even the basic &lt;code&gt;gc&lt;/code&gt; (commit) uses the &lt;code&gt;-v&lt;/code&gt; (verbose) flag. It runs &lt;code&gt;git commit -v&lt;/code&gt;, which shows you the actual diff of your changes at the bottom of the commit message editor. You can review what you're committing while writing the message - a small detail that makes a big difference in writing accurate commit messages.&lt;/p&gt;

&lt;p&gt;If you want to learn about other useful flags on git commands, e.g. “git log”, try &lt;code&gt;alias | grep "git log"&lt;/code&gt;, read about them, try them, and adopt what you like. Maybe &lt;code&gt;glols&lt;/code&gt; is your preferred way to see your git log? It's like git log but prettier and includes human readable relative time and which files were changed per commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Your Own Custom Aliases
&lt;/h2&gt;

&lt;p&gt;While the Oh My Zsh git plugin is incredibly comprehensive, you might find yourself wanting shortcuts for workflows it doesn't cover. Here's one I added that's become essential to my workflow:&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;fixup&lt;/code&gt; Alias
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fixup&lt;/code&gt; - &lt;code&gt;git commit --fixup&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add this to your &lt;code&gt;~/.oh-my-zsh/custom/aliases.zsh&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;alias fixup='git commit --fixup'&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;This is a game-changer for cleaning up commits during code review. When reviewers point out issues in specific commits, instead of creating "fix typo" or "address review comments" commits, I use &lt;code&gt;fixup&lt;/code&gt; to mark them for automatic squashing during an interactive rebase with &lt;code&gt;--autosquash&lt;/code&gt;. It keeps your commit history clean without the manual work of reordering commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Questions
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"What if I'm on a machine without my aliases?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is real pain. My solution: keep the most critical commands in muscle memory both ways. I can still type git status when needed, but gst is my default.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do I see all available aliases?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Run &lt;code&gt;alias | grep git&lt;/code&gt; to see everything. It's a long list! Or check the &lt;a href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/git" rel="noopener noreferrer"&gt;official plugin documentation&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can I customize existing aliases?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes! Add your overrides to &lt;code&gt;~/.oh-my-zsh/custom/aliases.zsh&lt;/code&gt;. Your custom aliases take precedence over plugin defaults.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Should I learn the full commands first?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Honestly? Yes. Understanding what &lt;code&gt;git rebase -i&lt;/code&gt; does makes &lt;code&gt;grbi&lt;/code&gt; powerful instead of magical. The shortcuts are a speed layer on top of solid fundamentals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Git shortcuts aren't just about saving keystrokes - they're about staying in flow, reducing context switching, and making git feel less like a chore.&lt;/p&gt;

&lt;p&gt;Give it a try. Your future self (and your fingers) will thank you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What are your favorite git shortcuts? Share them in the comments!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>productivity</category>
      <category>zsh</category>
      <category>terminal</category>
    </item>
    <item>
      <title>The Madman, Architect, Carpenter, and Judge: Turning Git Commits Into Stories</title>
      <dc:creator>David Shimon</dc:creator>
      <pubDate>Sat, 06 Dec 2025 18:53:32 +0000</pubDate>
      <link>https://dev.to/david_shimon/the-madman-architect-carpenter-and-judge-turning-git-commits-into-stories-19b3</link>
      <guid>https://dev.to/david_shimon/the-madman-architect-carpenter-and-judge-turning-git-commits-into-stories-19b3</guid>
      <description>&lt;p&gt;We've all done it: make a bunch of changes, then commit everything with a vague message like “fix issues”. But what if we approached Git commits like a writer approaches a manuscript?&lt;/p&gt;

&lt;p&gt;When I work on a large change, my commits aren't "pretty" on the first attempt. Still, I invest the time to organize them. I break the changes into atomic pieces - each one doing a single thing that's very easy to understand and review. For example: in the first commit, add a new type of constraint; in the second commit, add a negative constraint; in the third commit, modify one message to use both new constraints; and in the fourth commit, update another message that now needs to use the new constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Four Roles of Writing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Betty S. Flowers, Emerita Professor of English at the University of Texas at Austin, &lt;a href="https://www.ut-ie.com/b/b_flowers.html" rel="noopener noreferrer"&gt;wrote&lt;/a&gt; about roles in the writing process, offering a solution to writer's block by separating the creative and critical aspects of writing. She identified four roles that participate in the writing process, and to avoid getting stuck, they shouldn't "talk" to each other while we're writing. Instead, we need to "wear" a different hat at each stage so each can work without interference. The stages she identifies are: the Madman, the Architect, the Carpenter, and the Judge. If you want to write, I recommend reading her article to understand what each role does at each stage of writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Applying This to Git Commits&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I borrow her idea from prose writing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage One - The Madman&lt;/strong&gt; writes the code without thinking too much about who will read it. Fixes what needs fixing, adds what needs adding, checks that everything works and does what it should, without too much self-control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage Two - The Architect&lt;/strong&gt; comes to organize all the code the Madman wrote. They find the logic in the madness, identify the different stages, separate into the different parts that create the commits that tell the story of the change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage Three - The Carpenter&lt;/strong&gt; organizes each individual commit - ensures it contains exactly what it should, chooses an appropriate title, and details in the comment about the essence of the change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage Four - The Judge&lt;/strong&gt; examines the finished work - plays the role of the reviewer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6cuskzfhy2j866bpwwah.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6cuskzfhy2j866bpwwah.png" alt="The Four Roles of Clean Git Commits" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why Is This Worth the Investment?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is essentially self code review. As a code reviewer, it's much easier to review a pull request that's divided into clear, well-defined, and short commits. But beyond hoping I'm making my reviewers' lives easier, the main value of my work is that I'm doing a review for myself. I need to explain the change to myself - why I did each thing and how - in order to build the pull request this way. The additional pass over the recently written code, this time with different eyes - looking for the different stages in the change, the order and logic - helps me improve my code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Concrete Benefits
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Faster, better code reviews&lt;/strong&gt;: Reviewers can understand and approve changes faster when they can review one logical change at a time. Instead of facing a wall of changes, they can follow your thought process step by step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Easier debugging&lt;/strong&gt;: When something breaks, &lt;code&gt;git bisect&lt;/code&gt; becomes a powerful tool. With atomic commits, you can quickly pinpoint which specific change introduced a bug. Compare this to commits that bundle multiple unrelated changes - bisect becomes nearly useless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safer reverts&lt;/strong&gt;: If you need to roll back a change, atomic commits let you revert precisely what's broken without losing good work. You can revert just the third commit while keeping the first two, rather than throwing away an entire day's work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better documentation&lt;/strong&gt;: Six months from now, when you (or a teammate) run &lt;code&gt;git blame&lt;/code&gt; to understand why something works the way it does, a well-crafted commit with a clear message tells the story. The commit history becomes living documentation that explains not just &lt;em&gt;what&lt;/em&gt; changed, but &lt;em&gt;why&lt;/em&gt;. It's like having a time machine. I can go back and ask the writer (usually my past self) why this change was done, and get a clear answer instead of a shrug.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdutslpvcp0f2c3tfah9f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdutslpvcp0f2c3tfah9f.png" alt="Time machine to go back and ask the writer why this change was done" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smoother onboarding&lt;/strong&gt;: New team members can read through your pull requests and commit history to understand how changes are structured and how features are built incrementally. It's like leaving breadcrumbs for future developers.&lt;/p&gt;

&lt;p&gt;The time invested in organizing commits pays dividends every time someone (including future you) needs to understand, debug, or modify the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Tools of the Architect and Carpenter&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once you embrace this workflow, you'll need the right tools to reshape your commits. Here are the essential commands:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Interactive Rebase: Your Primary Tool&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Interactive rebase is the Swiss Army knife for organizing commits. It allows you to rewrite history before pushing:&lt;br&gt;&lt;br&gt;
&lt;code&gt;git rebase -i HEAD~5&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
This opens an editor showing your last 5 commits, where you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reorder commits&lt;/strong&gt; - move them up or down to tell a better story
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit commits&lt;/strong&gt; - modify the code or message of a specific commit
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Squash commits&lt;/strong&gt; - combine multiple commits into one
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split commits&lt;/strong&gt; - break one large commit into smaller atomic pieces
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drop commits&lt;/strong&gt; - remove commits that aren't needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each commit in the editor is prefixed with a command (&lt;code&gt;pick&lt;/code&gt;, &lt;code&gt;reword&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;squash&lt;/code&gt;, etc.). Change the command, save the file, and Git walks you through the changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Stage Partial Changes&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Sometimes the Madman mixed multiple logical changes in a single file. The Carpenter can separate them:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git add -p&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This command presents each change in the file and asks: "Stage this hunk?" You can answer &lt;code&gt;y&lt;/code&gt; (yes), &lt;code&gt;n&lt;/code&gt; (no), &lt;code&gt;s&lt;/code&gt; (split into smaller pieces), or &lt;code&gt;e&lt;/code&gt; (manually edit). This lets you commit only the relevant parts of a file, keeping each commit focused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example workflow&lt;/strong&gt;: You fixed a bug and also renamed a variable in the same file. Use &lt;code&gt;git add -p&lt;/code&gt; to stage only the bug fix for one commit, then stage the rename for a separate commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Quick Fixes with --fixup&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;When the Judge or the real reviewer find issues during review, create fixup commits:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git commit --fixup &amp;lt;commit-hash&amp;gt;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This creates a commit labeled &lt;code&gt;fixup! Original commit message&lt;/code&gt;. Later, when you run &lt;code&gt;git rebase -i --autosquash&lt;/code&gt;, Git automatically merges these fixup commits into their targets. It's perfect when you're polishing individual commits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip&lt;/strong&gt;: Make autosquash the default behavior so you don't have to remember the flag:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git config --global rebase.autosquash true&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Clean commits aren't just about being a good team player—they're about being kind to your future self. The time you invest in organizing your commits today pays back every time you need to debug an issue, understand why a decision was made, or safely revert a change.&lt;/p&gt;

&lt;p&gt;Start small: try this approach on your next pull request. Put on the Madman's hat and write your code freely. Then become the Architect and find the story in your changes. Let the Carpenter polish each commit. Finally, be the Judge and review your own work.&lt;/p&gt;

&lt;p&gt;Your reviewers will thank you. Your future self will thank you. And you might just write better code along the way.&lt;/p&gt;

</description>
      <category>git</category>
      <category>commit</category>
    </item>
    <item>
      <title>How to Fix a Commit Message</title>
      <dc:creator>David Shimon</dc:creator>
      <pubDate>Thu, 20 Nov 2025 07:02:51 +0000</pubDate>
      <link>https://dev.to/david_shimon/how-to-fix-a-commit-message-14ec</link>
      <guid>https://dev.to/david_shimon/how-to-fix-a-commit-message-14ec</guid>
      <description>&lt;p&gt;Good commit messages are important for improving collaboration, speeding up debugging, documenting a project's history, and simplifying code reviews.&lt;/p&gt;

&lt;p&gt;But sometimes we make typos in our commit messages because we are, for now, human. Sometimes the message we wrote isn't clear or detailed enough. In any case, sometimes we want to change the message we wrote for a commit.&lt;/p&gt;

&lt;p&gt;In this post, we'll go over two simple ways to change a commit message to make it clearer, more organized, more accurate, correct, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For example, you might want to change:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Vague message&lt;/span&gt;
fix bug

&lt;span class="c"&gt;# To something more descriptive&lt;/span&gt;
Resolve null pointer exception &lt;span class="k"&gt;in &lt;/span&gt;user login validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Case 1: Fixing the Last Commit Message
&lt;/h2&gt;

&lt;p&gt;We want to fix the message of the last commit we made. The command &lt;code&gt;git commit --amend&lt;/code&gt; allows us to change the last commit. &lt;/p&gt;

&lt;p&gt;Make sure there are no changes you've added to staging with &lt;code&gt;git add&lt;/code&gt;, because we only want to change the message, not the files in the commit (I'll cover changing commit content in a future post). &lt;/p&gt;

&lt;p&gt;After running &lt;code&gt;git commit --amend&lt;/code&gt;, an editor will open and allow us to change the message. Edit, save, &lt;a href="https://stackoverflow.com/questions/11828270/how-do-i-exit-vim" rel="noopener noreferrer"&gt;exit&lt;/a&gt; - and we're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: Fixing an Older Commit Message
&lt;/h2&gt;

&lt;p&gt;What happens if we want to change the message of an older commit? One we made four commits ago, for example, and now we've decided we want to change it?&lt;/p&gt;

&lt;p&gt;The command we'll use this time is interactive rebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rebase &lt;span class="nt"&gt;-i&lt;/span&gt; &amp;lt;base&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;code&gt;base&lt;/code&gt; is a commit in the branch history that comes before the commit we want to change. For example, to modify the last three commits, you would use &lt;code&gt;HEAD~3&lt;/code&gt;. If all your branch commits are after the main branch, use &lt;code&gt;git rebase -i main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An editor will open showing all the commits we've made since the base commit, and will allow us to make changes to their history. Before each commit there's a command that tells Git what we want to do with this commit during the rebase.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pick 4698cbda &lt;span class="c"&gt;# Change post header&lt;/span&gt;
pick 2f95451c &lt;span class="c"&gt;# Add cover image&lt;/span&gt;
pick fdae7c1b &lt;span class="c"&gt;# Comit message with typo we want to fix&lt;/span&gt;

&lt;span class="c"&gt;# Rebase 517de4f8..fdae7c1b onto 517de4f8 (3 commands)&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Commands:&lt;/span&gt;
&lt;span class="c"&gt;# p, pick &amp;lt;commit&amp;gt; = use commit&lt;/span&gt;
&lt;span class="c"&gt;# r, reword &amp;lt;commit&amp;gt; = use commit, but edit the commit message&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command we're interested in for this post is &lt;code&gt;reword&lt;/code&gt; - which says: use the commit but edit its message. We'll find the commit (or commits) we want to change, and replace the default &lt;code&gt;pick&lt;/code&gt; command (which means use the commit as is) that appears before it with &lt;code&gt;reword&lt;/code&gt;, or just the letter &lt;code&gt;r&lt;/code&gt; for short.&lt;/p&gt;

&lt;p&gt;When we save and exit the editor, the rebase will start executing the commands, and will open an editor with the previous message of the commit that we can change, save and exit until the rebase completes successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚠️ Warning: You're Rewriting History
&lt;/h2&gt;

&lt;p&gt;In both cases, we're changing history. Even though it's seemingly just a message change, it actually creates a new commit. Therefore, don't do this on branches that are shared with other people and not with commits that have already been merged into main, because it will create a mess for the team. Do this on a feature branch you're working on alone, and that hasn't been merged yet.&lt;/p&gt;

&lt;p&gt;If you've already pushed the branch to origin, you won't be able to push it again simply, because you've changed the commits that were there. To push the updated commits, you'll need to use &lt;code&gt;git push --force-with-lease&lt;/code&gt;. We're essentially saying that our local, corrected changes are what we want and we're happy to give up what's in origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Now your reviewer will be able to understand what you meant in each commit, and if in the future you need to understand why you made the change - you'll see clear and well-written messages on the commit.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>git</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Mystery of the Invisible Tab: A YAML Debugging Tale</title>
      <dc:creator>David Shimon</dc:creator>
      <pubDate>Sat, 15 Nov 2025 17:14:25 +0000</pubDate>
      <link>https://dev.to/david_shimon/the-mystery-of-the-invisible-tab-a-yaml-debugging-tale-3e3l</link>
      <guid>https://dev.to/david_shimon/the-mystery-of-the-invisible-tab-a-yaml-debugging-tale-3e3l</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Spent hours debugging why my OpenFGA CLI config file wasn't working. Turned out YAML doesn't support tabs. Good error messages matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;This week, I started using &lt;a href="https://openfga.dev/" rel="noopener noreferrer"&gt;OpenFGA&lt;/a&gt;, an open-source authorization system. It offers multiple ways to configure connection settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Command-line flags&lt;/li&gt;
&lt;li&gt;Environment variables
&lt;/li&gt;
&lt;li&gt;A YAML configuration file (&lt;code&gt;.fga.yaml&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The YAML option caught my attention. Why? Because I work with multiple OpenFGA servers. With YAML configs, I could create separate &lt;code&gt;.fga.yaml&lt;/code&gt; files in different directories and just run the CLI from the appropriate folder. Elegant!&lt;/p&gt;

&lt;p&gt;Or so I thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I created my config file, added the server URL, API token, and other settings. Ran the CLI. Nothing.&lt;/p&gt;

&lt;p&gt;The CLI completely ignored my configuration file and fell back to defaults. No errors. No warnings. Just... silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Debugging Session
&lt;/h2&gt;

&lt;p&gt;Enter debugging mode - with some LLM assistance, naturally. After lots of trial and error:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ The file path was correct&lt;/li&gt;
&lt;li&gt;✅ The YAML syntax looked valid&lt;/li&gt;
&lt;li&gt;✅ All values were properly formatted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yet somehow, it didn't work.&lt;/p&gt;

&lt;p&gt;Plot twist: I created a config file for my local environment, and &lt;strong&gt;that one worked perfectly&lt;/strong&gt;. Same format, same structure, different values. Very strange!&lt;/p&gt;

&lt;p&gt;At this point, I cloned the OpenFGA CLI repository, ready to hunt down this mysterious bug in the source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breakthrough
&lt;/h2&gt;

&lt;p&gt;I asked Cursor to generate a sample config file. After replacing the values with my own, this generated file worked flawlessly.&lt;/p&gt;

&lt;p&gt;Time for a &lt;code&gt;diff&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diff working-config.yaml my-config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output? Two "blank lines" at the end of my file. I deleted those lines, and suddenly everything worked.&lt;/p&gt;

&lt;p&gt;But wait - if I added blank lines back to the working file, it continued to work fine. What was going on?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Culprit
&lt;/h2&gt;

&lt;p&gt;Closer inspection revealed the truth: those weren't blank lines. One of them contained a &lt;strong&gt;tab character&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I discovered that YAML doesn't support tabs. In fact, it's &lt;a href="https://yaml.org/faq.html" rel="noopener noreferrer"&gt;FAQ #2 on yaml.org&lt;/a&gt; (right after "What's the official file extension?").&lt;/p&gt;

&lt;p&gt;Mystery solved. The culprit found. And as usual in these stories: it was me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lesson: Error Messages Matter
&lt;/h2&gt;

&lt;p&gt;Here's what bothered me most about this experience: &lt;strong&gt;the CLI found my config file but silently ignored it when parsing failed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If a configuration file exists but contains invalid syntax, the tool should tell me! A simple error message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Failed to parse configuration file '.fga.yaml'
YAML parsing error: tabs are not allowed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's why I &lt;a href="https://github.com/openfga/cli/issues/608" rel="noopener noreferrer"&gt;opened an issue&lt;/a&gt; suggesting this improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Good error messages are crucial for Developer Experience (DX). When building CLI tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fail loudly when something is wrong&lt;/strong&gt; - Silent failures lead to frustrating debugging sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be specific&lt;/strong&gt; - "Config file not found" vs "Config file found but failed to parse"
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is especially important for configuration files that developers might edit in different environments, with different editors, and different settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  In Conclusion
&lt;/h2&gt;

&lt;p&gt;YAML doesn't like tabs. CLI tools should tell you when your config is broken. And when in doubt, blame the developer (me) first - but make it easier for developers to catch their mistakes.&lt;/p&gt;

&lt;p&gt;Have you had similar debugging experiences with invisible characters or silent failures? Share your stories in the comments!&lt;/p&gt;

</description>
      <category>openfga</category>
      <category>debugging</category>
      <category>yaml</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
