<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Bit Byte Bit]]></title><description><![CDATA[Tech, Agile, Continuous Delivery]]></description><link>https://bitbytebit.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!moJi!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbitbytebit.substack.com%2Fimg%2Fsubstack.png</url><title>Bit Byte Bit</title><link>https://bitbytebit.substack.com</link></image><generator>Substack</generator><lastBuildDate>Fri, 03 Jul 2026 07:39:53 GMT</lastBuildDate><atom:link href="https://bitbytebit.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Zarar Siddiqi]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[bitbytebit@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[bitbytebit@substack.com]]></itunes:email><itunes:name><![CDATA[Zarar Siddiqi]]></itunes:name></itunes:owner><itunes:author><![CDATA[Zarar Siddiqi]]></itunes:author><googleplay:owner><![CDATA[bitbytebit@substack.com]]></googleplay:owner><googleplay:email><![CDATA[bitbytebit@substack.com]]></googleplay:email><googleplay:author><![CDATA[Zarar Siddiqi]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Anthropic's Steganography Controversy Explained in Non-Technical Terms]]></title><description><![CDATA[Claude Code was caught hiding data in its own prompts. A plain-English look at what Anthropic did and why it breaks trust.]]></description><link>https://bitbytebit.substack.com/p/anthropics-steganography-controversy</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/anthropics-steganography-controversy</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Wed, 01 Jul 2026 16:26:51 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f33fad08-dd0b-4275-8604-d3eda0bd5008_667x514.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You may have heard that Anthropic was caught doing something sneaky which I want to explain without any of the technical stuff because the lesson is for everyone, especially if you&#8217;re paying for AI tools or deciding whether your team should use them.</p><p><a href="https://thereallo.dev/blog/claude-code-prompt-steganography">Someone looked at the internals of Claude Code</a> and found it had been quietly doing something they didn&#8217;t tell anyone about, not exactly something malicious, but the point is that it was concealed from the user.</p><p>Every time it sent a message from the user to the backend (i.e., a prompt), it made a small inconsequential change to what was sent over the wire from your computer to Anthropic. Instead of sending a date like YYYY-MM-DD, they sent it like YYYY/MM/DD (notice hyphen replaced with slash) whenever the user was from certain parts of China. They also did similar things if you were using Claude through a reseller, etc., but the details of what they sent aren&#8217;t the important point, but how they sent it.</p><p>Companies collect information about their users all the time so that part isn&#8217;t the problem. If Anthropic wanted to know who its users are, they could just ask or relay that information plainly like, <code>{"location": "Shanghai"}</code> as part of the data that is sent to them from your Claude Code to Anthropic servers. What&#8217;s bothering people is how they did it.</p><p>They own the whole chain as the tool is theirs and the servers are theirs. If they wanted this information, they could have written it down plainly, in the open, the way every normal company does. Instead they chose to hide it and to hide it inside the one part of the message a developer actually relies heavily on: the prompt. It&#8217;s like a contractor you hired scribbling notes about you in invisible ink, on the very documents they hand back to you. This is known as <em>steganography</em>, the practice of concealing secret information within an ordinary, non-secret piece of information.</p><p>You don&#8217;t hide something you&#8217;re allowed to do. You hide something when you don&#8217;t want the other person to know you did it. This is irking people because if they do something like this here, it&#8217;s hard to believe they won&#8217;t do it elsewhere as well, and it becomes harder to trust their word when it comes to security and privacy. And nobody would have known, except one person happened to take the software apart, and that&#8217;s the problem: the fact that we only found it by accident.</p><p>These AI tools aren&#8217;t little chat windows anymore. They&#8217;re agents that run on our computers with total access to your computer. They can run commands, read your files, and reach out to the internet, all on your behalf. You hand them the keys to the house because they legitimately do something useful for you, and you inherently trust them.</p><p>So let&#8217;s think about that. A company shipped software that runs on your machine with the keys to everything, and it was quietly doing something it never disclosed. The hidden mark (e.g., the hyphen/slash swapping to reveal Chinese users) itself was nothing but it proves they&#8217;re willing to run things you can&#8217;t see. And you can&#8217;t check what you can&#8217;t see. Essentially, you trust the person who shows you their work over the one who says &#8220;just trust me.&#8221; Openness earns trust and hiding loses it and this is crystal clear evidence that Anthropic went out of their way to hide it from you. As an aside, the way they hid it is so sloppy that it makes you wonder whether &#8220;big tech&#8221; developers really are what they are propped up to be.</p><p>So where does that leave us? I think it&#8217;s a real argument for running these tools on AI models you can run yourself, on your own machine, where your data and your work never leave the building. The local models (<a href="https://zarar.dev/run-a-local-coding-model-with-pi-and-lm-studio/">like the one I wrote about here</a>) aren&#8217;t quite as sophisticated as the big ones yet, and &#8220;open&#8221; doesn&#8217;t automatically mean &#8220;safe&#8221; but the direction to run more locally is the right one (without even considering the cost angle). You want to be moving toward tools you can see into.</p><p>I&#8217;m not telling anyone to throw out their tools tomorrow. I&#8217;m saying this that when you&#8217;re choosing who to trust with the keys, pay less attention to what a company promises and more to whether you can check for yourself. The ones worth trusting are the ones who don&#8217;t ask you to take their word for it.</p>]]></content:encoded></item><item><title><![CDATA[Enforcing Invariants in AI-Generated Code with ADRs and Contracts]]></title><description><![CDATA[AI-generated code looks correct but quietly breaks constraints you assumed were safe. Enforce invariants with ADRs and contracts the agent can't bypass.]]></description><link>https://bitbytebit.substack.com/p/enforcing-invariants-in-ai-generated</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/enforcing-invariants-in-ai-generated</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Tue, 30 Jun 2026 17:21:13 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f5f6dec8-f388-41a4-8627-f60fb31360bf_3840x2160.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I had earlier written about <a href="https://zarar.dev/agent-hooks-deterministic-guardrails-for-ai-generated-code/">using Hooks</a> to enforce certain rules in AI-generated code. The idea was to use deterministic checks rather than prose-based guidance which can&#8217;t be enforced at time of generation. A hook here is just a script that runs at a point in the agent&#8217;s lifecycle and can block it from continuing. The broader idea is to enforce invariants and in this post I will show how we can use:</p><ol><li><p>Classic <a href="https://github.com/architecture-decision-record/architecture-decision-record">Architectural Decision Records</a> (ADRs) to record and enforce invariants</p></li><li><p>Use the <a href="https://www.rfc-editor.org/info/rfc2119/">RFC 2119</a> keywords like SHALL and MUST to record and enforce invariants</p></li></ol><p>But first, what is an invariant? Borrowed from <a href="https://en.wikipedia.org/wiki/Domain-driven_design">Domain-Driven Design</a>, an invariant is a rule that must always hold true for your system to be in a valid state. It&#8217;s a promise the code makes to itself that this condition is never allowed to break. An LLM will produce code that looks correct yet quietly violates a rule you assumed was safe, and it has no inherent memory of the constraints your system depends on. The job, then, is to encode those invariants where the AI can&#8217;t ignore them, so that generated code is forced to honour them.</p><h2><strong>Architecture Decision Records as Invariants</strong></h2><p>Decisions you have made about your architecture can be thought of as invariants that need to be followed. As you incrementally make decisions, we need to ensure they are recorded so agents can treat them as invariants (i.e., rules to be followed). ADRs provide a structured method to do this. To take advantage of them, we need to:</p><ol><li><p>Figure out when we need to record one</p></li><li><p>Actually record it</p></li><li><p>Point agents to treat the ADRs as invariants</p></li></ol><h3><strong>Knowing when to record one</strong></h3><p>The hardest part is knowing when to record one. Architectural decisions rarely announce themselves as they slip by inside an ordinary coding session when you pick one storage approach over another, add a major dependency, introduce a new abstraction, or replace an established pattern. To catch these, I use an <a href="https://gist.github.com/Arsenalist/598adc63a039e40a255dd7938830545a">ADR auto-suggest skill</a> that watches the shape of a conversation and flags an architectural inflection point as it happens. It looks for the tell-tale signals. An &#8220;X vs Y&#8221; comparison, &#8220;replace X with Y&#8221; or &#8220;deprecate X&#8221; language, the introduction of a new system service, or any pattern-setting choice that future work will inherit. It deliberately ignores bug fixes, styling, and behaviour-preserving refactors so it doesn&#8217;t fire on noise. The skill never writes the record itself and its only job is to recognize the moment and steer me toward the <code>/adr</code> command. It runs passively most of the time, but I can also invoke it manually whenever I want a second opinion on whether a decision I&#8217;m about to make deserves to be recorded.</p><h3><strong>Recording it</strong></h3><p>Once a decision is worth capturing, the <code>/adr</code><a href="https://gist.github.com/Arsenalist/877ed26bdfa356fa23c1298fdd0dada6"> command</a> does the mechanical work. It finds the next sequential ID, creates a new <code>NNNN-short-kebab-title.md</code> file, and fills in the frontmatter and template: a <strong>Context</strong> section for the &#8220;why now&#8221;, a <strong>Decision</strong> stated in a sentence or two, the <strong>Options considered</strong> with their trade-offs, and the <strong>Consequences</strong> that follow. The ADR is staged alongside the related feature commit, so there&#8217;s history next to the related code rather than in a separate commit (<a href="https://gist.github.com/Arsenalist/ffac80a538cfc96ba5277b155692e1b2">example ADR here</a>). Just as importantly, the command keeps an index page current with a table of live decisions and one for superseded decisions, so there is always a single, ordered map of every invariant the architecture has committed to.</p><h3><strong>Treating ADRs as invariants</strong></h3><p>Recording a decision is pointless if agents never read it and the index page is the entry point to all such decisions. I point the agent at it so that before touching anything architectural it consults the relevant ADRs and treats their Decision sections as hard constraints. I add a deterministic check in the same spirit as the <a href="https://zarar.dev/agent-hooks-deterministic-guardrails-for-ai-generated-code/">Hooks</a> from before that verifies the ADRs were actually consulted before code is allowed through. The check confirms that any change touching an area governed by an ADR references that ADR, and fails the run otherwise. This closes the loop: the decision is recorded, surfaced, and then <em>enforced</em>, so an invariant can&#8217;t be silently violated simply because the agent didn&#8217;t bother to look.</p><p>Below is a <code>Stop</code> hook which runs when the agent thinks it&#8217;s finished. Like every Claude Code hook it receives a blob of JSON on stdin, which is where the path to the session transcript comes from. Each ADR declares the paths it governs as a <code>scope</code> list of globs in its frontmatter, and the hook compares the files changed in the working tree against those globs. If a changed file falls under an ADR&#8217;s scope, that ADR has to have been opened during the session, which I detect by scanning the transcript for the ADR&#8217;s file path. If a governed file was touched but its ADR was never read, the hook exits non-zero and hands the agent the reason, forcing it to go back and consult the record before it can stop.</p><pre><code><em><span>#!/usr/bin/env bash</span></em>
<em><span># .claude/hooks/check-adrs.sh - runs on Stop</span></em>
<span>transcript=</span><strong><span>$(</span></strong>jq<span> </span>-r<span> &#8216;.transcript_path&#8217;</span><strong><span>)</span></strong><span>   </span><em><span># hook input arrives as JSON on stdin</span></em>
<span>changed=</span><strong><span>$(</span></strong>git<span> </span>diff<span> </span>--name-only<span> </span>HEAD<strong><span>)</span></strong>

<strong><span>for</span></strong><span> </span>adr<span> </span><strong><span>in</span></strong><span> </span>site/internal/src/content/docs/adrs/<span>[0</span>-9<span>]</span>*.md;<span> </span><strong><span>do</span></strong>
<span>  </span><em><span># read the scope globs from the ADR&#8217;s frontmatter, one per line</span></em>
<span>  </span><strong><span>for</span></strong><span> </span>scope<span> </span><strong><span>in</span></strong><span> </span><strong><span>$(</span></strong>yq<span> </span>--front-matter<span>=</span>extract<span> &#8216;.scope[]&#8217; &#8220;$adr&#8221;</span><strong><span>)</span></strong>;<span> </span><strong><span>do</span></strong>
<span>    </span><strong><span>for</span></strong><span> </span>file<span> </span><strong><span>in</span></strong><span> $changed</span>;<span> </span><strong><span>do</span></strong>
<span>      </span><strong><span>if</span></strong><span> [[ $file == $scope ]] &amp;&amp; </span>!<span> </span>grep<span> </span>-qF<span> &#8220;$adr&#8221; &#8220;$transcript&#8221;</span>;<span> </span><strong><span>then</span></strong>
<span>        echo &#8220;BLOCKED: $file is governed by $adr, which was never consulted.&#8221; </span>&gt;&amp;<span>2</span>
<span>        exit 2                           </span><em><span># non-zero -&gt; agent must address it</span></em>
<span>      </span><strong><span>fi</span></strong>
<span>    </span><strong><span>done</span></strong>
<span>  </span><strong><span>done</span></strong>
<strong><span>done</span></strong>
</code></pre><p>It doesn&#8217;t try to judge whether the code honours the decision, only that the decision was read. That alone removes the most common problem which is when an agent ignores a constraint. This leaves the RFC 2119 to do more semantic checks as described below.</p><h2><strong>RFC 2119 Keywords as Invariants</strong></h2><p>Where an ADR records a decision, <a href="https://www.rfc-editor.org/info/rfc2119/">RFC 2119</a> keywords record behaviour. Words like MUST, MUST NOT, SHALL, SHOULD and MAY turn a requirement into a rule and pairing them with a Gherkin style given/when/then makes each rule concrete enough to check. Written this way a requirement stops being a suggestion and becomes an invariant the code has to satisfy.</p><h3><strong>Keeping the spec in sync</strong></h3><p>A spec is only an invariant if it matches the behaviour of the code. The real risk with spec-driven work is drift, where you change your mind during implementation and the spec becomes out of date. I use <a href="https://openspec.dev/">OpenSpec</a> mostly for this reason. It produces the spec as part of planning and rewrites it after the fact when the implementation diverges, so the keywords and scenarios stay in sync. <a href="https://gist.github.com/Arsenalist/9765b6c853b8b894f857515b5bd320b8">Here&#8217;s an example</a> of a spec it generated that conforms to the RFC. A single requirement reads like this.</p><pre><code><em><span>#### Requirement: Fulfillment record creation</span></em>

<span>The system SHALL create a fulfillment record when an order is completed.</span>
<span>Each record MUST have a unique identifier.</span>

<strong><span>Scenario:</span></strong><span> Order completed with an add-on</span>
<span>  WHEN an order containing an add-on is marked complete</span>
<span>  THEN a fulfillment record is created for that add-on</span>
<span>  AND the record is assigned a unique id</span>
</code></pre><p>The keyword carries the weight. SHALL and MUST are the invariant, the scenario is how you check it.</p><h3><strong>Pointing the agent at the spec</strong></h3><p>A spec is useful only when the agent reads it before generating code. With OpenSpec this is automatic, since any code it generates consults the existing specs and checks for violations first. Without it you wire the same behaviour by hand. Keep the specs in a known directory, tell the agent in its instructions to load the relevant ones before writing code, and back that with a check like the ADR hook above that fails the run if a changed file&#8217;s spec was never opened. The tooling differs but the invariant concept still holds, which is that no code ships without its spec being consulted.</p><h2><strong>Wrapping up</strong></h2><p>ADRs and RFC 2119 specs solve the same problem from two ends. ADRs pin down the decisions that shape the architecture and specs pin down the behaviour the code has to honour. Both only work if the agent actually reads them, which is why each one is backed by a deterministic check rather than a prompt.</p><p>These checks are deliberately shallow. They check if a rule was consulted, not that the code truly honours it, and the harder semantic judgment still falls to you and your tests. What they help with is the the most common problem, which is the agent never knowing the rule existed in the first place.</p><p>Prose tells an agent what you would like, a check decides what it can ship. The more of your invariants you can move from prose to checks, the less you have to trust that the model remembered, and the more your codebase stays in a state that you recognize.</p>]]></content:encoded></item><item><title><![CDATA[Four Ways to Plan Agent Work, and When to Switch]]></title><description><![CDATA[Strategies for picking the right planning approach based on the change in front of you]]></description><link>https://bitbytebit.substack.com/p/four-ways-to-plan-agent-work-and</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/four-ways-to-plan-agent-work-and</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Mon, 22 Jun 2026 18:31:17 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!EQ3H!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54fcc773-fc4c-4a76-8496-8748d3302164_1200x771.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This post is for developers who want to learn about the different planning approaches to take during agentic software development. This is my experience, and all of this could be wrong, but it does work for me.</p><p>The type of planning I reach for depends on the type of change being introduced, and I&#8217;ll walk through these from the lightest to the heaviest. But picking the right approach up front is only part of it, as often I start in one mode and realize partway through that I&#8217;ve under-planned. The last section covers how I notice that and change direction mid-flight.</p><h2><strong>Planning Approaches</strong></h2><p>Before walking through each approach, here&#8217;s the whole idea in one chart. The way I see it, the effort of planning boils down to two things: 1) do I actually know what I want, and 2) how much breaks if I get it wrong? Everything below is just those two questions playing out.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!EQ3H!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54fcc773-fc4c-4a76-8496-8748d3302164_1200x771.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EQ3H!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54fcc773-fc4c-4a76-8496-8748d3302164_1200x771.webp 424w, https://substackcdn.com/image/fetch/$s_!EQ3H!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54fcc773-fc4c-4a76-8496-8748d3302164_1200x771.webp 848w, https://substackcdn.com/image/fetch/$s_!EQ3H!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54fcc773-fc4c-4a76-8496-8748d3302164_1200x771.webp 1272w, https://substackcdn.com/image/fetch/$s_!EQ3H!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54fcc773-fc4c-4a76-8496-8748d3302164_1200x771.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!EQ3H!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54fcc773-fc4c-4a76-8496-8748d3302164_1200x771.webp" width="1200" height="771" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/54fcc773-fc4c-4a76-8496-8748d3302164_1200x771.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:771,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;planning-approaches&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="planning-approaches" title="planning-approaches" srcset="https://substackcdn.com/image/fetch/$s_!EQ3H!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54fcc773-fc4c-4a76-8496-8748d3302164_1200x771.webp 424w, https://substackcdn.com/image/fetch/$s_!EQ3H!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54fcc773-fc4c-4a76-8496-8748d3302164_1200x771.webp 848w, https://substackcdn.com/image/fetch/$s_!EQ3H!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54fcc773-fc4c-4a76-8496-8748d3302164_1200x771.webp 1272w, https://substackcdn.com/image/fetch/$s_!EQ3H!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54fcc773-fc4c-4a76-8496-8748d3302164_1200x771.webp 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>Simple changes</strong></h3><p>If a change is dead simple and you feel the chances of the agent messing it up is low to none, you can enter a direct prompt. The three most recent changes I made using this approach are:</p><ul><li><p>Double the thickness of the border of the selected item on the product page</p></li><li><p>Change default radius of ad targeting from 15 km to 25 km</p></li><li><p>Use <code>&lt;.stat&gt;</code> component to display statistics instead of raw HTML on dashboard</p></li></ul><p>These are all changes that have easy verification and are 1-15 lines of changes, if that. I find there is no need to get into any sort of complex planning mode here because you could even hand-roll these changes out easily.</p><h3><strong>Agent plan mode</strong></h3><p>Most agents now ship with some sort of a plan mode which allows you to scope and bound the change before implementation. The plan mode can be invoked using <code>/plan</code> in Claude Code and other agents have similar, if not the exact, command. I reserve agent plan mode for changes where I know what I want to implement but where I want a &#8220;preview&#8221; of what the agent is about to do.</p><p>This doesn&#8217;t need to be a heavy weight blow-by-blow plan, but something that gives me confidence that it&#8217;s about to do the right things, and an opportunity for me to provide guardrails before it does it.</p><p>These are the three most recent changes I&#8217;ve requested an agent-driven plan for:</p><ol><li><p>The url is not a required field, but if it is provided, use the url path validator that we implemented recently which allows a / or a http(s) url to be used here.</p></li><li><p>Add a clickable link next to subscriber count on campaign audience page that opens a modal showing a random sample of targeted subscribers with name and email filters.</p></li><li><p>Remove the image_aspect thumbnail mode setting for event images as we now derive what aspect ratio to use based on whether the event has single or multiple images</p></li></ol><h3><strong>Spec Driven Development Execution</strong></h3><p>There are changes where I know what I want but I:</p><ol><li><p>Expect a bigger blast radius across the codebase (e.g., more than 3-5 files)</p></li><li><p>Want to spend more time on technical design and architecture</p></li><li><p>Want a more comprehensive historical record on why the change was introduced</p></li></ol><p>In these cases, I will use a tool like <a href="https://openspec.dev/">OpenSpec</a> or <a href="https://github.com/obra/Superpowers">Superpowers</a> to create a more comprehensive plan which will consider customer needs, technical designs and post-implementation documentation.</p><p>I&#8217;m a big fan of OpenSpec entirely because of how lightweight and non-prescriptive it is. I will start with an <code>/ospx:propose</code> command which will create the high-level proposal, a Gherkin-based spec which can be used as invariants by agents for future changes, a technical design and a task list for review. I will spend perhaps 10-30 minutes reviewing the plan and tweaking it before going into execution.</p><p>These are the three most recent changes I&#8217;ve done using <code>/opsx:propose</code>:</p><ol><li><p>Centralize object-level authorization in a shared on_mount hook and replace the per-LiveView/IDOR-open object checks with one shared hook</p></li><li><p>Create deep links from the order email customer receives to the refund page for that order&#8217;s items, while brand settings and not showing user refund links when not applicable to the item they purchased</p></li><li><p>Derive a brand&#8217;s default URL slug from brand name with a mnemonic suffix when URL slug collisions occur (use an existing Elixir library to create suffixes - not random numbers like currently)</p></li></ol><p>The above cases do a significant refactor (1), touch a crucial part of the customer experience which is the email they get (2) and change the internals of how we calculate public-facing URLs (3). These all affect the technical design of the feature to a degree where I want to review the impact in detail to ensure SRP, DRY etc are followed to my liking.</p><h3><strong>Spec Driven Development Brainstorming</strong></h3><p>In all the above cases so far, I have known what I want the outcome and design to be at some level. In many other cases, I find myself in a position where:</p><ol><li><p>I don&#8217;t know what exact customer experience I&#8217;d like and but have a general sense of what outcome I want</p></li><li><p>I have many options on how to implement a particular feature, and am not sure which is the right approach for me as I haven&#8217;t weighed the trade-offs</p></li><li><p>I haven&#8217;t implemented something like this before and need to come up with an approach which will help me narrow down scope and create MVPs</p></li></ol><p>The OpenSpec equivalent to this is <code>/opsx:explore</code> and the Superpowers equivalent is <code>/brainstorm</code>.</p><p>I find myself reaching for this planning approach a lot as it goes wide by diverging across different concerns before attempting to converge. In these case I don&#8217;t want to arrive at a solution quickly but want to uncover risks, align on the best customer experience, whittle down MVPs, and explore trade-offs and architectural patterns. I may spend hours to days in this mode going back and forth, and only after I&#8217;m comfortable I&#8217;ll convert the chat history into an <code>/opsx:propose</code>.</p><p>These are the three most recent changes I&#8217;ve initiated using <code>/opsx:explore</code> and where I spend at least a full day in this mode before hammering out a spec:</p><ol><li><p>Currently, admins must set an absolute price on every combination individually (e.g., 3 sizes x 3 colors = 9 prices to manage). Changing the base price requires updating all combinations manually. This doesn&#8217;t match the Uber Eats-style additive pricing model the modifier system was designed to support, where each option value has a +/- price adjustment and the final price is computed automatically. Let&#8217;s make this easier for admins by specifying base price and providing adjustments per combination. This change affects payouts, reports, analytics and possibly other areas of the app.</p></li><li><p>I want to extend the role-based system by having customers create their own roles instead of pre-defined set of roles. They should also be able to map permissions to users (not just roles). I will later also seek to add object-level authorization (instead of just account based)</p></li><li><p>Let merchants connect external email-marketing providers (Mailchimp, Squarespace) so that mailing-list signups and opted-in customers are automatically pushed to the merchant&#8217;s own audience on those platforms.</p></li></ol><p>As you can probably see, these are big changes with wide ranging impacts to multiple app modules. I see myself as not necessarily knowing what the solution here will be, and want to spend a lot of time branching out and exploring before aligning on a direction. Direct prompting is entirely useless here, agent planning mode is insufficient, and even creating a spec for immediate execution seems risky.</p><h2><strong>Changing Direction Mid-Flight</strong></h2><p>Everything above assumes I pick the right planning mode up front. In practice the more useful skill is noticing, partway through, that I picked too light a mode and bumping up a tier before I&#8217;ve wasted too much time. Here&#8217;s what I watch for.</p><p><strong>Switching from simplistic prompts to agent plan mode</strong> The giveaway is when the diff surprises me. I asked to swap raw HTML for the &lt;.stat&gt; component on the dashboard, expecting a handful of lines, and the agent starts reaching into how the dashboard loads its data. The moment a &#8220;1-15 line&#8221; change touches a file I didn&#8217;t picture, I stop and re-issue it as a /plan so I can see the full scope before it runs.</p><p><strong>Switching from agent plan mode to spec execution</strong> This happens when the plan coming back is bigger than the preview I expected. I asked for the optional-url-validator change expecting a bounded plan, and instead it touches five or six files, or contains a design decision I can&#8217;t make in thirty seconds, e.g., where the validation should live, whether it&#8217;s shared or a one-time use. When a &#8220;preview&#8221; turns into something I need to review for SRP and DRY, plan mode is no longer good enough and I need a spec to review.</p><p><strong>Switching from spec execution to brainstorming</strong> The clearest signal is that I&#8217;m starting to extensively rewrite the spec instead of just reviewing it. If I sit down to tweak an <code>/opsx:propose</code> output and keep changing the approach, or worse, I keep changing the Gherkin invariant because I don&#8217;t actually know what the correct behaviour should be, then I admit defeat and go into brainstorming/explore mode. I find that if I don&#8217;t do this, I end up baking my confusion into the code.</p><h2><strong>Conclusion</strong></h2><p>Your mileage may vary on these and you may have an entirely different approach, and that&#8217;s completely fine. The type of tools available to us has exponentially increased over the last three years, and there&#8217;s no single &#8220;right&#8221; way of approaching things. The key is to de-risk the implementation at the right juncture so that what you end up producing conforms to your mental model of how the software internally works, and the changeability of those internals as customer needs invariably shift.</p>]]></content:encoded></item><item><title><![CDATA[Don't rely on instructions; use Agent Hooks to enforce guardrails ]]></title><description><![CDATA[This post is for developers who use AGENTS.md or CLAUDE.md to provide guardrails for agent-generated code, but find that the agent sometimes ignores rules.]]></description><link>https://bitbytebit.substack.com/p/agent-hooks-deterministic-guardrails</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/agent-hooks-deterministic-guardrails</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Sat, 20 Jun 2026 02:57:25 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/06da8a7c-ae6a-4949-8d7a-d5a83effbbbb_2049x1401.avif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This post is for developers who use AGENTS.md or CLAUDE.md to provide guardrails for agent-generated code, but find that the agent sometimes ignores rules. If you want a deterministic check that will work 100% of the time, read on about agent hooks.</p><p>First, a clarification. Agent Hooks are different than <a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks">git hooks</a> which many developers are familiar with. The most popular Git hook might be the pre-commit hook which is called before you try to commit everything and is a popular place to do perhaps a <code>git pull</code> or some code formatting (e.g., prettier or <code>mix format</code>) to ensure your code is formatted as per the language&#8217;s standards. The limitation of a pre-commit hook is that it gets executed well after you have generated the code and just before you think you&#8217;re done (i.e., commit time).</p><p>Agent hooks are invoked when the agent (e.g., Claude Code) is doing work and allows developers to interject themselves into the agent&#8217;s workflow, rather than after the work is done (e.g., code review). Here&#8217;s a list of <a href="https://code.claude.com/docs/en/hooks">Claude Code Hooks</a> which we&#8217;ll refer to. As a caution, not all agents have the same hooks. Unlike <a href="https://agentskills.io/home">Skills where standard exist</a>, Hooks are a bit of a mess so you&#8217;ll have to see what hooks your agent makes available to you. I&#8217;m going to be doing two deterministic checks which have bit me in the past:</p><ol><li><p>Ensure that the agent never uses a <code>&lt;input&gt;</code> tag directly because I want it to use the design components I have</p></li><li><p>Ensure that the agent never tells me it&#8217;s done while my design-system ratchet test is failing</p></li></ol><p>These two fire at completely different points in the agent&#8217;s lifecycle. The first runs <em>before</em> the agent executes a tool; the second runs when the agent thinks it&#8217;s finished.</p><p>Every hook gets a blob of JSON on stdin, and the shape of that blob depends on the event. That&#8217;s what the <code>jq</code> calls below are digging into. I&#8217;ll show you exactly what each hook receives so the paths the <code>jq</code> tool is using makes sense. I&#8217;m using <code>jq</code> but you could have written a Python script, a shell script or anything that the agent could call.</p><h2><strong>1. No raw </strong><code>&lt;input&gt;</code><strong> tags</strong></h2><p>This one is a <code>PreToolUse</code> hook. PreToolUse fires right before Claude Code runs a tool, and it&#8217;s the one place where you can actually stop the tool from happening by exiting with an error code other than <code>1</code> or <code>2</code>. Whatever you wrote to stderr when exiting with exit code <code>2</code> will be seen by the agent as feedback. Exit <code>1</code> only logs a warning and lets the tool through.</p><p>I want every form field to go through my own <code>&lt;.cinput&gt;</code> component, not a bare <code>&lt;input&gt;</code>. So I check the content the agent is about to write and block it if I see the tag. This goes in <code>.claude/settings.json</code>:</p><pre><code>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;hooks&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">    </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;PreToolUse&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>[
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">      </span>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">        </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;matcher&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;Write|Edit&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">        </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;hooks&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>[
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">          </span>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">            </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;type&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;command&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">            </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;command&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;jq -r &#8216;.tool_input.content // .tool_input.new_string // empty&#8217; | grep -q &#8216;&lt;input&#8217; &amp;&amp; { echo &#8216;Use my &lt;.cinput&gt; design component, not a raw &lt;input&gt; tag.&#8217; &gt;&amp;2; exit 2; } || exit 0&#8221;</span>
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">          </span>}
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">        </span>]
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">      </span>}
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">    </span>]
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span>}
}
</code></pre><p>Here&#8217;s what the hook actually sees on stdin when the agent goes to write a file:</p><pre><code>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;hook_event_name&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;PreToolUse&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;tool_name&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;Write&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;tool_input&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">    </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;file_path&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;lib/amplify_web/components/form.ex&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">    </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;content&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;...the code the agent wants to write...&#8221;</span>
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span>},
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;session_id&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;&#8230;&#8221;</span>,<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;cwd&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;&#8230;&#8221;</span>,<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;transcript_path&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;&#8230;&#8221;</span>
}
</code></pre><p>That&#8217;s why the <code>jq</code> pulls <code>.tool_input.content</code>. A <code>Write</code> puts the whole file under <code>content</code>, but an <code>Edit</code> puts it under <code>new_string</code> instead (with <code>old_string</code> alongside it), so I fall back to <code>.tool_input.new_string</code> to cover both. The agent never gets to put a raw <code>&lt;input&gt;</code> on disk as the write dies and my message tells it to go use the component instead.</p><p>What I could&#8217;ve done instead, but didn&#8217;t trust:</p><ul><li><p>Just writing &#8220;always use <code>&lt;.cinput&gt;</code>, never raw <code>&lt;input&gt;</code>&#8220; in CLAUDE.md. That&#8217;s the exact thing the agent ignores half the time and the reason you&#8217;re reading this.</p></li><li><p>An <code>mix credo</code> (or <code>eslint</code> or pick your language&#8217;s linter) rule. Better, but it only catches the tag whenever something actually runs the linter, which the agent may not bother to do, and even then it&#8217;s at lint/commit time, well after the code is already written.</p></li></ul><h2><strong>2. Don&#8217;t let it stop until the ratchet test passes</strong></h2><p>This one&#8217;s a <code>Stop</code> hook, which fires the moment the agent decides it&#8217;s finished. It&#8217;s the inverse of PreToolUse as instead of blocking an action before it happens, it refuses to let the agent end the turn at all. Exit <code>2</code> here means &#8220;no, keep working,&#8221; and the stderr message tells it why.</p><p>I keep a ratchet test that locks in design-system decisions I&#8217;ve made at <code>test/amplify_web/design_system_ratchet_test.exs</code>. The thing that&#8217;s bitten me most is the agent announcing it&#8217;s done with that ratchet red. The agent may run tests it thinks it needs to verify it&#8217;s work, but the ratchet test doesn&#8217;t always get picked up as it&#8217;s more of a &#8220;global&#8221; check rather than specific to a feature. So I gate the finish on exactly that test, not the whole suite (it&#8217;s faster, and it&#8217;s the decision I actually care about):</p><pre><code>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;hooks&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">    </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;Stop&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>[
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">      </span>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">        </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;hooks&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>[
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">          </span>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">            </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;type&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;command&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">            </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;command&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;[ \&#8221;$(jq -r &#8216;.stop_hook_active&#8217;)\&#8221; = true ] &amp;&amp; exit 0; mix test test/amplify_web/design_system_ratchet_test.exs &gt;/dev/null 2&gt;&amp;1 || { echo &#8216;Design-system ratchet test is failing &#8212; fix it before you call it done.&#8217; &gt;&amp;2; exit 2; }&#8221;</span>
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">          </span>}
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">        </span>]
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">      </span>}
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">    </span>]
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span>}
}
</code></pre><p>The stdin for a Stop hook is much thinner since there&#8217;s no tool to inspect, just the fact that the agent wants to wrap up:</p><pre><code>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;hook_event_name&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;Stop&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;stop_hook_active&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><strong><span data-color="rgb(0, 112, 32)" style="color: rgb(0, 112, 32);">false</span></strong>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;session_id&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;&#8230;&#8221;</span>,<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;cwd&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;&#8230;&#8221;</span>,<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;transcript_path&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;&#8230;&#8221;</span>
}
</code></pre><p>No <code>tool_input</code> here since there&#8217;s no tool invocation happening. Stop hook runs my gate and decides whether the turn is allowed to end. So the <code>jq</code> only reaches for <code>.stop_hook_active</code>. Now the agent literally can&#8217;t wrap up until the ratchet test is passing.</p><p>One important point that tripped me up: that <code>stop_hook_active</code> check at the front is not optional. Once a Stop hook has forced a continuation, that flag comes back <code>true</code> on the next stop, and if you don&#8217;t bail out when you see it, a permanently-red ratchet will trap the agent in an infinite &#8220;fix &#8594; stop &#8594; blocked &#8594; fix&#8221; loop until you kill the session, so we must check the flag and let it stop.</p><p>What I could&#8217;ve done instead, but didn&#8217;t trust:</p><ul><li><p>CLAUDE.md (&#8221;always run the ratchet before saying you&#8217;re done&#8221;). Ignored, same as everything else in this category.</p></li><li><p>A <code>PostToolUse</code> hook running the ratchet after every edit. It works, but it fires constantly mid-task when the code is legitimately half-finished, so it&#8217;s slow and noisy. The Stop gate runs once, at the only moment that matters is when the agent claims it&#8217;s done.</p></li><li><p>Leaving it to pre-commit or CI. Catches it eventually, but only at commit/push time, i.e., after the agent&#8217;s already declared victory and I&#8217;ve moved on. That&#8217;s the exact &#8220;too late&#8221; problem with the pre-commit hook I opened this post complaining about.</p></li></ul><p>One more trap that applies to both is if <code>jq</code> fails silently. Get a path wrong (<code>.tool_input.content</code>, <code>.stop_hook_active</code>) and jq returns <code>null</code>, your check matches nothing, and the gate quietly does nothing while looking like it works. Test each one against a real hook payload before you trust it.</p><p>That&#8217;s it. Two checks at two different points in the loop, both deterministic, both fire every single time and give you more confidence that the agent isn&#8217;t going sideways by ignoring your MUST DO VERY IMPORTANT DON&#8217;T FORGET instructions in CLAUDE.md!</p>]]></content:encoded></item><item><title><![CDATA[Run a local coding model with pi and LM Studio]]></title><description><![CDATA[A getting-started guide for people who live in Claude, Codex, or Gemini]]></description><link>https://bitbytebit.substack.com/p/run-a-local-coding-model-with-pi</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/run-a-local-coding-model-with-pi</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Wed, 17 Jun 2026 16:13:58 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ca81267b-4b60-4305-ac53-0613e8a71123_799x904.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This post is for people who generally use Claude, Codex, or Gemini but have heard you can run open-source models locally for free. The goal is to get you set up in no time so you can play around with the power of local models.</p><p>If you already use a coding agent, you already know how this works. A coding agent (e.g., Claude Code) talks to a model over an HTTP API: it sends your request, the model sends back tokens, and the agent uses tools (read, edit, run) to do real work. With Claude Code or Codex, that API lives in a datacenter and you reach it over the internet.</p><p>Running locally changes one main thing: where the endpoint is. Instead of pointing your agent at a remote provider, you point it at a server running on your own machine. Same request in, same completion out except the model just happens to be sitting on your local computer.</p><p>Here&#8217;s the architecture, side by side:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!S6ts!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e0835f1-6516-49a7-9237-2cc87cff707d_1200x1454.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!S6ts!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e0835f1-6516-49a7-9237-2cc87cff707d_1200x1454.webp 424w, https://substackcdn.com/image/fetch/$s_!S6ts!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e0835f1-6516-49a7-9237-2cc87cff707d_1200x1454.webp 848w, https://substackcdn.com/image/fetch/$s_!S6ts!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e0835f1-6516-49a7-9237-2cc87cff707d_1200x1454.webp 1272w, https://substackcdn.com/image/fetch/$s_!S6ts!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e0835f1-6516-49a7-9237-2cc87cff707d_1200x1454.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!S6ts!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e0835f1-6516-49a7-9237-2cc87cff707d_1200x1454.webp" width="1200" height="1454" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3e0835f1-6516-49a7-9237-2cc87cff707d_1200x1454.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1454,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;local-vs-remote-llm-architecture&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="local-vs-remote-llm-architecture" title="local-vs-remote-llm-architecture" srcset="https://substackcdn.com/image/fetch/$s_!S6ts!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e0835f1-6516-49a7-9237-2cc87cff707d_1200x1454.webp 424w, https://substackcdn.com/image/fetch/$s_!S6ts!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e0835f1-6516-49a7-9237-2cc87cff707d_1200x1454.webp 848w, https://substackcdn.com/image/fetch/$s_!S6ts!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e0835f1-6516-49a7-9237-2cc87cff707d_1200x1454.webp 1272w, https://substackcdn.com/image/fetch/$s_!S6ts!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e0835f1-6516-49a7-9237-2cc87cff707d_1200x1454.webp 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The three pieces on the local side:</p><ul><li><p><strong>pi</strong> &#8212; the coding agent (the equivalent of Claude Code / Codex). It sends requests and runs tools.</p></li><li><p><strong>LM Studio</strong> &#8212; a local server that hosts the model and exposes an <em>OpenAI-compatible</em> endpoint on <code>http://localhost:1234/v1</code>. You can use other options like <code>ollama</code> which is headless but we&#8217;re going to stick with the easiest way (I think) and use LM Studio.</p></li><li><p><strong>qwen/qwen3.6-27b</strong> &#8212; the actual model, running on your hardware which you can download through LM Studio.</p></li></ul><p>Because <code>pi</code> speaks the OpenAI chat-completions protocol and LM Studio serves an OpenAI-compatible endpoint, hooking them together is a drop-in. You tell <code>pi</code> the base URL is <code>localhost:1234</code>, and that&#8217;s the whole trick.</p><h2>Step 1: Check what your machine can run</h2><p>Before downloading anything, check your hardware. Two browser tools detect your GPU, VRAM, and RAM and tell you which models will actually run (and how well):</p><ul><li></li></ul><p>https://www.canirun.ai/</p><ul><li><p> &#8212; detects your hardware and grades models from &#8220;runs great&#8221; to &#8220;too heavy.&#8221;</p></li><li></li></ul><p>https://www.caniusellm.com/</p><ul><li><p> &#8212; similar check, plus quantization recommendations (which INT4/INT8/FP16 build fits your specs).</p></li></ul><p>A 27B model like <code>qwen3.6-27b</code> at a 4-bit quant is roughly 15&#8211;16 GB of weights <em>before</em> you add any context, so a 24 GB GPU is a comfortable sweet spot. If your machine is smaller, the checker will point you at a model that fits.</p><p>I&#8217;m running this on an Apple M5 with 128 GB of unified memory. Because Apple Silicon shares that memory pool between CPU and GPU, the whole 128 GB is available to the model. A model like <code>qwen3.6-27b</code> plus a generous context window barely makes a dent, so I can run the full 256K window without thinking about it and even reach for higher-quality quants (Q6/Q8). If you&#8217;ve got a machine in this class you have plenty of headroom; on a smaller GPU, let the hardware checker steer your model and quant choice.</p><p>Quantization just means a compressed version of the weights. Q4 is the usual sweet spot between quality and size. If it fits and runs, you&#8217;re good, don&#8217;t overthink it for your first model.</p><h2>Step 2: Install LM Studio and load your model</h2><p><a href="https://lmstudio.ai/download">Download LM Studio</a> (macOS, Windows, Linux) and install it. The <a href="https://lmstudio.ai/docs/app">docs</a> walk through the app if you want them.</p><p>Inside LM Studio, use the model search to download <code>qwen3.6-27b</code> (or whichever model the hardware check recommended), picking the quant that fits your machine. Load it, then turn on the local server: open the <strong>Developer</strong> tab and toggle <strong>Start server</strong>. That&#8217;s what exposes the OpenAI-compatible endpoint at <code>http://localhost:1234/v1</code> that <code>pi</code> will talk to. (<a href="https://lmstudio.ai/docs/developer/core/server">Server docs</a>.)</p><h2>Step 3: Install pi</h2><p><a href="https://pi.dev/">pi</a> is the coding agent. Install it with the official script:</p><pre><code>curl<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>-fsSL<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>https://pi.dev/install.sh<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>|<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>sh
</code></pre><p>Or via npm if you prefer: <code>npm install -g @mariozechner/pi-coding-agent</code>. Either way, run <code>pi --version</code> to confirm it worked, and see the <a href="https://pi.dev/docs/latest/quickstart">quickstart docs</a> for first-run setup and logging into cloud providers. You could also use <a href="https://opencode.ai/">OpenCode</a> but I prefer <code>pi</code> so let&#8217;s run with it.</p><h2>Step 4: Point pi at your local model</h2><p><code>pi</code> finds custom providers in a <code>models.json</code> file in your agent directory. Open it:</p><pre><code>vi<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>~/.pi/agent/models.json
</code></pre><p>Here&#8217;s how I configured mine:</p><pre><code>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;providers&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">    </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;lmstudio&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">      </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;baseUrl&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;http://localhost:1234/v1&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">      </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;api&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;openai-completions&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">      </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;apiKey&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;lm-studio&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">      </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;models&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>[
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">        </span>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">          </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;id&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;qwen/qwen3.6-27b&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">          </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;input&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>[
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">            </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;text&#8221;</span>
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">          </span>]
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">        </span>}
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">      </span>]
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">    </span>}
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span>}
}
</code></pre><p>A few notes on what&#8217;s going on here:</p><ul><li><p><code>"api": "openai-completions"</code> tells <code>pi</code> to use the OpenAI chat-completions protocol. This is the part that makes any OpenAI-compatible local server (LM Studio, Ollama, vLLM) just work.</p></li><li><p><code>"apiKey": "lm-studio"</code> is required but ignored by LM Studio &#8212; any non-empty string is fine.</p></li><li><p>The <code>"id"</code> must match <strong>exactly</strong> what LM Studio exposes. If you&#8217;re not sure, run <code>curl http://localhost:1234/v1/models</code> and copy the id from there.</p></li></ul><p>Once it&#8217;s saved, you&#8217;ll see the model in <code>pi</code>&#8216;s model picker (<code>/model</code>). And whenever you want to jump back to a cloud model, the same picker switches you over with local and remote living side by side.</p><h2>Step 5: Set your context size and reload the model</h2><p>The context window is the model&#8217;s working memory, meaning everything it can &#8220;see&#8221; at once. On a cloud model this is fixed for you. Locally, <em>you</em> choose it when you load the model, because a bigger context costs more VRAM (the KV cache grows with context length).</p><p>In LM Studio, set the context length on the model&#8217;s load settings, then <strong>reload the model</strong> for it to take effect.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zF8O!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F176c6021-267a-4dce-9947-93f1767dd617_1200x471.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zF8O!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F176c6021-267a-4dce-9947-93f1767dd617_1200x471.webp 424w, https://substackcdn.com/image/fetch/$s_!zF8O!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F176c6021-267a-4dce-9947-93f1767dd617_1200x471.webp 848w, https://substackcdn.com/image/fetch/$s_!zF8O!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F176c6021-267a-4dce-9947-93f1767dd617_1200x471.webp 1272w, https://substackcdn.com/image/fetch/$s_!zF8O!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F176c6021-267a-4dce-9947-93f1767dd617_1200x471.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zF8O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F176c6021-267a-4dce-9947-93f1767dd617_1200x471.webp" width="1200" height="471" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/176c6021-267a-4dce-9947-93f1767dd617_1200x471.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:471,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;llm-studio-screenshot&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="llm-studio-screenshot" title="llm-studio-screenshot" srcset="https://substackcdn.com/image/fetch/$s_!zF8O!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F176c6021-267a-4dce-9947-93f1767dd617_1200x471.webp 424w, https://substackcdn.com/image/fetch/$s_!zF8O!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F176c6021-267a-4dce-9947-93f1767dd617_1200x471.webp 848w, https://substackcdn.com/image/fetch/$s_!zF8O!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F176c6021-267a-4dce-9947-93f1767dd617_1200x471.webp 1272w, https://substackcdn.com/image/fetch/$s_!zF8O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F176c6021-267a-4dce-9947-93f1767dd617_1200x471.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>How much context you want depends on what you&#8217;re doing and how much VRAM you can spare on top of the weights:</p><p>What you&#8217;re doingContext sizeExtra VRAM (rough)A couple files16K~1 GBSimple coding, UI development64K~4 GBMulti-file refactors, medium complexity tasks128K~8 GBPlanning with full-repo contet256K~16 GB</p><p>These are ballpark KV-cache numbers on top of the ~15&#8211;16 GB the 27B weights already use, so the full 256K window is pretty heavy. You have to budget for it before you crank the slider all the way to the right. When in doubt, start at 64K. It&#8217;s plenty for day-to-day work and unless you find the agent making mistakes and forgetting things a lot, you can stick around here.</p><h2>256K locally vs 1M in the cloud</h2><p><code>qwen3.6-27b</code> supports up to <strong>256K</strong> tokens of context natively. For comparison, Claude Opus 4.8 runs in a <strong>1M</strong>-token window. That gap is the main thing to keep in mind when deciding what to run where.</p><p>256K is still a lot and easily holds a focused slice of a codebase: the files for a feature, a module and its tests, a long debugging session. For most everyday coding, single-feature work, and contained refactors, you won&#8217;t feel a limitation here but your mileage may vary.</p><p>Where the 1M cloud window pulls ahead is the <em>whole-codebase, long-horizon</em> stuff: loading an entire large repo at once, agentic tasks that run for hundreds of steps and accumulate huge history, or analyses that need to hold a giant document set in view. If you find yourself constantly trimming what you feed the model, that&#8217;s the sign to reach for a cloud model for that task.</p><p>A reasonable rule of thumb:</p><ul><li><p><strong>Local (qwen3.6-27b):</strong> focused edits, day-to-day coding, contained refactors, anything you want to run offline, privately, or at zero cost. A great use case is throwaway work where you don&#8217;t want to spend tokens.</p></li><li><p><strong>Cloud (Claude / Codex / Gemini):</strong> whole-repo context, the hardest reasoning, and long agentic runs where the bigger window and top-tier capability earn their keep.</p></li></ul><h2>Re-use Claude Code skills</h2><p>If you&#8217;ve built up <code>SKILL.md</code> skills for Claude Code, <code>pi</code> uses the same <a href="https://agentskills.io/">open Agent Skills standard</a> (originally from Anthropic, now adopted across Claude Code, Codex, Gemini CLI, and more).</p><p>The catch is that <code>pi</code> only auto-discovers skills in <em>its own</em> locations by default. It does not look in <code>~/.claude/skills</code> unless you tell it to. So if your Claude skills aren&#8217;t showing up in <code>pi</code>, this is why.</p><p>Point <code>pi</code> at them in its settings file. Note this is <code>settings.json</code>, not the <code>models.json</code> from Step 4:</p><pre><code>vi<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>~/.pi/agent/settings.json
</code></pre><p>Here&#8217;s a full <code>settings.json</code> that points <code>pi</code> at your skills and boots it straight into your local model:</p><pre><code>{
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;defaultProvider&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;lmstudio&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;defaultModel&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;qwen/qwen3.6-27b&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span><strong><span data-color="rgb(6, 40, 115)" style="color: rgb(6, 40, 115);">&#8220;skills&#8221;</span></strong>:<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);"> </span>[
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">    </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;~/.claude/skills&#8221;</span>,
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">    </span><span data-color="rgb(64, 112, 160)" style="color: rgb(64, 112, 160);">&#8220;~/.codex/skills&#8221;</span>
<span data-color="rgb(187, 187, 187)" style="color: rgb(187, 187, 187);">  </span>]
}
</code></pre><p>The <code>skills</code> array is a list of directories <code>pi</code> scans for <code>SKILL.md</code> folders. (<code>defaultProvider</code> and <code>defaultModel</code> are optional; they match the provider name and model <code>id</code> from your <code>models.json</code> in Step 4 so <code>pi</code> starts on your local model automatically.)</p><p>Then <strong>restart </strong><code>pi</code> as it only scans skill locations at startup, so a running session won&#8217;t pick up the change. After the restart, <code>pi</code> loads skills exactly the way Claude Code does with only the descriptions loaded in context, and the full instructions load on demand when a task matches (or when you force it with <code>/skill:name</code>).</p><h2>Quick gotchas before you start</h2><p>A list of the things that tripped me up:</p><ul><li><p><strong>Start LM Studio&#8217;s local server.</strong> Loading the model isn&#8217;t enough and the server has to be switched on, or <code>pi</code> can&#8217;t reach <code>localhost:1234</code>. This is the most common problem.</p></li><li><p><strong>Match the model id exactly</strong> between LM Studio and your <code>models.json</code>. <code>curl http://localhost:1234/v1/models</code> shows you the truth.</p></li><li><p><strong>Expect it to be slower than cloud.</strong> A 27B running on your GPU won&#8217;t hit cloud token rates, and the first load takes a moment. That&#8217;s normal.</p></li></ul><h2>My workflow: Plan with Claude, build locally</h2><p><code>pi</code> isn&#8217;t local-only. It has built-in support for Claude, so you can run cloud and local models from the <em>same</em> agent. Authenticate once with <code>/login</code> (it works with a Claude Pro/Max subscription or an Anthropic API key, stored in <code>~/.pi/agent/auth.json</code>), and Claude shows up in the same <code>/model</code> picker as your local model.</p><p>That unlocks the workflow that makes local models genuinely practical: <strong>plan with Claude, execute locally.</strong></p><ul><li><p>Use a Claude model for the thinking such as architecting a feature, breaking the work into steps where top-tier reasoning and the 1M window come into play.</p></li><li><p>Then switch to your local model (<code>/model</code>) to execute the plan: the repetitive edits, running tests, grinding through the refactor &#8212; for free, offline, and private.</p></li><li><p>Flip back and forth as much as you want within a single session. Cloud for the hard thinking, local for the volume.</p></li><li><p>I use <a href="https://openspec.dev/">OpenSpec</a> so I create the plans with Claude and execute locally</p></li></ul><p>That&#8217;s the whole setup. Check your hardware, install LM Studio and load a model that fits, install <code>pi</code>, drop a provider into <code>models.json</code>, set your context, and start coding. You&#8217;re running a capable coding model entirely on your own machine.</p>]]></content:encoded></item><item><title><![CDATA[Spec-Driven Development: From Vibe Coding to Structured Development]]></title><description><![CDATA[I currently work with a Payments Engineering team and wrote this as we are introducing spec-driven development into our development workflow.]]></description><link>https://bitbytebit.substack.com/p/spec-driven-development-from-vibe</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/spec-driven-development-from-vibe</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Wed, 25 Feb 2026 00:50:43 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e266968f-774b-406e-a8f6-4251d975bc9b_2352x1558.avif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2><p>If you&#8217;ve used an AI coding tool in the last year, you&#8217;ve probably had the experience: you describe what you want, the AI generates something that looks right, you run it, and... it doesn&#8217;t quite work. You refine your prompt. The AI fixes one thing and breaks another. Three iterations later you&#8217;re debugging code you didn&#8217;t write and don&#8217;t fully understand.</p><p>This is the failure mode of what Andrej Karpathy called &#8220;vibe coding&#8221; and it&#8217;s become the default way most developers interact with AI. Spec driven development (SDD) is the emerging counter movement. Instead of throwing prompts at an LLM and hoping for the best, you write a structured specification first, then let the AI implement against it.</p><p>The idea isn&#8217;t new. We&#8217;ve been writing requirements documents since forever, but the tooling is new. Tools like GitHub&#8217;s Spec Kit, Amazon&#8217;s Kiro, and Fission AI&#8217;s OpenSpec are attempting to formalize this workflow into something repeatable. Whether that formalization helps or hinders depends entirely on what you&#8217;re building, how you&#8217;re building it, and the tradeoffs you&#8217;re willing to make.</p><p>Our team uses OpenSpec, so most of the practical examples in this post come from that experience. But the principles apply regardless of which tool you pick.</p><div><hr></div><h2>The Problem: Why &#8220;Just Prompting&#8221; Breaks Down</h2><p>The pitch for AI assisted coding is attractive: describe what you want in English and get working code back. And for simple tasks, a helper function, a config change, renaming a module, it works remarkably well. The challenges starts when changes aren&#8217;t trivial but require edits to multiple files or packages/modules.</p><p>The core issue is context loss. When you&#8217;re five prompts deep into a feature, the AI has no persistent memory of the architectural decisions you made in prompt one. It doesn&#8217;t know you chose a specific idempotency strategy for a reason. It doesn&#8217;t remember that you explicitly avoided storing raw card data outside the tokenization boundary. Every new prompt starts from a partial view of the world, and the AI fills in the gaps with whatever patterns it&#8217;s seen most in training data.</p><p>In payments systems, this produces particularly dangerous failures. Reconciliation logic scattered across three different modules because each prompt generated its own approach. A refund handler that doesn&#8217;t account for partial captures. Currency conversion applied twice because the AI didn&#8217;t know about the upstream normalization step. And perhaps most critically in our domain, security flaws: API keys committed to source, missing input validation on transaction amounts, authorization checks that live on the client instead of the server. Studies have found that roughly 45% of AI generated code contains security vulnerabilities. In a payments context, that&#8217;s more than just a bug but a compliance issue.</p><p>The other failure is architectural drift. Without a shared plan, each prompt/response cycle makes locally reasonable decisions that are globally incoherent. The AI can&#8217;t refactor itself out of architectural problems it doesn&#8217;t understand. You ask it to add retry logic to a payment gateway call and it builds a standalone retry mechanism, unaware that you already have a circuit breaker pattern in your infrastructure layer. Once the codebase reaches a certain size, the context window can only see fragments of it. You end up with a system that processes transactions but that nobody, including the AI, fully understands anymore.</p><p>This isn&#8217;t the AI being dumb. It&#8217;s the natural consequence of building without a map.</p><div><hr></div><h2>What Spec Driven Development Actually Is</h2><p>At its simplest, spec driven development means: write down what you&#8217;re building <em>before</em> you write the code, and make that written artifact the thing your AI agent works from.</p><p>That might sound like waterfall but It&#8217;s not, or at least, it doesn&#8217;t have to be. The key differences are timescale and scope. Traditional waterfall specs were project level documents written over weeks and often carved in stone. SDD specs are feature level documents written in minutes and meant to evolve. You&#8217;re not planning an entire system upfront; you&#8217;re planning the next meaningful chunk of work in enough detail that an AI can implement it without guessing.</p><p>A typical SDD workflow looks like this:</p><ol><li><p><strong>Define requirements.</strong> What should this feature do? Who is it for? What are the acceptance criteria? What are the edge cases?</p></li><li><p><strong>Create a technical design.</strong> How should it be implemented? What&#8217;s the data model? What APIs are involved? What patterns should be followed?</p></li><li><p><strong>Break it into tasks.</strong> What are the discrete, testable units of work? In what order should they be done?</p></li><li><p><strong>Implement.</strong> The AI executes against the task list, one piece at a time, with the full spec as context.</p></li></ol><p>You&#8217;re not writing all of this yourself. You describe the intent in natural language, and the AI generates the spec artifacts: the proposal, the requirements, the design, the task breakdown. Your job is to review, refine, and correct. You steer and the AI does the heavy lifting. This is what makes the process fast enough to be practical. Writing a 200 line spec by hand for every feature would be painful. Having the AI draft it in 30 seconds and then spending 5 minutes reviewing and adjusting it is a different proposition entirely.</p><p>The spec becomes a persistent artifact, a &#8220;super prompt&#8221; that doesn&#8217;t disappear when your chat session ends. It lives in version control alongside your code. When the AI drifts, you point it back to the spec. When requirements change, you update the spec and regenerate.</p><p>The fundamental shift is that the specification becomes the source of truth, and code becomes the derived artifact. Traditional documentation describes code that already exists. SDD inverts that relationship. You define the behaviour, constraints, and architecture in the spec, and the AI produces code that conforms to it. The spec isn&#8217;t something you write after the fact to explain what was built but the input that determines what gets built. Code is the output.</p><div><hr></div><h2>The Tooling Landscape</h2><p>Three tools have emerged as the most prominent in this space. Each takes a different philosophical approach.</p><h3>GitHub Spec Kit</h3><p>Spec Kit is an open source CLI from GitHub that scaffolds a spec driven workflow into your existing project. It&#8217;s agent agnostic, working with GitHub Copilot, Claude Code, Gemini CLI, and others. The workflow follows rigid phases driven by slash commands: <code>/speckit.constitution</code> to establish project principles, <code>/speckit.specify</code> to create feature specs, <code>/speckit.plan</code> for a technical plan, <code>/speckit.tasks</code> for work items, and <code>/speckit.implement</code> to execute.</p><p><strong>Strengths:</strong> Thorough documentation output, the &#8220;constitution&#8221; concept for project wide principles, works with many agents.</p><p><strong>Weaknesses:</strong> Heavyweight. Sometimes it get generate a lot of artifacts for simple changes. Rigid phase gates mean you can&#8217;t easily jump back and forth between planning and implementing.</p><h3>Amazon Kiro</h3><p>Kiro is a full IDE (a VS Code fork) with spec driven development baked into the editing experience. The workflow follows a similar shape (requirements &#8594; design &#8594; tasks &#8594; implement) but is tightly integrated with the editor. It generates user stories with acceptance criteria, creates technical design documents, and produces task lists. It also introduces &#8220;Hooks,&#8221; user defined prompts triggered by file changes.</p><p><strong>Strengths:</strong> Most polished integrated experience. The Hooks system is excellent and something you&#8217;d have to configure manually if you decide to do it on your own. No context switching between planning and editing because of the IDE integration.</p><p><strong>Weaknesses:</strong> You&#8217;re locked into their IDE and limited to Claude models. Can be overkill for small changes. One developer reported a simple bug fix generating 4 user stories with 16 acceptance criteria. The overhead can be significant.</p><h3>OpenSpec (Fission AI)</h3><p>OpenSpec is the most lightweight of the three. It&#8217;s a TypeScript CLI with a fluid, iterative workflow and no rigid phase gates. Where Spec Kit enforces a strict sequence and Kiro wraps everything in an IDE, OpenSpec gets out of your way and lets you move between planning artifacts freely.</p><p>Its distinguishing philosophy is &#8220;brownfield first.&#8221; While the other tools are optimized for building new things from scratch, OpenSpec is designed to work with existing codebases. Each change produces a &#8220;spec delta,&#8221; a document that captures what&#8217;s being added, modified, or removed relative to the existing system. Over time, these deltas merge into a living specification that reflects the current state of the system.</p><p>OpenSpec also handles change history better. Every completed change is archived with its full artifact set: the original proposal, the spec deltas, the design, and the task list. This means you can go back and see not just what changed in the system, but <em>why</em> it changed, what alternatives were considered in the design, and what the original acceptance criteria were. Spec Kit and Kiro generate artifacts during planning but don&#8217;t have the same structured archive and merge cycle. In OpenSpec, the <code>openspec/changes/archive/</code> directory becomes a chronological record of every significant change to the system, and the <code>openspec/specs/</code> directory is always the merged, current truth. For regulated environments where auditability matters, this distinction is significant.</p><p><strong>Strengths:</strong> Works with 20+ AI tools including Claude Code, Cursor, Copilot, Windsurf, and many others. The brownfield focus is valuable in our context as most real work is on existing codebases. Fluid workflow lets you update any artifact at any time and you are not forced into a linear way of working. The archive/merge cycle produces both a living spec and an auditable change history.</p><p><strong>Weaknesses:</strong> Less hand holding in the spec writing process is the trade-off it makes while allowing you to navigate back-and-forth between spec and implementation. The tool is newer and the ecosystem is still growing.</p><div><hr></div><h2>Installing OpenSpec</h2><p>OpenSpec requires Node.js 20.19.0 or higher.</p><p>Install OpenSpec globally:</p><pre><code>npm install -g @fission-ai/openspec@latest
</code></pre><p>Then navigate to your project directory and initialize:</p><pre><code>cd your-project
openspec init
</code></pre><p>The init process will ask which AI tool you&#8217;re using and configure the appropriate slash commands or agent instructions for your environment.</p><p>OpenSpec also works with pnpm, yarn, bun, and nix. See the <a href="https://github.com/Fission-AI/OpenSpec/blob/main/docs/installation.md">official installation docs</a> for alternative paths.</p><h3>Keeping OpenSpec Updated</h3><p>Upgrade the package:</p><pre><code>npm install -g @fission-ai/openspec@latest
</code></pre><p>Then refresh agent instructions in each project:</p><pre><code>openspec update
</code></pre><div><hr></div><h2>OpenSpec&#8217;s Workflow in Depth</h2><p>Understanding the full lifecycle of an OpenSpec change is worth the time, because the artifacts it generates serve different roles on the team in different ways.</p><h3>The Core Commands</h3><p>OpenSpec&#8217;s workflow is built around the <code>opsx</code> slash commands. Here&#8217;s the complete set, the ones you interact with the most are bolded:</p><p>CommandPurpose<code>/opsx:onboard</code>Guided tutorial through the complete workflow using real code<code>/opsx:explore</code>Think through ideas, investigate problems, clarify requirements before committing to a change<code>/opsx:new</code>Create a new change folder with metadata<code>/opsx:continue</code>Progress a change to its next phase (proposal &#8594; design &#8594; tasks)<code>/opsx:ff</code>&#8220;Fast forward&#8221;: generate all planning artifacts at once<code>/opsx:apply</code>Implement tasks, writing code and checking off items<code>/opsx:verify</code>Validate that implementation matches the artifacts (completeness, correctness, coherence)<code>/opsx:sync</code>Merge delta specs into main specs without archiving (useful for long running changes)<code>/opsx:archive</code>Archive a completed change, merging delta specs into main specs<code>/opsx:bulk-archive</code>Archive multiple completed changes at once, handling spec conflicts</p><p>The typical flow is <code>new &#8594; ff &#8594; apply &#8594; archive</code>, but the power of OpenSpec is that you can break out of that sequence at any point. Need to revisit the design after you&#8217;ve started implementing? Just edit <code>design.md</code>. Want to add acceptance criteria while coding? Update the spec delta. There are no phase gates forcing you to &#8220;finish&#8221; one stage before moving to another.</p><h3>Starting a Change: Explore vs. New</h3><p>One of the first decisions in any OpenSpec workflow is how you enter it. There are two entry points, and choosing the right one makes a real difference in the quality of what comes out the other side.</p><p><code>/opsx:new</code><strong> is for when you know what you&#8217;re building.</strong> You have a clear feature in mind, you understand the requirements well enough to describe them, and you&#8217;re ready to start generating planning artifacts. Maybe you&#8217;ve already discussed this in a planning meeting. Maybe you&#8217;ve built something similar before. Maybe the ticket is well defined and you just need to formalize it. In these cases, <code>/opsx:new add-feature-name</code> followed by <code>/opsx:ff</code> gets you from zero to a full set of planning documents in minutes.</p><pre><code>You: /opsx:new add-payment-retry-with-exponential-backoff

AI:  Created openspec/changes/add-payment-retry-with-exponential-backoff/
     Ready to create: proposal

You: /opsx:ff

AI:  Creating all planning artifacts...
     &#10003; proposal.md
     &#10003; specs/
     &#10003; design.md
     &#10003; tasks.md
     Ready for implementation!
</code></pre><p>This works well when the problem space is familiar and constraints are understood. You&#8217;ve implemented retry logic before, you know the gateway&#8217;s behavior, you just need to formalize it.</p><p><code>/opsx:explore</code><strong> is for when you&#8217;re still figuring things out.</strong> Maybe you have a vague idea but the requirements aren&#8217;t clear. Maybe you&#8217;re investigating a production anomaly and you don&#8217;t know where the root cause is yet. Maybe a stakeholder said &#8220;we need to support instant payments&#8221; and you need to figure out what that actually means for your settlement architecture before you can spec anything.</p><p>Explore opens a broader, open ended conversation with the AI <em>before</em> any change folder is created and before any artifacts are generated. There&#8217;s no structure imposed, no proposal template to fill out, no pressure to define acceptance criteria. You&#8217;re just thinking out loud with an assistant that has context on your codebase.</p><pre><code>You: /opsx:explore

AI:  What would you like to explore?

You: We&#8217;re seeing intermittent failures on our direct debit 
     collections via the pain.008 pathway. Some mandates are 
     being rejected with AM04 (insufficient funds) but the 
     retry logic doesn&#8217;t seem to be kicking in correctly. I&#8217;m 
     not sure if it&#8217;s a status mapping issue, a timing problem 
     with our batch processing window, or something in our 
     mandate lifecycle management. I want to figure out where 
     to focus before committing to a specific fix.

AI:  Let me help investigate. Based on your codebase, I can 
     see a few potential areas...
     [Analyzes code paths, suggests profiling approaches, 
      identifies likely bottlenecks, asks clarifying questions]
</code></pre><p>The explore conversation might last five minutes or an hour. It might branch into several tangents before converging on a clear problem statement. That&#8217;s intentional. You&#8217;re doing the messy, nonlinear thinking that produces good requirements, and you&#8217;re doing it before the spec exists rather than discovering gaps during implementation.</p><p>This is also where existing requirements documents from business analysts become valuable. In payments organizations, BAs often produce detailed feature requirements independently of any SDD tool: field mapping spreadsheets, business rule matrices, sample ISO 20022 payloads (pain.001, pain.008, pacs.008), regulatory constraint documents, and workflow diagrams. These artifacts are the raw material that explore turns into actionable specs.</p><p>Rather than asking the AI to invent requirements from scratch, you feed it the BA&#8217;s work:</p><pre><code>You: /opsx:explore

AI:  What would you like to explore?

You: Our BA has written up requirements for adding SEPA Instant 
     Credit Transfers. I&#8217;m attaching their document which 
     includes the field mappings from our internal format to 
     pacs.008, the business rules for amount limits and BIC 
     validation, and sample XML payloads. I need to understand 
     how this fits into our existing payment orchestration 
     layer and what the technical implications are before we 
     spec the implementation.

AI:  I&#8217;ve reviewed the BA requirements document. Let me walk 
     through the key integration points...
     [Maps BA requirements against existing codebase, identifies 
      gaps, flags technical decisions that need to be made]
</code></pre><p>The explore phase becomes a bridge between the BA&#8217;s domain knowledge and the engineering reality of the codebase. The BA doesn&#8217;t need to know about your GenServer architecture or your Ecto schema conventions. The developer doesn&#8217;t need to memorize the ISO 20022 payload structure. Explore lets both perspectives converge into a proposal that reflects both business intent and technical feasibility.</p><p>When you&#8217;ve reached clarity, you transition naturally into the structured workflow:</p><pre><code>You: OK, the main complexity is in the real-time settlement 
     confirmation flow. The BA&#8217;s field mappings look solid 
     but we need to add timeout handling for the 10 second 
     SCT Inst window. Let&#8217;s spec that.

You: /opsx:new add-sepa-instant-credit-transfers

AI:  Created openspec/changes/add-sepa-instant-credit-transfers/
     Ready to create: proposal
</code></pre><p>Now the proposal and specs will be grounded in both the BA&#8217;s requirements and the technical understanding you built during exploration, rather than being generated from a one line prompt.</p><p><strong>When to use which:</strong></p><p>Use <code>/opsx:new</code> when you can describe the feature or fix in a sentence and you&#8217;re confident in the scope. Use <code>/opsx:explore</code> when any of the following are true: you&#8217;re unsure what the root cause of a problem is, the requirements are ambiguous or underspecified, you need to evaluate multiple approaches before committing to one, or you want to pressure test an idea before investing in formal planning. In practice, we find ourselves using explore more often than we initially expected. The few minutes spent thinking before speccing consistently produce better specs, which in turn produce better code.</p><h3>The Artifact Lifecycle</h3><p>When you run <code>/opsx:new add-idempotent-refunds</code>, OpenSpec creates a change directory:</p><pre><code>openspec/changes/add-idempotent-refunds/
&#9500;&#9472;&#9472; .openspec.yaml          # Metadata: change name, status, timestamps
&#9492;&#9472;&#9472; (ready for artifacts)
</code></pre><p>Running <code>/opsx:ff</code> (or stepping through with <code>/opsx:continue</code>) generates the planning artifacts:</p><pre><code>openspec/changes/add-idempotent-refunds/
&#9500;&#9472;&#9472; .openspec.yaml
&#9500;&#9472;&#9472; proposal.md             # Why we&#8217;re doing this, what&#8217;s changing, scope
&#9500;&#9472;&#9472; specs/                  # Requirements and scenarios (the spec delta)
&#9474;   &#9492;&#9472;&#9472; refunds/
&#9474;       &#9492;&#9472;&#9472; spec.md         # Functional requirements with ADDED/MODIFIED/REMOVED markers
&#9500;&#9472;&#9472; design.md               # Technical approach, data model, component structure
&#9492;&#9472;&#9472; tasks.md                # Ordered implementation checklist
</code></pre><p>Each of these artifacts has a specific purpose and a specific audience. Let&#8217;s look at what goes into them.</p><p><strong>proposal.md</strong> is the &#8220;why&#8221; document. It describes the motivation for the change, the scope of what&#8217;s included and excluded, and any constraints or dependencies. This is the document you&#8217;d share in a planning meeting or attach to a ticket. It answers the question: &#8220;Why are we doing this, and what does &#8216;done&#8217; look like at a high level?&#8221; For a refunds feature, this might capture that the driver is duplicate refund incidents costing the business money, that the scope includes full and partial refunds but excludes chargebacks, and that the constraint is backwards compatibility with the existing refund API contract.</p><p><strong>specs/</strong> contains the spec delta, the functional requirements for this specific change. Requirements are marked as <code>ADDED</code>, <code>MODIFIED</code>, or <code>REMOVED</code> relative to the current system. Each requirement uses structured language (&#8221;The system SHALL...&#8221;) with clear acceptance criteria and scenarios. This is where edge cases live. This is where you define what happens when a refund is submitted with the same idempotency key as a previous request, what the system does when the gateway returns a timeout mid refund, or how partial refunds interact with the original transaction&#8217;s settlement status.</p><p><strong>design.md</strong> is the technical blueprint. It covers the data model, API contracts, component architecture, sequence flows, and any technology choices specific to this feature. For the refunds example, it&#8217;s where you&#8217;d document the idempotency key storage strategy, the state machine transitions for refund lifecycle, and the gateway adapter interface for multi acquirer support.</p><p><strong>tasks.md</strong> breaks the work into discrete, ordered implementation steps. Each task is small enough to verify independently, ideally something that can be implemented in under 30 minutes. Tasks have clear completion criteria so both the developer and the AI know when they&#8217;re done.</p><h3>What Happens at Archive</h3><p>When all tasks are complete and verified, <code>/opsx:archive</code> does something important: it merges the spec deltas from the change back into the main <code>openspec/specs/</code> directory. The change folder moves to <code>openspec/changes/archive/</code>, preserving the history. The main specs now reflect the updated state of the system.</p><p>This is the mechanism that turns specs into a living document. After a dozen features have been built and archived, <code>openspec/specs/</code> contains a comprehensive, up to date description of what the system does. Not what it was designed to do originally, but what it actually does right now.</p><div><hr></div><h2>Who Benefits: SDD Across Roles</h2><p>One of the underappreciated aspects of spec driven development is that the artifacts aren&#8217;t just for the developer writing the code. They create value across every role that touches the project.</p><h3>For Developers</h3><p>The immediate benefit is implementation quality. Instead of translating a vague Jira ticket into code via a series of increasingly frustrated prompts, you&#8217;re working from a spec that already captures requirements, edge cases, and technical decisions. The AI produces better code because it has better context. You spend less time debugging and reworking because misunderstandings surface during spec review, not during code review.</p><p>The longer term benefit is onboarding and maintenance. When you come back to a feature six months later, or when a new developer joins the team, the spec explains not just what the code does but <em>why</em> it was built that way. The proposal captures the business motivation. The design doc captures the technical rationale. The spec captures the behavioral contract.</p><h3>For Business Analysts and Product Managers</h3><p>The proposal and spec artifacts are written in structured natural language, not code. A BA or PM can read <code>proposal.md</code> and immediately understand the scope, motivation, and acceptance criteria for a change without needing to parse a pull request.</p><p>More importantly, they can contribute to these documents. If the spec says &#8220;The system SHALL retry failed direct debit collections up to 3 times&#8221; and the BA knows the scheme rules mandate a maximum of 2 retries with specific interval requirements, they can flag that in the spec before any code is written. The spec becomes a shared contract between product and engineering, reviewable by both sides.</p><p>BAs in payments organizations often produce detailed requirements documents that exist outside of any development tool: field mapping spreadsheets between internal formats and ISO 20022 messages, business rule matrices for transaction routing, sample payloads for pain.001 or pacs.008 messages, regulatory constraint documents, and scheme specific validation rules. These documents don&#8217;t need to be rewritten into OpenSpec format. Instead, they serve as input to the <code>/opsx:explore</code> conversation and as reference material that the proposal and specs can point to. The spec might say &#8220;Field mappings follow the BA&#8217;s pain.008 mapping document (see docs/ba-requirements/sepa-dd-field-mappings.xlsx)&#8221; rather than duplicating that content. OpenSpec captures the engineering requirements; the BA&#8217;s documents capture the domain requirements. The two reference each other.</p><p>For teams practicing any kind of requirements analysis, the spec delta format (ADDED/MODIFIED/REMOVED) maps naturally to how BAs think about change impact. You can see at a glance exactly what existing behavior is changing and what&#8217;s new.</p><h3>For QA Engineers</h3><p>The specs are essentially test plans waiting to happen. Each requirement with its acceptance criteria maps directly to test cases. &#8220;WHEN a refund is submitted with an idempotency key matching a previously completed refund, THEN the system SHALL return the original refund response without processing a duplicate&#8221; is a test case in all but name.</p><p>QA can review specs before implementation begins, catching gaps in test coverage at the cheapest possible point in the development cycle. In payments, where edge cases around timeouts, partial failures, and concurrent operations are where bugs hide, having QA eyes on the spec early is especially valuable. They can also use specs to verify completeness: does the implementation actually cover every scenario in the spec? OpenSpec&#8217;s <code>/opsx:verify</code> command automates part of this check, but human QA review of the spec itself is where the real value lies.</p><h3>For Tech Leads and Principal Engineers</h3><p>The design document is where architectural oversight happens. A principal can review <code>design.md</code> to ensure the proposed approach fits the system&#8217;s overall architecture, without needing to wait for a code review to discover that someone introduced a new database table that duplicates an existing one, or bypassed the payment gateway abstraction layer by calling the acquirer API directly.</p><p>The proposal document is equally valuable at this level. It provides enough context to make prioritization decisions, estimate impact on downstream systems like settlement and reconciliation, and flag dependencies before work begins.</p><p>For organizations running architecture review boards or design review processes, OpenSpec artifacts slot directly into those workflows. The artifacts are markdown in version control, which means they can be reviewed through the same pull request process as code.</p><h3>For the Whole Team</h3><p>The <code>openspec/specs/</code> directory, the living spec that accumulates as changes are archived, becomes something like institutional memory for the project. It captures not just the current state of the system but the evolution of requirements over time. New team members can browse the specs to understand the system. Archived changes provide an audit trail of what changed, when, and why.</p><p>This is especially valuable for distributed teams where not everyone is in every meeting. The spec is always available, always current, and always in the repo.</p><div><hr></div><h2>Bridging BA Requirements and Engineering Specs</h2><p>In most payments organizations, business analysts produce detailed requirements documents long before any developer opens an IDE. These documents are the product of weeks of domain analysis: field mapping spreadsheets that map internal data structures to ISO 20022 message formats like pain.001 or pacs.008, business rule matrices that define transaction routing logic, validation rules derived from scheme specifications, sample XML payloads, regulatory constraint summaries, and workflow diagrams for processes like mandate lifecycle management or chargeback dispute flows.</p><p>These artifacts are enormously valuable. They represent concentrated domain knowledge that no AI model has in its training data. But they exist outside of any development tool, usually in Confluence pages, shared drives, or Excel files. The question is: where do they plug into the SDD workflow?</p><p>The answer is <code>/opsx:explore</code>.</p><p>Rather than asking the AI to invent payment domain requirements from scratch (which it will get wrong in subtle, dangerous ways), you feed the BA&#8217;s existing documents into the explore conversation. The AI reads the BA&#8217;s field mappings, business rules, and sample payloads, then maps them against your actual codebase. The explore conversation becomes a structured dialogue about feasibility: which business rules can be implemented directly, which ones require architectural changes, where the BA&#8217;s requirements conflict with existing behavior, and what edge cases the BA may not have considered from a technical perspective.</p><pre><code>You: /opsx:explore

AI:  What would you like to explore?

You: The BA team has finalized requirements for adding SEPA 
     Direct Debit collections. I&#8217;m attaching their document 
     which includes the pain.008 field mappings from our 
     internal mandate format, the business rules for collection 
     timing windows, the R-transaction handling matrix (AM04, 
     MS02, MD01 etc.), and sample XML for both first and 
     recurring collections. I need to understand what this 
     means for our payment orchestration layer before we plan 
     the implementation.

AI:  I&#8217;ve reviewed the BA&#8217;s requirements. Let me walk through 
     the integration points with your existing codebase...
     
     Your mandate schema already covers most of the creditor 
     reference fields, but the BA&#8217;s mapping requires three 
     additional fields for the CreditorSchemeIdentification 
     that aren&#8217;t in your current model...
     
     The R-transaction handling matrix defines 12 reason codes 
     but your existing rejection handler only supports 4 
     generic rejection states. This will need a more granular 
     status model...
</code></pre><p>This conversation might reveal that 70% of the BA&#8217;s requirements map cleanly to existing patterns and 30% require new design decisions. Those design decisions then flow into the proposal and spec with full context, rather than being invented by the AI from a one sentence prompt.</p><p>The key principle: <strong>the BA&#8217;s documents are input to the spec, not replaced by it.</strong> The OpenSpec proposal can reference them directly (&#8221;Field mappings follow the BA&#8217;s pain.008 mapping document, see docs/ba-requirements/sepa-dd-field-mappings.xlsx&#8221;). The spec captures the engineering interpretation of business requirements, while the BA&#8217;s artifacts remain the authoritative source for domain rules. The two complement each other.</p><p>For teams with a strong BA function, this workflow turns explore into the most valuable step in the entire process. It&#8217;s where domain expertise meets technical reality, and where misunderstandings between product and engineering get caught before they become expensive.</p><div><hr></div><h2>Beyond Epics and User Stories</h2><p>For years, the standard way to decompose work in software organizations has been the Agile hierarchy: Epics break into Features, Features break into User Stories, User Stories break into Tasks. Each layer adds structure, and each layer adds overhead. Grooming sessions to refine stories. Estimation ceremonies to assign points. Sprint planning to negotiate what fits. Story splitting when something is &#8220;too big.&#8221; Acceptance criteria written in Given/When/Then format.</p><p>This process was designed for a world where humans wrote every line of code, and work needed to be decomposed into pieces small enough for one developer to complete in a sprint. The granularity served a coordination function: if three developers are working on the same feature in parallel, you need clearly bounded units of work to avoid stepping on each other.</p><p>With AI agents handling the bulk of code generation, developers now work in significantly larger chunks. A feature that would have been split into 8 user stories with 24 tasks can be described as a single spec and implemented in one session. The AI doesn&#8217;t need two week sprints to context switch between stories. It doesn&#8217;t need story points to estimate effort. It doesn&#8217;t care whether a unit of work is a 3 or a 5. It needs a clear description of what to build and enough context to build it correctly.</p><p>The overhead of the old hierarchy was always significant. Ceremonies consume 15-30% of a team&#8217;s time. The BA writes detailed requirements and translates them into epics and stories. The tech lead estimates them. The developer re-interprets them during implementation. Each translation step is an opportunity for information loss.</p><p>SDD collapses this. Instead of an Epic with 5 Features containing 20 User Stories containing 60 Tasks, you have a proposal that captures the business intent, a spec that defines the requirements, a design that describes the technical approach, and a task list that the AI executes against. The entire chain from &#8220;why are we doing this&#8221; to &#8220;what code gets written&#8221; lives in one change folder, reviewable as a single unit. There&#8217;s no translation loss and the spec is the shared artifact that all of those roles read, review, and contribute to.</p><p>This doesn&#8217;t mean you abandon planning. It means the unit of planning shifts from &#8220;what can one developer finish in two days&#8221; to &#8220;what is the next coherent change to the system.&#8221; That change might be small (fix a settlement rounding error) or large (add multi currency support). The spec scales to fit either. And because the AI generates the task breakdown from the spec, you don&#8217;t need to spend planning meetings debating granularity. The tasks exist to guide the AI&#8217;s implementation, not to fill a sprint board.</p><p>SDD offers a natural landing spot for a switch to AI-first development. You keep the parts that matter (clear requirements, technical review, incremental delivery) and shed the parts that were only ever coordination scaffolding.</p><div><hr></div><h2>A Practical Workflow: Adding Multi Currency Settlement to a Payment Engine</h2><p>Let&#8217;s walk through a concrete example using OpenSpec to add multi currency settlement support to a payment processing platform.</p><p><strong>Step 1: Explore (optional but useful)</strong></p><p>If the requirements aren&#8217;t fully clear, or if you have BA created documents that need to be digested before planning, start with exploration:</p><pre><code>You: /opsx:explore
AI:  What would you like to explore?

You: We need to support settlement in multiple currencies. 
     Right now everything settles in the merchant&#8217;s home 
     currency, but we&#8217;re onboarding merchants in the EU who 
     need EUR settlement even when the cardholder pays in GBP.
     I&#8217;ve attached the BA&#8217;s requirements document which includes 
     the field mappings, currency pair rules, and the regulatory 
     constraints for cross border settlement. I need to 
     understand how this fits into our existing reconciliation 
     pipeline and whether our current batch architecture can 
     handle currency grouping.

AI:  I&#8217;ve reviewed the BA requirements document. Let me map 
     these against your existing settlement pipeline...
     [Analyzes BA requirements against codebase, identifies 
      integration points, flags technical decisions needed]
</code></pre><p>This step is for thinking, not committing. No change folder is created yet.</p><p><strong>Step 2: Create the change</strong></p><pre><code>You: /opsx:new add-multi-currency-settlement

AI:  Created openspec/changes/add-multi-currency-settlement/
     Ready to create: proposal
</code></pre><p><strong>Step 3: Generate planning artifacts</strong></p><p>If you have a clear picture of what to build, fast forward through all planning stages:</p><pre><code>You: /opsx:ff

AI:  Creating all planning artifacts...
     &#10003; proposal.md
     &#10003; specs/
     &#10003; design.md
     &#10003; tasks.md
     Ready for implementation!
</code></pre><p>Alternatively, use <code>/opsx:continue</code> to step through one phase at a time, reviewing and refining each artifact before moving to the next.</p><p><strong>Step 4: Review and refine the artifacts</strong></p><p>This is the most important step. Read the generated proposal, specs, and design. Push back on anything that doesn&#8217;t match your intent:</p><pre><code>You: The design proposes fetching FX rates at settlement time. 
     We need to lock the rate at authorization and store it on 
     the transaction record. Also the spec is missing the 
     requirement that settlement files must include both the 
     original transaction currency and the settlement currency.

AI:  I&#8217;ll update design.md to lock FX rates at authorization 
     and add the dual currency settlement file requirement 
     to the spec.
     [Updates artifacts]
</code></pre><p><strong>Step 5: Implement</strong></p><pre><code>You: /opsx:apply

AI:  Working through tasks...
     &#10003; 1.1 Add fx_rate and settlement_currency to transactions schema
     &#10003; 1.2 Create FX rate locking module with gateway adapter
     &#10003; 1.3 Update authorization flow to capture and persist rate
     &#10003; 2.1 Modify settlement batch builder for multi currency grouping
     &#10003; 2.2 Update reconciliation to match on settlement currency
     &#10003; 2.3 Add settlement file format with dual currency columns
     All tasks complete!
</code></pre><p><strong>Step 6: Verify</strong></p><pre><code>You: /opsx:verify

AI:  Checking implementation against artifacts...
     &#10003; All requirements covered
     &#10003; Design patterns followed
     &#10003; All tasks marked complete
     &#9888; Spec mentions &#8220;handle rate expiration gracefully&#8221; but 
       current implementation raises an unhandled error when 
       the cached rate is older than 30 minutes. Consider a 
       re-fetch with merchant notification.
</code></pre><p><strong>Step 7: Archive</strong></p><pre><code>You: /opsx:archive

AI:  &#10003; Merged specs into openspec/specs/
     &#10003; Archived to openspec/changes/archive/2026-02-24-add-multi-currency-settlement/
     Specs updated. Ready for the next feature.
</code></pre><h3>Handling Interrupts and Stopping Mid Implementation</h3><p>Real work isn&#8217;t linear. OpenSpec handles context switches gracefully because your plan lives in the artifacts, not in the AI&#8217;s memory.</p><p>One important detail: during <code>/opsx:apply</code>, you can stop the AI at any point while monitoring its output. If you see it heading in the wrong direction on task 1.3, you stop it, correct course, and resume. The task list in <code>tasks.md</code> tracks what&#8217;s been completed, so nothing about your plan is lost. This is fundamentally different from vibe coding, where stopping mid stream means losing context and hoping you can reconstruct where you were.</p><p>The same principle applies to bigger interrupts. You can pause an entire feature to handle something urgent:</p><pre><code>You: I&#8217;m working on multi-currency-settlement but we have an 
     urgent issue: the nightly batch is double-settling some 
     transactions.

You: /opsx:new fix-duplicate-settlement-batch

AI:  Created openspec/changes/fix-duplicate-settlement-batch/

You: /opsx:ff
     [Plans the fix]

You: /opsx:apply
     [Implements the fix]

You: /opsx:archive
     &#10003; Archived fix-duplicate-settlement-batch

You: Let&#8217;s get back to multi currency settlement.

You: /opsx:apply add-multi-currency-settlement
AI:  Resuming add-multi-currency-settlement...
     Picking up at task 2.2: Update reconciliation to match 
     on settlement currency...
</code></pre><p>The multi currency feature picks up exactly where it left off. The artifacts held the plan while you were away, whether that was five minutes or five days.</p><div><hr></div><h2>When NOT to Use Spec Driven Development</h2><p>SDD is not appropriate in all cases. Here are some cases where the overhead isn&#8217;t worth it:</p><p><strong>Quick bug fixes.</strong> If you know exactly what&#8217;s wrong and the fix is a one line change to a gateway timeout value, writing a spec is like filing a building permit to hang a picture frame. Just fix it.</p><p><strong>Exploratory prototyping.</strong> When you&#8217;re trying to figure out <em>what</em> to build, not how to build it, specs slow you down. Vibe coding is genuinely great for rapid exploration. If you&#8217;re prototyping a new merchant dashboard layout to see what feels right, just build it iteratively.</p><p><strong>Highly visual or interactive work.</strong> SDD tools are text based. If your feature is primarily about UI layout, animation, or interaction design, you&#8217;ll spend more time describing the visual result in markdown than you&#8217;d spend just building it with visual feedback (though pairing SDD with TideWave can work wonders for UI work).</p><p><strong>Trivial features.</strong> Updating an error message string, renaming a config key, bumping a dependency version. These don&#8217;t need a spec. Use your judgment about the complexity threshold.</p><p><strong>Rapidly changing requirements.</strong> If you&#8217;re in a phase where the payment scheme keeps revising the spec and requirements shift weekly, maintaining your own specs becomes overhead that fights against your pace. Get to stability first, then spec the features that need to stick.</p><p>The general rule: <strong>if you can hold the entire change in your head and verify it by looking at it, you probably don&#8217;t need a spec.</strong> If the change involves multiple files, multiple concerns, or behavior you can&#8217;t verify visually, a spec starts paying for itself.</p><div><hr></div><h2>What to Watch Out For</h2><p>Having used these tools and studied the experiences of others, here are the traps:</p><p><strong>Spec bloat.</strong> The AI loves to generate exhaustive specifications. A feature that would take you 30 minutes to implement can produce 800+ lines of markdown. You have to be disciplined about trimming specs to what&#8217;s actually useful. If you&#8217;re not reading the spec carefully, it&#8217;s worse than not having one because you&#8217;ll have false confidence that edge cases are covered when they&#8217;re not.</p><p><strong>The waterfall trap.</strong> SDD can slide into big design up front if you&#8217;re not careful and start bundling many features into one spec. If changing the spec feels expensive or bureaucratic, you&#8217;ve over formalized. OpenSpec&#8217;s fluid workflow helps here since there are no phase gates, but you still need the discipline to keep specs lightweight enough to throw away and rewrite if you find yourself going down the wrong path.</p><p><strong>Spec drift.</strong> The spec says one thing; the code does another. This happens when you make implementation fixes outside the spec workflow. Either update the spec when you deviate, or accept that the spec is aspirational rather than authoritative. OpenSpec&#8217;s <code>/opsx:sync</code> command can help keep specs aligned during long running changes.</p><p><strong>The AI ignores its own spec.</strong> This is a real and documented problem. Context windows are larger, but that doesn&#8217;t mean the AI attends to everything in them equally. People have reported that AI agents generate code that contradicts the spec they just wrote, creating duplicate classes, ignoring constraints, or implementing patterns the spec explicitly avoided. The <code>/opsx:verify</code> step exists specifically to catch this.</p><p><strong>Review fatigue.</strong> SDD adds a new category of artifact to review. You&#8217;re now reviewing specs AND code. If your team doesn&#8217;t value spec review as highly as code review, specs become rubber stamped documents that provide an illusion of rigour.</p><p><strong>Over application to small changes.</strong> The tooling doesn&#8217;t scale down well. Applying the full SDD workflow to a minor feature creates overhead that dwarfs the implementation time. You need a personal threshold for when to spec and when to just build.</p><div><hr></div><h2>The Waterfall Question</h2><p>Every discussion of SDD eventually arrives at the same question: isn&#8217;t this just waterfall with better marketing?</p><p>The comparison is fair to raise and unfair to leave unexamined. Traditional waterfall failed because of long feedback loops: months of design, months of implementation, and discovery at the end that the design didn&#8217;t match reality. The feedback cycle was measured in quarters.</p><p>SDD, practiced well, has feedback cycles measured in minutes to hours. You write a spec for a single feature, not an entire system. You review the generated design before implementation starts. You implement in small, verifiable tasks. And critically, changing the spec and regenerating is cheap. The whole point is that code is a derived artifact you can throw away and recreate.</p><p>SDD can slide into waterfall like rigidity if you treat specs as immutable, if the spec writing phase becomes its own bottleneck, or if you use SDD as a substitute for iterative discovery. As Gojko Adzic observed, the movement builds on solid intent-first ideas but could reintroduce rigidity if practitioners aren&#8217;t thoughtful about it.</p><p>The Thoughtworks perspective captures the nuance well: the problems of vibe coding come from being too fast, spontaneous, and haphazard, while the problems of waterfall come from being too slow, rigid, and disconnected from reality. SDD, when practiced well, occupies the middle ground. It provides a mechanism for shorter and more effective feedback loops than either extreme.</p><p>The honest answer is that SDD sits on a spectrum. At one end, you have &#8220;spec as lightweight sketch,&#8221; a quick outline that gives the AI direction without constraining it. At the other end, you have &#8220;spec as source of truth,&#8221; a comprehensive document that the code must conform to. OpenSpec&#8217;s fluid approach leans toward the lighter end of that spectrum, which is why it appeals to teams who want discipline without ceremony.</p><div><hr></div><h2>Pros and Cons</h2><h3>What SDD Gives You</h3><p><strong>Reduced rework.</strong> Catching misunderstandings at the spec level is dramatically cheaper than catching them in code. When a BA&#8217;s field mapping is wrong, you want to discover that while reviewing a proposal, not while debugging a failed settlement file at 2 AM.</p><p><strong>Persistent context.</strong> Specs survive session boundaries, tool switches, and team changes. Six months from now, when someone asks why the FX rate locking works the way it does, the spec and its proposal explain both the what and the why.</p><p><strong>Reviewable intent across roles.</strong> You can review a spec without reading any code. Product managers, BAs, QA, and principals can participate in spec review and catch requirement gaps before implementation begins. In a payments context, this means compliance can review the spec for regulatory alignment without needing to read Elixir.</p><h3>What SDD Costs You</h3><p><strong>Time upfront.</strong> Writing and reviewing specs takes time that vibe coding doesn&#8217;t require. For simple tasks, this overhead is pure cost with minimal benefit.</p><p><strong>False precision.</strong> Detailed specs can create an illusion of completeness. Just because the spec covers edge cases on paper doesn&#8217;t mean the AI will implement them correctly. You still need to test.</p><p><strong>Tool immaturity.</strong> These tools are all early stage. Expect rough edges, breaking changes, and workflow gaps. The ecosystem is moving fast, which means today&#8217;s best practices may be obsolete in six months.</p><div><hr></div><h2>Where This Is Heading</h2><p>Spec driven development is less than a year old as a named practice, and the tooling is evolving fast. The fundamental insight, that AI agents produce better code when given structured intent rather than ad hoc prompts, seems durable even if the specific tools don&#8217;t survive.</p><p>What&#8217;s interesting is the convergence. BDD (Behavior Driven Development), TDD (Test Driven Development), and now SDD all share the same DNA: define the desired behavior before writing the implementation. SDD is that idea adapted for a world where the implementer is an AI agent rather than a human developer.</p><p>The open question is whether specs will remain the domain of dedicated tools, or whether this discipline gets absorbed into the AI coding tools themselves. We&#8217;re already seeing Cursor, Claude Code, and Copilot add planning and multi step reasoning capabilities that accomplish some of what SDD tools do, without the explicit spec writing step.</p><p>For now, the practical takeaway is simple: if you&#8217;re doing anything more complex than a quick prototype with AI coding tools, some form of structured planning, whether you call it SDD or just &#8220;thinking before prompting,&#8221; will produce better results than vibing your way through it. The tools can help enforce that discipline, but the discipline itself is what matters.</p><p>The spec isn&#8217;t the point. The thinking is.</p>]]></content:encoded></item><item><title><![CDATA[It's a Great Time to be a Software Engineer]]></title><description><![CDATA[It's been two years of AI use and life couldn't be better.]]></description><link>https://bitbytebit.substack.com/p/its-a-great-time-to-be-a-software</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/its-a-great-time-to-be-a-software</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Wed, 07 Jan 2026 02:02:56 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/9ddec87c-dfb1-4cd0-b366-750211313593_2048x1536.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Published first <a href="https://zarar.dev/its-a-great-time-to-be-a-software-engineer/">here</a>.</p><p>Here are some thoughts on AI development based on my experience of the last two years. As with any list, these are in no particular order.</p><ol><li><p>Get excited. AI is only coming for your job if you treat it as an optional part of your job. It&#8217;s here to help you become a better and more efficient software engineer. Embrace it wholeheartedly just like you embraced IDEs in favour of text editors. Using AI doesn&#8217;t make you a lesser programmer and not using it doesn&#8217;t make you special in any way. In fact, not using it or resisting it makes you look out-of-touch. This is what you have been waiting for to love your job again, and it just might remind you that you got into this business because it feels great to create things, not necessarily code things.</p></li><li><p>Most code (upwards of 80%) should be AI generated at this point. If it&#8217;s not, there is something inherently flawed about your workflow. Just put your pride aside, and acknowledge that AI is a better programmer than you. Your coding skills are now worth little, but your software engineering skills are worth a lot more. Invest in the latter, don&#8217;t cling on to the former. AI code is still &#8220;your&#8221; code so you can take the same pride in it as you did before. You just learned how to type faster. A lot faster!</p></li><li><p>SRP, DRY, SOLID and clean design/code should be the focus of the programmer. Guiding AIs to get these right requires understanding the business context in which the software is being used, which AI doesn&#8217;t know. How a feature is expected to change in the future, and what trade-offs need to be made there is something you need to be an expert at. Do I create a new module? Is this method named appropriately? Is it taking too many parameters? Am I violating Demeter&#8217;s Law? Is this file getting too big? Should I separate these two concerns? What would make this more reusable? These are the decisions you should be spending time on. This requires understanding the product more than you needed to in the past. You&#8217;re not only a Software Engineer, you&#8217;re a Product Engineer, and that requires a deep understanding of something you may have ignored in the past.</p></li><li><p>Context management (or engineering) is where efficiencies are to be gained. If you find yourself repeating things to a forgetful AI, then that&#8217;s a problem to be solved. Simple solutions include <a href="https://code.claude.com/docs/en/skills">Claude Skills</a> and more sophisticated ones include using <a href="https://github.com/steveyegge/beads">Beads</a>. Your workflow should be constantly &#8220;saving&#8221; things to memory to make you more efficient. Sometimes I find myself frustrated by having to remind Claude that it needs to &#8220;do X first when it&#8217;s doing Y&#8221; - those rules should be codified. Don&#8217;t treat AGENTS.md or any other instruction file as a static document or it&#8217;ll waste your time. How to manage your own context (and your team&#8217;s) is something to dedicate time to. If you work in a large company, this is an especially interesting challenge as you have to balance alignment and autonomy, hard rule and guidelines, etc.</p></li><li><p>Everyone should read a book where you <a href="https://www.goodreads.com/book/show/209234015-build-a-large-language-model">build an LLM from scratch</a>. It&#8217;s going to be painful and, like me, you&#8217;re probably going to have to re-read chapters just to get it through your head (I did, many times), but when it does, you&#8217;ll be better off for it. Though chances are you&#8217;ll never develop your own LLM and probably use a frontier model most of the time, it helps knowing how things are working underneath the hood. You&#8217;ll need to tweak model parameters at some point in your career, and having this foundational knowledge will be the difference between winging it and knowing what you&#8217;re doing.</p></li><li><p>Code review is the new bottleneck. The good news is that we already have tools popping up that make this easier (e.g., <a href="https://www.coderabbit.ai/">Code Rabbit</a>). For reviewing code locally, multi-agent workflows work great. Having a separate agent contextualized to reviewing code for correctness, security etc. with rules and guidelines are easy to implement, e.g. <code>claude-code review --aspect "correctness" src/ &gt; /tmp/review_correctness.md</code>. If you&#8217;re not using multi-agent workflows, this is an easy place to start. Here&#8217;s a couple other candidates: 1) an agent dedicated to providing good commit messages based on <code>git diff</code>, 2) test refactoring agent which gets invoked to clean up tests; shoving test clean up rules into the &#8220;development&#8221; context may be too much, so having a separate focused agent will work better.</p></li><li><p>There is no excuse not to have clean code. Refactoring is cheap, writing tests is cheaper. If you have code that&#8217;s not clean, generate higher-level tests for it, and then ask the agent to refactor. The tests will serve as your guiding light on whether something went wrong. This is especially valuable in brownfield codebases where changes are the riskiest. Having dedicated workflows to &#8220;clean up code&#8221; is another example of easy to implement multi-agent workflows.</p></li><li><p>Documentation is free. Whether it be inline code documentation, architectural diagrams or Correction of Error analysis, what used to take days now takes minutes. There is simply no excuse not to have comprehensive and up-to-date documentation, both from a product and engineering point of view. Not only should your code describe what it does where clarity is needed, it should also indicate the business rules behind it (whether it be inline or linked to external docs). A programmer reading the code should have a single point of entry to understand both the design decisions and the context in which the customer is using it.</p></li><li><p>Cost optimization is now part of software engineering. Not every task needs Claude Opus, and knowing when to delegate to cheaper AIs is a skill. Even better, a free one like Qwen Code should be installed locally for simple tasks and basic CRUD operations (which is about 90% of all development). Complex refactoring with business context is worth the Opus pricing. You should have mental models about which model to reach for given the problem at hand. Track your AI costs per feature just like you&#8217;d track compute costs on AWS so you can optimize your workflow and not just the code. Running expensive models on trivial tasks is wasteful and unprofessional.</p></li><li><p>High-Level System Design is where you are needed. AI will crush implementation details but architectural decisions require human judgment that understands business constraints, team capabilities, and long-term maintenance burden. You need to get better at system design, understanding trade-offs between different architectural patterns, and making decisions that account for factors AI can&#8217;t know - like the fact your team hates microservices or that you&#8217;re planning to acquire a company next quarter. This is where your value multiplies.</p></li></ol>]]></content:encoded></item><item><title><![CDATA[Scope discipline when AI makes building fast]]></title><description><![CDATA[I wrote a feature which would take weeks in hours, but traps lurk everywhere.]]></description><link>https://bitbytebit.substack.com/p/scope-discipline-when-ai-makes-building</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/scope-discipline-when-ai-makes-building</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Tue, 23 Dec 2025 16:00:39 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/9b6d0874-cc33-4b06-9b7f-4dd9132c749d_2574x1930.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I needed to show users contextual messages, e.g., banners for announcements, modals for important actions, tours for onboarding. I already use PostHog for analytics and PostHog allows the user to create apps which can provide this functionality while being tightly integrated with their analytics capabilities.</p><p>But I built my own system instead. Here&#8217;s why, and why the hardest part wasn&#8217;t building but knowing when to stop.</p><h2>The Business Problem</h2><p>I run an event management platform. Venues use it to sell tickets, manage events, run marketing campaigns, create ads etc. The product has grown sophi . New features ship, but users don&#8217;t find them.</p><p>This isn&#8217;t a documentation problem as users don&#8217;t read docs. It&#8217;s not an email problem either. Announcement emails get 20% open rates if you&#8217;re lucky. The features exist and users need them. They just don&#8217;t know they&#8217;re there.</p><p>The real problem breaks down into specifics:</p><p><strong>Feature discovery.</strong> I ship something new, maybe a better way to handle refunds or a new analytics dashboard. The users who would benefit most never click on it because they don&#8217;t know it exists.</p><p><strong>Contextual nudges.</strong> A user logs in through SSO but hasn&#8217;t set a password. That&#8217;s fine for now, but if SSO breaks they&#8217;re locked out. I want to prompt them to set a password, but only when it&#8217;s relevant, not in an email they&#8217;ll ignore.</p><p><strong>Onboarding flows.</strong> New users need guidance. Not a wall of text. Step by step tours that show them where things are. &#8220;Click here to create your first event. Now add tickets. Now publish.&#8221;</p><p><strong>Multi-tenant complexity.</strong> This isn&#8217;t a simple user model. I have accounts (the venue), users within accounts (staff), and customers (ticket buyers). A message might be relevant to one account but not another. Dismissing a message as one user shouldn&#8217;t dismiss it for your colleague.</p><p><strong>Non-intrusive UX.</strong> Whatever I build needs to be easy to dismiss. Remember that the user dismissed it. Not show it again. Respect their attention.</p><p>These requirements shaped everything that followed.</p><h2>Why PostHog Was a Serious Candidate</h2><p>PostHog is the obvious choice due to it already being used and providing a way to track user behavior. It has capabilities that you can extend for this kind of thing and the AI was quick to suggest building a PostHog custom app which extended it&#8217;s core features to delivery on the following using these approaches:</p><p><strong>Surveys</strong> work as modal-style messages. Create a popover or modal, target by URL or user properties, built-in dismiss tracking. No code needed for basic use cases.</p><p><strong>Feature flags with payloads</strong> could drive banner content. The flag controls who sees the message. The payload contains the content. Evaluate client-side and render.</p><pre><code><strong>const</strong> flag = posthog.getFeatureFlag(&#8217;welcome-banner&#8217;)
<strong>const</strong> payload = posthog.getFeatureFlagPayload(&#8217;welcome-banner&#8217;)
<em>// payload: { title: &#8220;Welcome!&#8221;, content: &#8220;...&#8221;, style: &#8220;info&#8221; }</em>
</code></pre><p><strong>Site Apps</strong> let you write custom JavaScript that runs in PostHog&#8217;s context. Full control if surveys and flags aren&#8217;t enough.</p><p>I seriously considered this path. PostHog handles targeting UI, cohort management, percentage rollouts. That&#8217;s real value. The code was also already there and it was tempting to go with this but a pause was needed.</p><h2>Why PostHog Didn&#8217;t Work</h2><p>The problems started when I mapped PostHog&#8217;s features to my actual requirements.</p><p><strong>No dismissal tracking for feature flags.</strong> Surveys track dismissals automatically. Feature flags don&#8217;t. If I use flags for banners, I&#8217;d need to:</p><ol><li><p>Send a custom event when user dismisses: <code>posthog.capture('message_dismissed', { message_id: 'xyz' })</code></p></li><li><p>Create a cohort of users who have that event</p></li><li><p>Exclude that cohort from the feature flag</p></li><li><p>Repeat for every message</p></li></ol><p>That&#8217;s a lot of manual cohort management. It gets messy fast.</p><p><strong>No per-account targeting.</strong> PostHog targets users, not accounts. My multi-tenant model needs messages scoped to specific accounts. User A on Account X dismisses a message. User A on Account Y should still see it. User B on Account X should also still see it.</p><p>PostHog would require setting account_id as a user property, then creating cohorts per account. That doesn&#8217;t scale to hundreds of accounts.</p><p><strong>No path-based targeting for feature flags.</strong> Surveys can target by URL. Feature flags can&#8217;t. I&#8217;d need to check the path client-side:</p><pre><code><strong>if</strong> (window.location.pathname.startsWith(&#8217;/dashboard&#8217;)) {
  <strong>const</strong> flag = posthog.getFeatureFlag(&#8217;dashboard-message&#8217;)
}
</code></pre><p>That works, but now I&#8217;m writing conditional logic in JavaScript for every message. The targeting that should be configuration becomes code.</p><p><strong>No tours.</strong> PostHog has nothing like driver.js. No step-by-step walkthroughs. I&#8217;d integrate driver.js separately and use feature flags to control when tours trigger. At that point I&#8217;m building half the system myself anyway.</p><p><strong>Server-side control.</strong> My app is Phoenix LiveView. I&#8217;ve worked hard to keep logic server-side. Adding PostHog&#8217;s JavaScript SDK for messaging means rendering decisions happen in the browser. State lives in two places. Debugging gets harder.</p><p><strong>The dependency question.</strong> PostHog is great today. But SaaS products change pricing, get acquired, pivot. Messaging is core infrastructure for my product. If PostHog changed their pricing model or discontinued a feature, I&#8217;d need to rebuild under pressure. Owning it from the start avoids that risk.</p><p>My decision: custom for system messages and tours, PostHog for surveys and A/B tests where their tooling genuinely adds value. Hybrid approach and the right tool for the job.</p><p>This was an example of AI confidently providing very reasonable sounding options, but without someone sitting down and mapping the requirements to the solution with a view to long-term maintenance, one would have easily gone down a reasonable but dangerous path. In my view, software engineering knowledge is key and without that chaos will ensue. It&#8217;s why I&#8217;m genuinely liking the <em>Product Engineer</em> title which is being thrown around. Maybe more on that some other time</p><h2>Brainstorming with AI</h2><p>I used Claude to brainstorm the system. This is where things got dangerous.</p><p>The initial ideas list was ambitious:</p><ul><li><p>Snooze and remind later with configurable intervals</p></li><li><p>Message dependencies (show X only after Y is dismissed)</p></li><li><p>Role-based targeting</p></li><li><p>Feature flag integration</p></li><li><p>Time-bounded messages with start and end dates</p></li><li><p>Event-triggered auto-dismissal</p></li><li><p>Multiple dismiss behaviors (close, complete, snooze)</p></li></ul><p>AI is good at generating possibilities. Too good. Every feature seemed reasonable and each one addressed a real use case. The ideas kept expanding the deeper I went into exploration.</p><p>Here&#8217;s the thing about AI-assisted development: it makes building fast. Features that would take days take hours and even though this sounds like a benefit, it&#8217;s actually a trap.</p><p>When building is slow, you naturally filter ideas. &#8220;That would take a week&#8221; is a forcing function for scope and you have a tendency to avoid it. When building is fast, that filter disappears and it becomes an extra hour. Suddenly, you realize that you&#8217;ve been going back-and-forth on things you may not need because it&#8217;s so easy to cover this case and that case.</p><p>I had to keep asking myself one question: Is this the problem that I wanted to solve when I started this feature?</p><p>The problem was users not knowing about new features. That&#8217;s it. Show a message. Let them dismiss it. Everything else is optimization for problems I don&#8217;t have yet.</p><p>The ideas list is just that, a bunch of half-thought ideas, not a backlog. Not a roadmap. Not a commitment. When someone says &#8220;you should add snooze functionality&#8221; I can say &#8220;that&#8217;s on the ideas list&#8221; without it meaning anything about when or if I&#8217;ll build it.</p><p>Backlogs should be short. If it&#8217;s not solving the current problem it doesn&#8217;t belong there.</p><h2>The Temptation of Clean Code</h2><p>The hardest moment wasn&#8217;t writing features, but trying to figure out the code I was going to delete even though it worked. As an example, Claude had generated a complete implementation of snooze functionality. The schema changes were done, the UI was wired up and test cases were written as well. The code was clean:</p><pre><code><strong>defp</strong> handle_messaging_event(&#8221;snooze_message&#8221;, %{&#8221;message-id&#8221; =&gt; id, &#8220;days&#8221; =&gt; days}, socket) <strong>do</strong>
  scope = socket.assigns[:current_scope]
  account_id = socket.assigns[:messaging_account_id]
  snooze_until = <strong>DateTime</strong>.add(<strong>DateTime</strong>.utc_now(), <strong>String</strong>.to_integer(days), :day)

  <strong>Messaging</strong>.snooze_message(id, scope, account_id, snooze_until)

  updated_messages = remove_message_from_assigns(socket.assigns.app_messages, id)
  {:halt, assign(socket, :app_messages, updated_messages)}
<strong>end</strong>
</code></pre><p>It looked good and it worked. I was so close to having this capability and was tempted to just commit it and move on.</p><p>But I didn&#8217;t need it, at least not right now and just because the code is in front of you doesn&#8217;t mean you have to take it. That&#8217;s the real danger of AI-assisted development. It&#8217;s not that the code is bad. Often it&#8217;s excellent. The danger is that good code is seductive. You want to keep it. You rationalize why you might need it someday. You commit it &#8220;just in case.&#8221;</p><p>This is the moment where you have to view clean code as technical debt, not because there&#8217;s something wrong with the code, but because it adds to your maintenance burden without an immediate benefit. The potential benefit is in the future and once you discount it by time, it becomes a bad idea to keep it. Especially when it was so easy to generate in the first place.</p><h2>MVP Scope</h2><p>What I kept:</p><ul><li><p>Banners and modals (two display types cover most cases)</p></li><li><p>Path-based targeting (show message only on certain pages)</p></li><li><p>Function-based targeting (custom Elixir rules for complex conditions)</p></li><li><p>Per-user and per-account dismissals (remember who saw what)</p></li></ul><p>What I cut:</p><ul><li><p>Snooze and remind later (just dismiss it)</p></li><li><p>Role-based targeting (function rules can check roles if needed)</p></li><li><p>Message dependencies (not needed for announcing features)</p></li><li><p>Feature flag integration (can add later)</p></li><li><p>Time-bounded messages (same)</p></li></ul><p>The JSON targeting field was my escape hatch. It means I can add role targeting, feature flags, and dependencies later without database migrations. I&#8217;m not saying no to these features. I&#8217;m saying not yet. And &#8220;not yet&#8221; is fine because none of them solve the problem of telling users about new features.</p><h2>Implementation</h2><p>Two patterns carry the system.</p><p><strong>Global event handling with attach_hook.</strong> This is where AI genuinely helped. I needed dismiss events to work from any LiveView without adding handlers to each one. Claude suggested <code>attach_hook</code> for handle_event:</p><pre><code><strong>def</strong> on_mount(:default, _params, session, socket) <strong>do</strong>
  <strong>if</strong> connected?(socket) <strong>do</strong>
    socket
    |&gt; assign(:app_messages, %{})
    |&gt; assign(:messaging_account_id, session[&#8221;account_id&#8221;])
    |&gt; attach_hook(:messaging_params, :handle_params, &amp;handle_messaging_params/3)
    |&gt; attach_hook(:messaging_events, :handle_event, &amp;handle_messaging_event/3)
  <strong>end</strong>
<strong>end</strong>

<strong>defp</strong> handle_messaging_event(&#8221;dismiss_message&#8221;, %{&#8221;message-id&#8221; =&gt; message_id}, socket) <strong>do</strong>
  scope = socket.assigns[:current_scope]
  account_id = socket.assigns[:messaging_account_id]

  <strong>if</strong> scope &amp;&amp; account_id <strong>do</strong>
    <strong>Messaging</strong>.dismiss_message(message_id, scope, account_id)
  <strong>end</strong>

  updated_messages = remove_message_from_assigns(socket.assigns.app_messages, message_id)
  {:halt, assign(socket, :app_messages, updated_messages)}
<strong>end</strong>

<strong>defp</strong> handle_messaging_event(_event, _params, socket), do: {:cont, socket}
</code></pre><p>The elegance here is the fallthrough. Events this hook doesn&#8217;t care about return <code>{:cont, socket}</code> and flow to the LiveView&#8217;s own handlers. Events it does handle return <code>{:halt, socket}</code> and stop there. One module handles all messaging events globally. Individual LiveViews don&#8217;t know messaging exists.</p><p>This pattern is powerful. Add the <code>on_mount</code> to a <code>live_session</code> and every page in that session gets messaging. No changes to individual LiveViews. No copy-paste of event handlers. The layout renders banners and modals from <code>@app_messages</code>. The hook handles dismissals. Everything works.</p><p><strong>Function-based rules.</strong> This is where the power lives. I&#8217;m in a functional programming environment so everything should boil down to a simple function call with no side effects:</p><pre><code><strong>def</strong> evaluate(func_name, scope, _account_id, _message_id) <strong>do</strong>
  <strong>case</strong> func_name <strong>do</strong>
    &#8220;has_no_password&#8221; -&gt; has_no_password(scope)
    _ -&gt; :show
  <strong>end</strong>
<strong>end</strong>

<strong>def</strong> has_no_password(scope) <strong>do</strong>
  <strong>case</strong> scope <strong>do</strong>
    %{unified_user: %{hashed_password: nil}} -&gt; :show
    _ -&gt; :hide
  <strong>end</strong>
<strong>end</strong>
</code></pre><p>Adding a new rule means adding a function. The function receives context (user, account, message) and returns <code>:show</code> or <code>:hide</code>. No configuration files. No complex DSL. Just functions.</p><p>Need to check if a user has created events? Write a function. Need to check subscription tier? Write a function. The <code>MessageRules</code> module becomes a library of predicates that the messaging system evaluates.</p><h2>Lessons</h2><p><strong>Right tool for the right job.</strong> I didn&#8217;t replace PostHog entirely. I use it for surveys and A/B tests where their tooling adds value. For core messaging that touches my domain model, custom won. The hybrid approach means I get the best of both.</p><p><strong>AI makes scope discipline harder, not easier.</strong> Claude will build whatever you ask for. That&#8217;s the problem. The speed removes the natural friction that used to prevent scope creep. You have to actively ask: is this the current problem I&#8217;m solving? If not, it goes on the ideas list, not the backlog.</p><p><strong>Good code is seductive.</strong> The hardest code to delete is code that works. AI generates clean, functional implementations quickly. The temptation is to keep them. Resist. Just because it&#8217;s in front of you doesn&#8217;t mean you have to take it.</p><p><strong>Technical patterns matter.</strong> The <code>on_mount</code> hook and <code>attach_hook</code> pattern solved a hard problem: how do you add cross-cutting behavior to every LiveView without modifying each one? Understanding LiveView&#8217;s lifecycle deeply made this possible.</p><p><strong>Functions over configuration.</strong> In a functional language, the most flexible targeting system is just a function. No complex rule engines. No JSON DSL. Just scope in, show or hide out. Everything else is syntax sugar over that core idea.</p>]]></content:encoded></item><item><title><![CDATA[Embedding-Based Tool Selection for AI Agents]]></title><description><![CDATA[Simple approach to minimizing context size when tooling increases]]></description><link>https://bitbytebit.substack.com/p/embedding-based-tool-selection-for</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/embedding-based-tool-selection-for</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Sun, 21 Dec 2025 17:36:56 GMT</pubDate><content:encoded><![CDATA[<p>When I first built our AI assistant, it had five tools. Look up an order. Process a refund. Check ticket availability. Simple stuff. Fast forward six months and we&#8217;re at nearly 40 tools spanning orders, events, marketing campaigns, contests, and customer management.</p><p>The problem became obvious during a routine cost review: we were burning thousands of tokens on every single request just describing tools the model would never use. Someone asks &#8220;What time does my show start?&#8221; and we&#8217;re sending the full spec for <code>process_refund</code>, <code>create_email_campaign</code>, and <code>manage_contest_prizes</code>. Wasteful.</p><h2>The Tool Explosion Problem</h2><p>Each tool definition isn&#8217;t trivial. You need a name, a description detailed enough for the LLM to understand when to use it, and parameter specifications with types and constraints. Here&#8217;s what one looks like in our codebase:</p><pre><code>%<strong>ToolDefinition</strong>{
  name: &#8220;process_refund&#8221;,
  description: &#8220;&#8221;&#8220;
  Process a refund for a specific order. Validates the refund amount
  against the original order total and available balance. Requires
  order_id from get_order_details. Returns confirmation with refund ID.
  &#8220;&#8221;&#8220;,
  parameters: [
    %{name: &#8220;order_id&#8221;, type: :string, required: true},
    %{name: &#8220;amount&#8221;, type: :number, required: true},
    %{name: &#8220;reason&#8221;, type: :string, required: false}
  ],
  handler: {<strong>RefundsRegistry</strong>, :handle_process_refund},
  category: :refunds
}
</code></pre><p>Multiply by 40 and you&#8217;re looking at 3,000+ tokens before the user even says anything. The costs add up, latency increases, and here&#8217;s the kicker: having too many tools actually makes the model worse at picking the right one. More noise, more confusion.</p><h2>Semantic Selection with Embeddings</h2><p>The fix is conceptually simple. Instead of sending every tool on every request, we embed all tool descriptions into vectors and store them in Postgres using pgvector. When a query comes in, we embed it too, then find the 5-10 most semantically similar tools using cosine distance.</p><p>The query &#8220;refund order #12345&#8221; gets embedded, compared against all tool embeddings, and returns <code>process_refund</code>, <code>calculate_refund_amount</code>, <code>get_order_details</code>. We send only those to the LLM.</p><p>This cuts our tool payload by 75-90% on most requests. The model sees fewer, more relevant options and picks better.</p><h2>Choosing an Embedding Provider</h2><p>We debated two main approaches: calling OpenAI&#8217;s embedding API or running our own model.</p><p>OpenAI&#8217;s <code>text-embedding-3-small</code> is the path of least resistance. It&#8217;s a REST call, returns 1536-dimensional vectors, costs about a hundredth of a cent per embedding, and just works. The semantic understanding is excellent. The downside is the external dependency. Every query needs a network round-trip, your data touches their servers, and you&#8217;re subject to their rate limits and outages.</p><p>Running something like ModernBERT locally is appealing for different reasons. Zero marginal cost, sub-millisecond latency since there&#8217;s no network hop, and complete data privacy. But now you&#8217;re managing infrastructure. You need a server running the model, monitoring, scaling considerations, and you&#8217;re on the hook for model selection and updates. For a small team, that operational burden is real.</p><p>There&#8217;s also a hybrid approach: use OpenAI in production for reliability, run a local model in development and testing to avoid API costs and flakiness. We built our system with a provider abstraction to make this possible:</p><pre><code><strong>defmodule</strong> <strong>Amplify.EmbeddingProvider</strong> <strong>do</strong>
  @callback generate_embedding(<strong>String</strong>.t()) :: {:ok, list(float())} | {:error, any()}
  @callback dimensions() :: pos_integer()
  @callback model_id() :: <strong>String</strong>.t()
<strong>end</strong>
</code></pre><p>Switching providers is a config change. The abstraction cost an extra hour upfront but buys flexibility later.</p><h2>Why We Went With OpenAI</h2><p>For our volume, OpenAI was the obvious choice. We process hundreds of queries daily, not millions. At $0.00001 per embedding, we&#8217;re talking pennies per month. The reliability is excellent, the semantic quality is strong for our e-commerce domain, and there&#8217;s zero infrastructure to manage.</p><p>If we were processing millions of queries or had strict data residency requirements, the calculus would be different. But for a small team running a ticketing platform, paying a few cents to avoid running another service is a good trade.</p><h2>Generating Embeddings in Development</h2><p>Adding a new tool or updating an existing one means regenerating embeddings. In development, it&#8217;s a mix task:</p><pre><code>mix generate_tool_embeddings
</code></pre><p>This iterates through all tool definitions, calls OpenAI for each, and upserts the results into the <code>tool_embeddings</code> table. Takes about 10 seconds for 40 tools. The task is idempotent so you can run it whenever.</p><p>The implementation is straightforward. We convert each <code>ToolDefinition</code> to embedding text that captures the name, description, and parameter info, then store the vector alongside the tool name and model ID.</p><h2>Generating Embeddings in Production</h2><p>For production, we built a simple admin page. Navigate to the AI operations screen, see the current embedding count, click a button to regenerate. Non-technical team members can trigger it after tool updates without touching the console.</p><p>The alternative is shelling into the production console:</p><pre><code><strong>Amplify.Services.ToolSelector</strong>.regenerate_embeddings()
</code></pre><p>Either way, regeneration is safe to run anytime. It deletes existing embeddings and creates fresh ones. The whole process takes seconds.</p><p>One gotcha: if you ever switch embedding providers, you must regenerate everything. OpenAI&#8217;s 1536-dimension vectors are incompatible with a local model&#8217;s 768-dimension vectors. We store <code>model_id</code> with each embedding to catch mismatches and make debugging easier.</p><h2>Handling Multi-Step Operations</h2><p>Pure similarity search has a gap. If someone says &#8220;refund order #12345&#8221;, we&#8217;ll find <code>process_refund</code>. But the LLM also needs <code>get_order_details</code> to look up the order before it can refund anything. Those two tools aren&#8217;t semantically similar enough to both appear in the top results.</p><p>We solved this with category expansion. Each tool has a category like <code>:orders</code>, <code>:refunds</code>, or <code>:events</code>. When we select tools via similarity, we expand to include related categories:</p><pre><code>@category_expansions %{
  orders: [:orders, :refunds, :customers],
  refunds: [:orders, :refunds, :payments],
  events: [:events, :tickets]
}
</code></pre><p>So finding <code>process_refund</code> (category <code>:refunds</code>) automatically pulls in order lookup tools. The LLM gets everything it needs for multi-step workflows.</p><h2>The pgvector Query</h2><p>For those curious about the database side, here&#8217;s the actual query we run:</p><pre><code><strong>SELECT</strong> name, 1 - (embedding &lt;=&gt; $1) <strong>as</strong> similarity
<strong>FROM</strong> tool_embeddings
<strong>WHERE</strong> (embedding &lt;=&gt; $1) &lt;= $3
<strong>ORDER</strong> <strong>BY</strong> embedding &lt;=&gt; $1
<strong>LIMIT</strong> $2
</code></pre><p>The <code>&lt;=&gt;</code> operator is pgvector&#8217;s cosine distance. We filter by a similarity threshold (0.4 by default) to avoid returning completely irrelevant tools, then take the top K results. The whole thing runs in under 10ms.</p><h2>Testing Without Hitting OpenAI</h2><p>We use Mimic for mocking in tests. Every test that touches tool selection stubs the embedding provider to return consistent vectors:</p><pre><code><strong>Mimic</strong>.stub(<strong>EmbeddingProvider</strong>, :generate, <strong>fn</strong> _text -&gt;
  {:ok, <strong>List</strong>.duplicate(0.1, 1536)}
<strong>end</strong>)
</code></pre><p>This keeps tests fast, deterministic, and free of API dependencies. We can simulate failures too, testing that the system gracefully falls back to using all tools when embedding generation fails.</p><h2>What We Learned</h2><p>A few things surprised us along the way.</p><p>The similarity threshold matters more than we expected. Too high and you filter out useful tools. Too low and you&#8217;re back to noise. We settled on 0.4 after some experimentation but it&#8217;s worth tuning for your domain.</p><p>Category expansion was an afterthought that became essential. Pure semantic similarity misses the dependencies between tools. If your assistant does multi-step operations, you need something like this.</p><p>The provider abstraction was worth it even though we haven&#8217;t switched providers. It forced us to think cleanly about the interface and made testing much easier. The Mimic stubs work because there&#8217;s a clear boundary to mock.</p><p>Cold start is a real concern. If your embeddings table is empty, you need a fallback. We log a warning and use all tools, which isn&#8217;t ideal but prevents complete failure.</p><h2>Results</h2><p>After rolling this out, our per-request token usage for tool definitions dropped 60-80%. Latency improved by about 200ms since the model processes fewer tokens. Tool selection accuracy actually got slightly better because there&#8217;s less noise confusing the model.</p><p>The embedding costs are negligible. We&#8217;re at maybe $0.01 per day for our volume. The whole system adds a 10ms database query per request, which disappears in the noise of the LLM call.</p><p>For anyone dealing with tool explosion in their AI agents, this approach is worth considering. The implementation isn&#8217;t complex, the costs are minimal, and the benefits compound as your tool count grows.</p>]]></content:encoded></item><item><title><![CDATA[React2Shell serves as good reminder why JavaScript is no fun]]></title><description><![CDATA[There goes the weekend]]></description><link>https://bitbytebit.substack.com/p/react2shell-serves-as-good-reminder</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/react2shell-serves-as-good-reminder</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Sun, 07 Dec 2025 20:49:57 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!c9JT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb05a28a6-1fbb-409a-ba1a-85f0ed3e74f0_1200x601.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Got this email from Vercel after the JavaScript ecosystem <a href="https://aws.amazon.com/blogs/security/china-nexus-cyber-threat-groups-rapidly-exploit-react2shell-vulnerability-cve-2025-55182/">had another moment</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!c9JT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb05a28a6-1fbb-409a-ba1a-85f0ed3e74f0_1200x601.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!c9JT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb05a28a6-1fbb-409a-ba1a-85f0ed3e74f0_1200x601.webp 424w, https://substackcdn.com/image/fetch/$s_!c9JT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb05a28a6-1fbb-409a-ba1a-85f0ed3e74f0_1200x601.webp 848w, https://substackcdn.com/image/fetch/$s_!c9JT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb05a28a6-1fbb-409a-ba1a-85f0ed3e74f0_1200x601.webp 1272w, https://substackcdn.com/image/fetch/$s_!c9JT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb05a28a6-1fbb-409a-ba1a-85f0ed3e74f0_1200x601.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!c9JT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb05a28a6-1fbb-409a-ba1a-85f0ed3e74f0_1200x601.webp" width="1200" height="601" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b05a28a6-1fbb-409a-ba1a-85f0ed3e74f0_1200x601.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:601,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;nextjs-react-shell&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="nextjs-react-shell" title="nextjs-react-shell" srcset="https://substackcdn.com/image/fetch/$s_!c9JT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb05a28a6-1fbb-409a-ba1a-85f0ed3e74f0_1200x601.webp 424w, https://substackcdn.com/image/fetch/$s_!c9JT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb05a28a6-1fbb-409a-ba1a-85f0ed3e74f0_1200x601.webp 848w, https://substackcdn.com/image/fetch/$s_!c9JT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb05a28a6-1fbb-409a-ba1a-85f0ed3e74f0_1200x601.webp 1272w, https://substackcdn.com/image/fetch/$s_!c9JT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb05a28a6-1fbb-409a-ba1a-85f0ed3e74f0_1200x601.webp 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I understand the urgency of the matter, but blocking deployments is a step too far as I don&#8217;t even have RSC enabled, and need to make changes to the app. This isn&#8217;t a simple upgrade as I was on Next 14 and React 18. The peer-dependency situation is hell as there are many libraries that have hardcoded dependencies to React 18. In some cases you can use <code>legacy-peer-deps=true</code> to get around things, but in others you have to update critical libraries that you had no real need to do, e.g., PixiJS 7 -&gt; 8, and those have breaking changes. Not to mention the code mods that need to be applied from Next 14 to 15 only leave you with a sense of dread. But a weekend later, all is done.</p><p>What this experience reminded me of is why I got out of the JavaScript world and switched to Elixir/Phoenix (about 80% migration done). Half the time I was chasing library upgrades and framework changes, rather than focusing on actual valuable work. Since I&#8217;ve shifted away from it, it&#8217;s not an exaggeration that boilerplate plus maintenance of libraries has gone from taking about 30-50% of time down to about 5%.</p><p>All the JavaScript frameworks ultimately either:</p><ol><li><p>Aim to manage state so that reactive experiences can be easier to develop.</p></li><li><p>Want to provide some performance optimization (e.g., SSR)</p></li></ol><p>Compared to how Phoenix/LiveView does it, they&#8217;ve all failed. Every. Single. One. There is ton of boilerplate and your app is always worried about the next breaking change some library which doesn&#8217;t care about backward compatibility is about to publish, leaving you behind.</p><p>You can&#8217;t avoid JavaScript but I&#8217;ve found exiting the React/JavaSCript ecosystem has made for a healthier programming lifestyle.</p>]]></content:encoded></item><item><title><![CDATA[Good riddance to Auth0 and social logins]]></title><description><![CDATA[Phoenix makes life simple]]></description><link>https://bitbytebit.substack.com/p/good-riddance-to-social-logins-and</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/good-riddance-to-social-logins-and</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Fri, 21 Nov 2025 16:36:31 GMT</pubDate><content:encoded><![CDATA[<p>Three months ago I got rid of social logins, fired Auth0 and made the decision to use Magic Links (easy in <a href="https://hexdocs.pm/phoenix/mix_phx_gen_auth.html">Phoenix</a> - hello <code>mix phx.gen.auth</code>).</p><p>I had made the decision to go with Auth0 after I had implemented by own social logins to FB and Google, and wanted to support GitHub, in addition to regular username/passwords. I had grand ideas on how I&#8217;d use Auth0 to manage permissions and implement their &#8220;Actions&#8221; by implementing manage middleware, create user journeys which route people this way and that way, but most of all it was about security. I didn&#8217;t want to get into the business of managing passwords and and tokens, and wanted to focus on value-added feature, you know, things people pay for.</p><p>But here I am a year later reversing it all, and these are some reasons why. These are in no particular order.</p><ol><li><p>85% of customers just use regular email/passwords so the additional options on login just confuse them. I wanted to make things easier for people and had thought I would reduce signup friction if I added social logins. This was an incorrect assumption.</p></li><li><p>People mistakenly login use the email from their regular email/password combo to login to social logins which they also have an account, and it&#8217;s chaos to manage that, especially from a customer support standpoint.</p></li><li><p>I didn&#8217;t have to manage keys from Meta, Google and the like, e.g., policy updates, token expirations, other random &#8220;Required Actions&#8221; being thrown at me in the Meta Developer Portal where each page takes 20 seconds to load.</p></li><li><p>I had overestimated how much work implementing your own security system would be, but Phoenix 1.8 made it dead simple to implement Magic Links and with the help of Claude, the whole thing took a weekend to go from zero to in-production.</p></li><li><p>I liked the idea of outsourcing authentication to a user&#8217;s email provider (usually Gmail) and let it be the weakest link in the chain. They think about this stuff all the time so I don&#8217;t need to. They basically implement MFA for me.</p></li><li><p>I didn&#8217;t want to incur a needless cost. Though I&#8217;m on Auth0&#8217;s &#8220;Startup Program&#8221;, they can jack up fees any time and I didn&#8217;t like that unpredictability. I&#8217;m startup poor.</p></li><li><p>Managing permissions in a separate system was too complex. I&#8217;m using RBAC which isn&#8217;t hard to implement, and crossing network and system boundaries just to see &#8220;does this user have access?&#8221; seemed overkill. I did shove all the permissions in the JWT via an action, but any update to those permissions required pulling that information again, and my users update permissions all the time. Too much work to sync state and it felt needlessly complex.</p></li><li><p>Implementing resource-based authorization turned out to be much simpler using Elixir&#8217;s <a href="https://github.com/woylie/let_me">LetMe library</a>. Just give me a database I can query over an API I have to call - so much simpler. The UI to manage was also snappier to implement in LiveView as we&#8217;re avoiding REST calls.</p></li><li><p>There was very little control when using Auth0&#8217;s Universal Login on how you&#8217;d like to customize the screen. I realize login screens have to be simple, but just putting some other links was hard. Just having the ability to customize branding isn&#8217;t enough.</p></li><li><p>Lot of users were confused by the redirect to a &#8220;different&#8221; site even though I had a custom domain going. The older users felt like they had done something wrong when the header of the site changed.</p></li><li><p>I implemented account spoofing but it was a pain as I couldn&#8217;t just decrypt the token from Auth0 as they don&#8217;t provide the secret to do so (rightfully so, I suppose). I had a really funky workaround for operations people to fix customer issues via spoofing. My implementation could&#8217;ve been better, but it&#8217;s still unnecessary code complexity.</p></li><li><p>I hate Meta and Google and want to steer my users away from them, not towards them.</p></li><li><p>I felt I could secure customer data by using proper system admin, storage and encryption practices, and didn&#8217;t feel I was getting any additional security benefit by outsourcing this info to an external provider. I had overestimated how complex this was to manage (knock on wood). I felt having a good cloud and storage provider was more important than having a good identity management provider.</p></li></ol><p>All in all, learned a lot through this whole process and Auth0 did help me get off the ground quickly back when Phoenix 1.8 wasn&#8217;t released. Maybe this is just another case of Elixir/Phoenix making development a joy more than an indictment on hiring identity providers.</p>]]></content:encoded></item><item><title><![CDATA[Pass on polymorphic tables]]></title><description><![CDATA[They seem simpler but it's best to avoid them.]]></description><link>https://bitbytebit.substack.com/p/the-temptation-of-polymorphic-tables</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/the-temptation-of-polymorphic-tables</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Fri, 31 Oct 2025 19:04:37 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_ZGx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A decision I often have to make when designing databases is whether to create a polymorphic table or not. Let me give an example. I have:</p><ul><li><p>A discount table with an id which stores discount codes</p></li><li><p>A mailing_list table where signing up gives you a discount</p></li><li><p>A contest table where participating gives you a discount</p></li></ul><p>You could design this as:</p><pre><code>discount(id, code)
mailing_list_discounts(mailing_list_id, discount_id)
contest_discounts(contest_id, discount_id)
</code></pre><p>Or you could go:</p><pre><code>discount(id, code)
auto_discounts(mailing_list_id, contest_id, discount_id)
</code></pre><p>One of <code>mailing_list_id</code> or <code>contest_id</code> would always be null. The query complexity for either will be very similar given proper indexing, and I find it generally easier to look for things in fewer tables than more, so <code>auto_discounts</code> is very tempting.</p><p>The challenge comes in when you have to store information related to specific types of discounts, say <code>contest_winning_attempt</code>, in which case we now have to have <code>null</code> values in cases where it doesn&#8217;t apply:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_ZGx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_ZGx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png 424w, https://substackcdn.com/image/fetch/$s_!_ZGx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png 848w, https://substackcdn.com/image/fetch/$s_!_ZGx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png 1272w, https://substackcdn.com/image/fetch/$s_!_ZGx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_ZGx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png" width="1418" height="382" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:382,&quot;width&quot;:1418,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:55661,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://bitbytebit.substack.com/i/177682099?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_ZGx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png 424w, https://substackcdn.com/image/fetch/$s_!_ZGx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png 848w, https://substackcdn.com/image/fetch/$s_!_ZGx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png 1272w, https://substackcdn.com/image/fetch/$s_!_ZGx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe787c15e-f33d-4c94-b537-70deec7eb70b_1418x382.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>Another issue is that when when querying against a single table you need to add a check for null. For example, if you want to find out which discount codes were awarded for people who didn&#8217;t win on their first try, you&#8217;d have to do:</p><pre><code>SELECT * FROM auto_discounts
WHERE contest_id IS NOT NULL AND contest_winning_attempt != 1;
</code></pre><p>You need the <code>contest_id IS NOT NULL</code> check or it would pick up records that have nothing to do with contests. You could add a discriminator like <code>type</code> which is either <code>contest</code> or <code>mailing_list</code> and that makes things even more verbose and error prone.</p><p>So as much I sometimes get critiqued for having too many tables in my app, I generally opt for separate tables to keep concerns separate (though are are somewhat related). There&#8217;s also the benefit of optimizing indexes on a per-table basis, lesser chance of misuse since columns that don&#8217;t concern you just aren&#8217;t there to misuse, and you keep scaling flexibility on the table (e.g., future sharding). </p>]]></content:encoded></item><item><title><![CDATA[Stop Worrying and Love the Bomb]]></title><description><![CDATA[AI is here to give you a back rub and a hot massage]]></description><link>https://bitbytebit.substack.com/p/stop-worrying-and-love-the-bomb</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/stop-worrying-and-love-the-bomb</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Sat, 11 Oct 2025 14:29:33 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!v2NU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!v2NU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!v2NU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg 424w, https://substackcdn.com/image/fetch/$s_!v2NU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg 848w, https://substackcdn.com/image/fetch/$s_!v2NU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!v2NU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!v2NU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg" width="1456" height="764" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:764,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:295710,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://bitbytebit.substack.com/i/175882571?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!v2NU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg 424w, https://substackcdn.com/image/fetch/$s_!v2NU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg 848w, https://substackcdn.com/image/fetch/$s_!v2NU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!v2NU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F88a37b1e-22e9-4a22-8fc9-630e81158a17_1658x870.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A variant of <a href="https://news.ycombinator.com/item?id=45512098">this comment</a> on HN is something I&#8217;ve heard too often:</p><blockquote><p><em>I used to have this hard-to-get, in-demand skill that paid lots of money and felt like even though programming languages, libraries and web frameworks were always evolving I could always keep up because I&#8217;m smart...I find it way less fun to be waiting around for agents to do stuff and it&#8217;s way harder to get into flow state managing multiple of these things. It makes me want to move into something completely different like sales</em></p></blockquote><p>Yeah, it sucks that a skill we all had has been commoditized. You always heard stories about factory workers getting their jobs outsourced or automated, but we (incorrectly) thought that this would happen to white-collar jobs like software engineers. In hindsight, this was quite naive but now that we&#8217;re here, we have no choice but to deal with it and just embrace it like a helpless man on a beach staring at an incoming tsunami. Ride the wave or drown.</p><p>And yes, we all have job loss anxiety, especially those of us who don&#8217;t have FT jobs but contract their way around, but I want to acknowledge just how relieved of actual work stress I have been because of AI. Simply put, no upcoming feature request scares me because I know I have help. Have to look at a legacy codebase and make a change? No problem, I can understand the codebase 50x faster than I could have before. A customer requested a major change that requires a bunch of refactoring? Child&#8217;s play. I want to parse through a bunch of logs to find a needle in a haystack? Done. Need to create a plan for a double-entry accounting system and I&#8217;m not even familiar with accounting concepts? Bring it on. Code has poor documentation and you&#8217;re procrastinating? Not any more. You get the idea.</p><p>All these things used to stress me because the research, investigation or learning curve for these things was steep and time-consuming. Some of the curves may still be steep, but it certainly isn&#8217;t time consuming anymore, and that in itself has improved my quality of life. The amount of time AI has freed up for me to simply read a book, or watch a TV show, or spend more time with family is significant. My weekends used to be swamped with jumpcomedy.com work and I actually went for a bike ride because I know something that would take me 20 hours will take me 4, and I actually have the option to take up leisure time. This is wild to me.</p><p>Do I miss writing code from scratch? I don&#8217;t know the answer to that question. I do know that I don&#8217;t miss getting stuck even though getting stuck is how I learned many things. I do know that I like seeing my ideas come to life faster - in minutes and hours instead of weeks and months. This means I get to experiment more, so maybe I&#8217;ve replaced &#8220;getting stuck learning&#8221; with &#8220;experimental learning&#8221;. The number of iterations has increased and so did their speed so I&#8217;m going through the inspect-and-adapt loop many more times than before. It&#8217;s the Lean Startup cycle on overdrive.</p><p>But again, do I miss writing code from scratch? Gun to my head. I&#8217;d say...no. That sounds like a betrayal of my art and profession, but code was always a means to an end, and I seem to be getting to the end a lot faster, so what am I feeling sad about? Is it nostalgia? Is it letting go of something you invested in for so long? Is it that writing code was part of your identity? Is it that learning how to code feels like a sunk cost? Are my degrees all for naught? No, yes, a little bit, maybe.</p><p>What matters now is that we have arrived in an entirely new world and I have some skills that meet the moment. It turns out that the core principles of software engineering have little to do with syntax and language, and the skills that I now use most aren&#8217;t too different than pre-AI, but seem to be more valuable because AI seems to work against some of these, some examples:</p><ul><li><p>Problem decomposition, i.e., breaking a larger problem into manageable parts and then focusing on each one intentionally without bloating your problem/context space</p></li><li><p>A dedication to the YAGNI/KISS principles because it&#8217;s so easy to generate code with AI and implement things you might not need</p></li><li><p>Finding the right application architecture and abstractions because you know your customer&#8217;s needs better than AI and are better at anticipating change, because you talk to customers</p></li><li><p>Greater focus on the business problem rather than the &#8220;how&#8221; which is what code is. I find myself knowing more about the problem domain than before.</p></li></ul><p>These aren&#8217;t necessarily hard engineering skills but they are engineering skills which I&#8217;ve elevated to be my &#8220;primary&#8221; skills, replacing writing code. I feel some nostalgia, but I also see results faster which more than makes up for it. Is this going to eventually result in my unemployment and loss of income? Maybe. But the future is worry and the past is regrets, so may s well just live in the moment.</p>]]></content:encoded></item><item><title><![CDATA[Everything that's wrong with Google Search in one image]]></title><description><![CDATA[Make Search Great Again (or just kill it)]]></description><link>https://bitbytebit.substack.com/p/everything-thats-wrong-with-google</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/everything-thats-wrong-with-google</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Wed, 24 Sep 2025 22:11:17 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!nDXu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda55367e-de31-4413-9924-755a634e62de_2268x2164.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I typed in Midjourney to search for Midjourney because I wanted to use Midjourney. Here&#8217;s what I got instead. It&#8217;s the fifth result down on the page. So if you want to rank high on Google, not only do you need to build a great product to have enough backlinks but also pay Google so other lesser products can&#8217;t pay themselves to be ahead of you.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nDXu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda55367e-de31-4413-9924-755a634e62de_2268x2164.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nDXu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda55367e-de31-4413-9924-755a634e62de_2268x2164.png 424w, https://substackcdn.com/image/fetch/$s_!nDXu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda55367e-de31-4413-9924-755a634e62de_2268x2164.png 848w, https://substackcdn.com/image/fetch/$s_!nDXu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda55367e-de31-4413-9924-755a634e62de_2268x2164.png 1272w, https://substackcdn.com/image/fetch/$s_!nDXu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda55367e-de31-4413-9924-755a634e62de_2268x2164.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nDXu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda55367e-de31-4413-9924-755a634e62de_2268x2164.png" width="1456" height="1389" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/da55367e-de31-4413-9924-755a634e62de_2268x2164.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1389,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:505590,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://bitbytebit.substack.com/i/174487535?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda55367e-de31-4413-9924-755a634e62de_2268x2164.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nDXu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda55367e-de31-4413-9924-755a634e62de_2268x2164.png 424w, https://substackcdn.com/image/fetch/$s_!nDXu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda55367e-de31-4413-9924-755a634e62de_2268x2164.png 848w, https://substackcdn.com/image/fetch/$s_!nDXu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda55367e-de31-4413-9924-755a634e62de_2268x2164.png 1272w, https://substackcdn.com/image/fetch/$s_!nDXu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda55367e-de31-4413-9924-755a634e62de_2268x2164.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>SAD! Thank you for your attention to this matter.</p>]]></content:encoded></item><item><title><![CDATA[I'm rejuvenated by the Elixir EU Conference]]></title><description><![CDATA[I think my long-term goal is to be a full-time Elixir programmer.]]></description><link>https://bitbytebit.substack.com/p/im-rejuvenated-by-the-elixir-eu-conference</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/im-rejuvenated-by-the-elixir-eu-conference</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Fri, 16 May 2025 19:57:53 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/4f58f10d-0f35-40e5-86fc-70d369224487_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Finished my first ever Elixir conference and it feels great for three reasons:</p><ol><li><p>Learned a lot</p></li><li><p>Realized that I don't know a lot</p></li><li><p>Felt grateful to be part of a community where I can possibly fill the gap</p></li></ol><p>More than ever I've felt I've made the right decision opting for the Elixir/Erlang stack for jumpcomedy.com. It was comforting to hear from people who were facing similar decisions 7-8 years ago and took the cold plunge. They were risk takers, and I'm merely a follower who has the benefit of a vast amount of work done by immensely talented people. Here are five highlights of many from the conference.</p><h3><strong>Electric SQL / Phoenix Sync</strong></h3><p><a href="https://electric-sql.com/">This</a> is a game-changer. In a world where real-time views are the norm and attention spans are decreasing fast, having data sync'd between the UI and the data store is ever more important for a great UX. Phoenix Sync does this by looking at the PostGres replication logs and syncing your LiveViews (or even JS front-ends) in near instant time. Combine this with the power of LiveView streams and you literally have to change five characters, and an INSERT done from a PostGres GUI will show up in your LiveView UI. I kid you not - it seems too good to be true to not have to write any code to make this happen, but that's how it works.</p><h3><strong>Ash AI</strong></h3><p>There was a lot of AI talk and some amazing demos using <a href="https://github.com/brainlid/langchain">LangChain</a> but what excited me most was <a href="https://github.com/ash-project/ash_ai">Ash Framework's AI extension</a>, simply because I think they've nailed the declarative syntax and even made Model Context Protocol implementations, dare I say, rather trivial. Elixir/Phoenix has always given, and I don't say this lightly, a 5x productivity boost compared to anything else and this implementation just demonstrated how easy they've made it to power up your apps with AI.</p><h3><strong>LiveVue / JS Escape Hatches</strong></h3><p>Any good Elixir abstraction library provides a clean escape hatch, and <a href="https://github.com/Valian/live_vue/blob/v0.5.7/README.md#L1">LiveVue</a> (and LiveReact and LiveSvelte) do just that - instead of sending HTML diffs based on state changes across the wire, they sync props, which means that you can use any front-end framework with Phoenix backends seamlessly for cases where UIs need to have heavy JavaScript for whatever reason. This is a fear of many who contemplate LiveView which is extremely server-centric, i.e., not having control over front-end state can keep a lot of developers away, especially those who rely on framework utilities in their app (<code>npm i</code> is just too easy). This addresses that problem.</p><h3><strong>Type Checking vs Type Inference</strong></h3><p>Jose Valim spoke about what's coming up in Elixir 1.19, which will have support for even more inferred types, notably inferring the types of keys in Maps. He suggested that in 18-24 months Elixir will have type declarations and that it's a matter of when, not if. This would close a huge perceived gap in the language and pave the way for developers who are used to TypeScript and Java to give Elixir a shot. Not having types never scared me because it forced me to not depend on types, which promoted a behaviour of writing more obvious code, smaller functions and better tests. There are already a lot of big companies using Elixir, like BBC, Bleacher Report, etc. but typing would open up the door even more to larger enterprises who often see it as a requirement simply because that's what they're used to.</p><h3><strong>Waffle</strong></h3><p>I have to give a shout out to the many lightning talks, and one of them was <a href="https://github.com/elixir-waffle/waffle">Waffle</a>, which is a classic example of small Elixir library that does one job, does it well, and doesn't try to do anything else. Waffle is a file processor/uploader with extensions to S3, Azure etc. I like how the author said that "we're pretty much done" with this library and that's what I love about Elixir libraries: a "last commit" of months or even years ago doesn't imply that the library is abandoned, it's that it just did what it was supposed to do really well and there's nothing more to do.</p><p>--</p><p>Chris McCord, the creator of Phoenix gave the closing keynote titled <em>Code Generators are Dead. Long Live Code Generators</em>, and man, he looked a little jaded as he was introducing how AIs he built are replacing the traditional Phoenix code generators. He literally built a customized TodoApp in 20 minutes and pondered the role of developers in the age of AI while doing it, and also while low-key dissing "vibe coding". His main idea seemed to be that AI isn't going to replace developers but understands why that might be a fear. That people who cut-and-pasted StackOverflow will continue to cut-and-paste AI-generated code and that once things normalize, the human will still end up being needed.</p><p>I don't know what to think of this but AI hits different than the Stack Overflow metaphor simply because Stack Overflow still required you to connect the dots. Lot of AIs don't. A metaphor that resonates with me is one comparing AI with the industrial revolution, which revolutionized everything but demand even more labour than before. Time will tell.</p>]]></content:encoded></item><item><title><![CDATA[DORA: AI boosting productivity, hindering delivery]]></title><description><![CDATA[The 2024 State of DevOps report was released this week and it finds that AI increases developer productivity while decreasing software delivery metrics. This is a short-term phenomenon.]]></description><link>https://bitbytebit.substack.com/p/dora-ai-boosting-productivity-hindering</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/dora-ai-boosting-productivity-hindering</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Fri, 22 Nov 2024 00:47:14 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5b99e9a6-a598-4fa5-99e8-1b81c9ad9bb2_1484x1156.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The <a href="https://services.google.com/fh/files/misc/2024_final_dora_report.pdf">new DORA report</a> got released and has 30 pages dedicated to AI adoption. One particular finding stuck out to me: an increase in AI adoption was correlated with an increase in code quality, quicker code reviews, decrease in code complexity, decrease in technical debt and improved documentation. However, it was also correlated with a decrease in the key DORA metrics of lead time, throughput and change failure rate, while having no positive impact on product performance.</p><p>This is counter intuitive because you would imagine improving your software development process would lead to better software delivery performance. After all, if you're writing better code, having that code reviewed faster, you'd think that it would have some notable downstream impacts. Turns out it's not and the authors hypothesize that:</p><blockquote><p><em>...the fundamental paradigm shift that AI has produced in terms of respondent productivity and code generation speed may have caused the field to forget one of DORA&#8217;s most basic principles&#8212;the importance of small batch sizes. That is, since AI allows respondents to produce a much greater amount of code in the same amount of time, it is possible, even likely, that changelists are growing in size. DORA has consistently shown that larger changes are slower and more prone to creating instability.</em></p></blockquote><p>Basically, a rush to the red light effect where the deployment process of organizations isn't keeping up with the increase in developer productivity, leading to larger changes which lead to riskier deploys. This is fascinating to me and confirms one of my long-held beliefs (biases?) that keeping change size small is central to continuous delivery since, as all things being equal:</p><ol><li><p>Smaller the change, the lower the risk</p></li><li><p>Smaller the change, the more changes you need to make, the better you become at making changes</p></li><li><p>Larger the change, the more coordination required, making the change more expensive, thus reducing the incentive to make the change</p></li></ol><p>The positive impact of smaller batch sizes in increasing quality and performance is best highlighted in <a href="https://www.goodreads.com/book/show/6278270-the-principles-of-product-development-flow">Reinertsen's work</a> which is summed up neatly in this <a href="https://hbr.org/2012/05/six-myths-of-product-development">HBR article from 2012</a> and the DORA authors have been hammering that point home for years.</p><p>I don't know if they have enough data to confirm this hypotheses but it is a reasonable one. I'd like to take it a step further and hypothesize that this is only a short-term trend. It is relatively easy for developers to adopt AI tools since it can be as simple as opening up a new browser window. However, for pipelines and deployment processes it will be slower since it requires deeper changes to platform-level toolsets which span beyond individuals to teams and departments. We may be seeing a lag between the "left" and "right" side of the delivery process and I think this is a short-run problem which will go away sooner than expected because of the urgency around AI.</p><p>For example, though the report doesn't explicitly link the two, earlier in the document they do point out that organizations are:</p><blockquote><p><em>...willing to forgo the typical huge bureaucracy involved in adopting new technology because they felt an urgency to adopt AI, questioning "what if our competitor takes those actions before us"</em></p></blockquote><p>Sidebar: This reminds me of the JDK Version Index which is conceptually similar to the <a href="https://en.wikipedia.org/wiki/Big_Mac_Index">Big Mac Index</a> and a quick indicator of how far behind an organization is with industry. Big companies are usually around 4-5 (sometimes more) behind what the widely accepted JDK version is. I remember when JDK 1.5 came out with generics, lot of gigs I had insisted on sticking around with 1.4 for years because there just wasn't enough impetus to change something seen as so core. Similar stories can be heard about Java 8 and Java 17.</p><p>AI is different and the adoption speed is going to be faster since there is a perceived foregoing of tangible benefits if you don't. It's amazing how lumbering organizations who always cite their size and "oh we're so complex" as reasons not to adapt quickly, when faced with a crisis (or perceived crisis), are able to suddenly slice through the red tape to get stuff done (<a href="https://www.goodreads.com/book/show/1237300.The_Shock_Doctrine">Shock Doctrine, anyone?</a>).</p><p>Covid responses are a great example of this. Unfortunately, in my experience whatever process waste that was slashed during Covid has crept back in. Though I doubt anything will drive the impetus to change as Covid did, AI looks to have similar impact so DORA's findings of AI's positive impact on developer not translating to software delivery performance may just be a short-term phenomenon.</p>]]></content:encoded></item><item><title><![CDATA[Good software development habits]]></title><description><![CDATA[You're always learning better ways of doing things. This post is a list of lessons learned and reminders to myself.]]></description><link>https://bitbytebit.substack.com/p/good-software-development-habits</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/good-software-development-habits</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Thu, 05 Sep 2024 13:21:02 GMT</pubDate><content:encoded><![CDATA[<p>This post is not advice, it's what's working for me.</p><p>It's easy to pick up bad habits and hard to create good ones. Writing down what's working for me helps me maintain any good habits I've worked hard to develop. Here's an unordered list of 10 things that have helped me increase speed and maintain a respectable level of quality in the product I'm currently developing.</p><ol><li><p>Keep commits small enough that you wonder if you're taking this "keep commits small" thing a little too far. You just never know when you have to revert a particular change and there's a sense of bliss knowing where you introduced a bug six days ago and only reverting that commit without going through the savagery of merge conflicts. My rule of thumb: compiling software should be commitable.</p></li><li><p>Live Kent Beck's <a href="https://x.com/KentBeck/status/250733358307500032?lang=en">holy words of wisdom</a>: "for each desired change, make the change easy (warning: this may be hard), then make the easy change". Aim for at least half of all commits to be refactorings. Continuous refactoring is thinking of changes I can make in under 10 minutes that improve something. Doing this pays off whenever a bigger requirement comes in and you find yourself making a small change to satisfy it only because of those smaller improvements. Big refactorings are a bad idea.</p></li><li><p>All code is a liability. Undeployed code is the grim reaper of liabilities. I need to know if it works or at least doesn't break anything. Tests give you confidence, production gives you approval. The hosting costs might rack up a little with so many deploys but it's a small price to pay for knowing the last thing you did was a true sign of progression. <em>Working software is the primary measure of progress</em>, says one of the <a href="https://agilemanifesto.org/principles.html">agile principles</a>. Working and progress are doing a lot of heavy lifting in that sentence, so I've defined them for myself. Working is something being working enough to be deployed, and if it's code that's contributing to a capability, that's progress.</p></li><li><p>Know when you're testing the framework's capability. If you are, don't do it. The framework is already tested by people who know a lot more than you, and you have to trust them that the <code>useState()</code> hook does what it's supposed to do. If you keep components small, then you reduce the need for a lot of tests as the framework will be doing most of the heavy lifting in the component. If the component is big, then you introduce more complexity and now you need to write a lot of tests.</p></li><li><p>If a particular function doesn't fit anywhere, create a new module (or class or component) for it and you'll find a home for it later. It's better to create a new independent construct than to jam it into an existing module where you know deep down it doesn't make sense. Worst comes to worst, it lives as an independent module which isn't too bad anyway.</p></li><li><p>If you don't know what an API should look like, write the tests first as it'll force you to think of the "customer" which in this case is you. You'll invariably discover cases that you would not have thought of if you had just written the code first and tests after. You don't have to be religious about TDD and it's OK to work in larger batches (e.g., write more than just a couple lines of code before making it pass). The amount of code to write in a red/failing state doesn't always have to be small. You know what you're doing, don't let dogma get in the way of productivity.</p></li><li><p>Copy-paste is OK once. The second time you're introducing duplication (i.e., three copies), don't. You should have enough data points to create a good enough abstraction. The risk of diverging implementations of the same thing is too high at this point, and consolidation is needed. It's better to have some wonky parameterization than it is to have multiple implementations of nearly the same thing. Improving the parameters will be easier than to consolidate four different implementations if this situation comes up again.</p></li><li><p>Designs get stale. You can slow the rate at which they get stale by refactoring, but ultimately you'll need to change how things work. Don't feel too bad about moving away from something that was dear to you a while ago and something you felt proud about at the time. You did the right thing then and shouldn't beat yourself up for not getting it right enough that you wouldn't need to change anything. Most of the time writing software is changing software. Just accept it and move on. There's no such thing as the perfect design, and change is at the core of software development. How good you are at changing things is how good you are at software development.</p></li><li><p>Technical debt can be classified into three main types: 1) things that are preventing you from doing stuff now, 2) things that will prevent you from doing stuff later, and 3) things that <em>might</em> prevent you from doing stuff later. Every other classification is a subset of these three. Minimize having lots of stuff in #1 and try to focus on #2. Ignore #3.</p></li><li><p>Testability is correlated with good design. Something not being easily testable hints that the design needs to be changed. Sometimes that design is your test design. As an example, if you find yourself finding it difficult to mock <code>em.getRepository(User).findOneOrFail({id})</code>, then chances are you either need to put that call into its own function that can be mocked, or write a test utility which allows for easier mocking of the entity manager methods. Tests go unwritten when it's hard to test, not because you don't want to test.</p></li></ol><p>There's probably a lot more, but 10 is a nice number.</p>]]></content:encoded></item><item><title><![CDATA[Symptoms of a System in Stasis]]></title><description><![CDATA[Signs that agile is a name-only thing.]]></description><link>https://bitbytebit.substack.com/p/symptoms-of-a-system-in-stasis</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/symptoms-of-a-system-in-stasis</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Mon, 26 Aug 2024 13:11:34 GMT</pubDate><content:encoded><![CDATA[<p>Here's an unordered list of symptoms that might indicate more profound&nbsp;issues with your organizational culture - one that's preventing you from delivering on your true potential.</p><ol><li><p>You're "doing agile" by using some sort of iterative development pattern, and have gained enough efficiencies compared to any other way of working that you've ceased trying to systematically improve the development process. There are important milestones to hit and probing the development process doesn't appear like a way to hit those since all the juice out of the lemon is perceived to have been squeezed.</p></li><li><p>Retrospectives happen as a formality more than an inquiry of how the team is working within and across the organization. Only limited options of change are on the table, most are off limits. Improvement ideas are not stifled or suppressed, but everyone knows the <a href="https://en.wikipedia.org/wiki/Overton_window">Overton Window</a> of what's up for debate.</p></li><li><p>Standardization is seen to increase efficiency and considered as a productivity gaining approach. Deviation from the common process is generally viewed as a defect to be corrected rather than a possibly novel paradigm-shift .</p></li><li><p>Process metrics dominate value metrics. Greater focus is given to team velocity metrics than to customer outcome metrics. The latter is seen to drive the former, with no evidence supporting this perceived causality.</p></li><li><p>The team is subdivided into skill-sets based on technologies, creating hand-offs within the team leading to longer queues and wait times. This forces managers to to allocate work based more on individual skill availability than overall team capacity.</p></li><li><p>Work is started more often than it is finished, leading to a pile of zombie initiatives where managers grapple with the sunk cost fallacy as they try to extract positive interpretations out of negative outcomes.</p></li><li><p>The cost of organizational change/restructuring is seen as too high, but at the same time there's acknowledgement that the current structure is not conducive to delivering the value everyone agrees needs to be delivered. Nobody wants to tackle this math as it's politically explosive.</p></li><li><p>Competitor offerings are seen as a proxy for what customers desire more than a direct line of communication with the customer. The team's communication with customers is mediated by multiple layers of organization constructs, making it difficult to&nbsp;discern their actual needs.</p></li><li><p>Work fills the time allocated to it (<a href="https://en.wikipedia.org/wiki/Parkinson%27s_law">Parkinson's Law</a>) with most projects being relatively on-time, thus providing little incentive to interrogate the system in which they are delivered.</p></li><li><p>Most ideas for the next project come from above, not from the software development team, which is seen as a CPU to execute work, more than a source of ideas on what to do next. Work is pushed to the team rather than pulled by the team.</p></li></ol>]]></content:encoded></item><item><title><![CDATA[The anatomy of a 2AM mental breakdown]]></title><description><![CDATA[When things go wrong and you have nowhere to turn]]></description><link>https://bitbytebit.substack.com/p/the-anatomy-of-a-2am-mental-breakdown</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/the-anatomy-of-a-2am-mental-breakdown</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Tue, 20 Aug 2024 15:02:48 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/414b2ad6-7cf3-456f-b736-de86cfe12767_2454x1614.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Around 2AM this morning I had a realization that this was the most stressed I have ever been. On verge of a complete breakdown.</p><p>Why? Because I noticed around 10PM that jumpcomedy.com was entirely broken with all HTTP POST calls made by RTK Query failing. Nothing worked and though I had deployed recent changes, none of them would cause this. I was at a complete loss as to where to look, especially as this is working locally. Posting on the usual Discords (NextJS, Vercel) is leading to dead silence. I'm alone and have to fix this issue which I didn't cause.</p><p>This isn't the first production defect I've introduced in my 25 years of working, but this is the first one where I had absolutely nobody to turn to in a time of crisis while customer complaints are piling up at a rate never seen before. No production support, no SRE, no Sr. Engineer, no manager to make it go away. Nothing. And here's the worst part: people who have taken a chance on me to the point where their entire small businesses depend on me are sad. Not only do I have no idea how to fix this, I'm also hurting people. This absolutely sucks. I felt shame, sorrow, and incompetence. Oh the incompetence and the imposter syndrome that comes with it.</p><p>The thoughts that were crossing my mind were bizarre: do I just shut this business down? Do I send a mass apology email to my customers and just ask them to pick a different event management provider? What do I do because I don't know where to look and it's been four hours already.</p><p>Enter Eminem. <em>Alright, calm down, relax, start breathin'</em></p><p>I started breathing but it didn't help a damn as I still didn't know what the issue was. No matter how many <code>console.log()</code> statements I sprinkled around, nothing made sense. Was it the headers, the length of the API token, the sequence of calls...but it was just working. Why? WHY? WHY???? IS THIS HAPPENING? And why are GET and DELETE calls working?</p><p>It's OK. The world won't end. So what if your business entirely fails and you're paraded at the next tech conference as a case of what not to do. Oh well, that's your destiny, just deal with it BUT right now deal with this goddamn bug that you didn't cause but have to suffer through. The only clue I have is that it's working on localhost, which reminded me of that old joke where during a production outage the junior developer tells his boss, "but it's working on my machine". Well buddy, you're the junior developer. Also, you're a sack of shit. No, no, don't go there. There's plenty of time for self-reflection and self-hate later, but right now just see why those cursed POST calls are failing with:</p><p><em>TypeError: failed to execute 'fetch' on 'window': &#8230;with a request object that has already been used</em></p><p>Now that error message is a complete red herring and tells me nothing. It may as well have said, "The Lannisters refuse to pay their debts and flight UA763 from Miami is delayed".</p><p>Haha. I start making jokes to add some levity to the situation. It's not so bad, life is about nature and trees and sooner this business shuts down and you take a boat to a deserted island, the sooner you can start your memoirs and the first chapter of the memoir would be: <em>TypeError: failed to execute 'fetch'</em>.</p><p>My wife. Oh my poor wife. She offered me a cup of tea and ruffled my hair. "It's OK, big companies have production outages too". Ah, that's so sweet of her. I told her to go to bed while I question every major life decision leading up to this moment. Oh shit, what's this? It's customer emails piling up in my inbox. Lovely.</p><p>"Hey Zarar, I can't change the the price of my event"</p><p>"Hi Zarar, I'm trying to remove a promo code and it won't let me"</p><p>....</p><p>Please, can I just delete my email at this point and take a bus to the northern wilderness? Because I still have no clue what's going on and now I'm thinking maybe I should take that break to clear my head. You know, like they say in those self-help books, but what they don't say is that every five minutes I'm getting an email saying something's broken and my response is basically, "I apologize. Working on it". But I'm not working on it, I'm just staring at the screen putting debug statements where I feel Chrome Inspector is saying, "Bro you serious? You think there's a bug on <em>this</em> line?"</p><p>Ah, what's this? A Chrome update came in today? Could that have caused it? Hmmm...hope, I see hope. DASHED. HOPE IS DASHED! This is reproducible in Firefox and Edge. Edge? Even Edge is like WTF. Back to console.log() and break points. Now I'm dealing with source maps and libraries that don't publish source maps so now I'm looking at code that looks like this:</p><pre><code>eC=Math.random().toString(36).slice(2),eE="__reactFiber$"+eC,ex="__reactProps$"+eC,ez="__reactContainer$"+eC,eP="__reactEvents$"+eC,eN="__reactListeners$"+eC,e_="__reactHandles$"+eC,eL="__reactResources$"+eC,eT="__reactMarker$"+eC;
</code></pre><p>This is no good. Let me just try reverting to a version from a month ago. Nothing. Three months ago? Nothing. Still failing. A year ago? Zilch.</p><p>OK, so you re-ask the question what's happening in prod that's happening locally. Or vice-versa. Some candidates:</p><ul><li><p>Sentry is disabled locally</p></li><li><p>Databases are pointing to docker instead of cloud providers</p></li></ul><p>Got rid of Sentry in production. Nothing. Pointed to PROD databases locally. Nothing. Maybe I should take that break, if only to calculate the financial damage and the much more significant reputational damage.</p><p>What else is different? Maybe PostHog, I have the api_key blanked out locally to reduce costs, so let me just add it to see what gives. Shot in the dark. 1 in a million chance. Let's do it.</p><p>WHAT?! REPRODUCED ON LOCALHOST. GIVE ME THAT FUCKING CUP OF TEA NOW!</p><p>Next commit: take out PostHog and everything is working.</p><p>At this point I'm thinking all the people I've recommended PostHog to as this "amazing tool which <em>shows</em> you what your users are experiencing". How naive I was? Right now I hate PostHog more than anything and can't believe I was about to pay for that product (still a good product, I'm overreacting here). But still, in the moment I wanted to burn the company down.</p><p>But I did feel good about finding the defect because soon after many people reported the same:</p><p><a href="https://github.com/PostHog/posthog/issues/24471">https://github.com/PostHog/posthog/issues/24471</a></p><p><a href="https://github.com/reduxjs/redux-toolkit/issues/4573">https://github.com/reduxjs/redux-toolkit/issues/4573</a></p><p>I write more on <a href="https://zarar.dev/">zarar.dev</a>. I don&#8217;t post as much on Substack as the editor isn&#8217;t really conducive to code.</p>]]></content:encoded></item><item><title><![CDATA[Empowered Developers Write Clean Code]]></title><description><![CDATA[A talk with Tom Howlett, Head of Product Management at Sonar]]></description><link>https://bitbytebit.substack.com/p/empowered-developers-write-clean</link><guid isPermaLink="false">https://bitbytebit.substack.com/p/empowered-developers-write-clean</guid><dc:creator><![CDATA[Zarar Siddiqi]]></dc:creator><pubDate>Mon, 12 Aug 2024 18:09:42 GMT</pubDate><enclosure url="https://substackcdn.com/image/youtube/w_728,c_limit/jk_uXT-J-O0" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Site note: I&#8217;m writing a lot more on my blog <a href="https://zarar.dev/">zarar.dev</a> because I don&#8217;t want to spam people&#8217;s inbox&#8217;s in Substack as many of the posts can be technical and not really suited for Substack formatting (e.g., code snippets).</p><p>However, very excited to have <a href="https://www.linkedin.com/in/tom-howlett-85ab6716/">Tom Howlett</a>, Head of Product Management at Sonar as our guest for this episode of the Continuous Delivery Podcast. </p><div id="youtube2-jk_uXT-J-O0" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;jk_uXT-J-O0&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/jk_uXT-J-O0?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div>]]></content:encoded></item></channel></rss>