<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: mohammed faizan mohiuddin</title>
    <description>The latest articles on DEV Community by mohammed faizan mohiuddin (@faizanmohiuddin482).</description>
    <link>https://dev.to/faizanmohiuddin482</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F476520%2Fc3d62dec-0a2f-4745-b8eb-a4abc4ed37f0.png</url>
      <title>DEV Community: mohammed faizan mohiuddin</title>
      <link>https://dev.to/faizanmohiuddin482</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/faizanmohiuddin482"/>
    <language>en</language>
    <item>
      <title>Smashing an "index out of range [-1]" crash in lazygit (80k⭐)</title>
      <dc:creator>mohammed faizan mohiuddin</dc:creator>
      <pubDate>Mon, 27 Jul 2026 10:32:45 +0000</pubDate>
      <link>https://dev.to/faizanmohiuddin482/smashing-an-index-out-of-range-1-crash-in-lazygit-80k-2902</link>
      <guid>https://dev.to/faizanmohiuddin482/smashing-an-index-out-of-range-1-crash-in-lazygit-80k-2902</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7z1ez71qiytknn07ca65.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7z1ez71qiytknn07ca65.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fethc1rguq08rbd1ltf3h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fethc1rguq08rbd1ltf3h.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fky5swhus79f56pes2i0m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fky5swhus79f56pes2i0m.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsszm9umiq82dhcswolvv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsszm9umiq82dhcswolvv.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcac6drwcftppad6ge24f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcac6drwcftppad6ge24f.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmx1lwazhmyp8vvm5uzmw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmx1lwazhmyp8vvm5uzmw.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69q3ec5jhreq7wvo8ohu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69q3ec5jhreq7wvo8ohu.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/jesseduffield/lazygit" rel="noopener noreferrer"&gt;lazygit&lt;/a&gt; is a terminal UI for git used by tens of thousands of developers daily. One of its power features is the &lt;strong&gt;custom patch builder&lt;/strong&gt; — you cherry-pick individual hunks or files out of commits and move them around. I picked up &lt;a href="https://github.com/jesseduffield/lazygit/issues/5802" rel="noopener noreferrer"&gt;issue #5802&lt;/a&gt;: a hard crash (&lt;code&gt;panic: runtime error: index out of range [-1]&lt;/code&gt;) triggered through that feature.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repro:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a custom patch from a commit on branch &lt;code&gt;B&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check out branch &lt;code&gt;A&lt;/code&gt;, which doesn't contain that commit.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;"Move patch out into index."&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;💥 &lt;code&gt;panic: runtime error: index out of range [-1]&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Root cause.&lt;/strong&gt; The patch remembers the &lt;em&gt;hash&lt;/em&gt; of the commit it was built from. When the action fires, lazygit looks that hash up in the currently-displayed commits to get an index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;CustomPatchOptionsMenuAction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;getPatchCommitIndex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commit&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commits&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Git&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Patch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PatchBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="c"&gt;// &amp;lt;- not found (e.g. after switching branches)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the checkout, that commit isn't in the list, so the function returns the sentinel &lt;code&gt;-1&lt;/code&gt;. That &lt;code&gt;-1&lt;/code&gt; is passed straight into a slice index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// commits[-1] panics&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;commitIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;commitIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsMerge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The existing guard only checks the &lt;strong&gt;upper&lt;/strong&gt; bound. &lt;code&gt;-1 &amp;lt; len(commits)&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, so execution proceeds to &lt;code&gt;commits[-1]&lt;/code&gt; and Go panics. And it isn't just the one action — the same &lt;code&gt;-1&lt;/code&gt; flows into &lt;strong&gt;all five&lt;/strong&gt; custom-patch menu handlers (delete-from-commit, move-to-selected-commit, move-into-index, and the two pull-into-new-commit actions).&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PR: &lt;a href="https://github.com/jesseduffield/lazygit/pull/5865" rel="noopener noreferrer"&gt;jesseduffield/lazygit#5865&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rather than sprinkle lower-bound guards on every slice access downstream, I fixed it at the &lt;strong&gt;source&lt;/strong&gt; of the bad value: &lt;code&gt;getPatchCommitIndex()&lt;/code&gt; now returns an error when the commit isn't found, and every caller surfaces a friendly message instead of crashing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;CustomPatchOptionsMenuAction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;getPatchCommitIndex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commit&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commits&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Git&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Patch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PatchBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PatchCommitNotInCommitsErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;commitIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getPatchCommitIndex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, instead of a crash, you get:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cannot find the commit this custom patch was created from in the current commits list. This can happen after switching branches; recreate the patch to continue.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed the root cause, not the symptom.&lt;/strong&gt; One unhandled sentinel fed five different crash paths. Guarding each downstream slice access would've been five band-aids; making the lookup return an error fixes the whole class in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Added a regression test that actually reproduces it.&lt;/strong&gt; lazygit has a great integration-test harness, so I wrote &lt;code&gt;MoveToIndexWhenCommitNotInCurrentBranch&lt;/code&gt;: it builds a patch, checks out a branch without the source commit, and moves the patch into the index.

&lt;ul&gt;
&lt;li&gt;On &lt;code&gt;master&lt;/code&gt;: &lt;strong&gt;fails with &lt;code&gt;panic: runtime error: index out of range [-1]&lt;/code&gt;&lt;/strong&gt; — the exact reported crash.&lt;/li&gt;
&lt;li&gt;With the fix: &lt;strong&gt;passes&lt;/strong&gt; (asserts the friendly error popup).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flagged a sibling.&lt;/strong&gt; &lt;code&gt;PatchBuildingController&lt;/code&gt; has the same &lt;code&gt;return -1&lt;/code&gt; pattern; I noted it in the PR and offered to cover it too, keeping this PR scoped to the reported crash.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;I used &lt;strong&gt;Google's Gemini 3.6 Flash&lt;/strong&gt; (via &lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;AI Studio&lt;/a&gt;) to independently pressure-test my diagnosis before writing the fix. I gave it the buggy function, the downstream slice access, and the repro, and asked for the root cause and minimal fix. It was sharp:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; an unhandled sentinel value (-1) representing a missing commit… &lt;code&gt;getPatchCommitIndex()&lt;/code&gt; fails to match the hash and returns -1 … &lt;code&gt;commits[-1]&lt;/code&gt; triggers Go's runtime panic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the guard fails:&lt;/strong&gt; &lt;code&gt;commitIndex &amp;lt; len(commits)&lt;/code&gt; only checks the upper bound. &lt;code&gt;-1 &amp;lt; len(commits)&lt;/code&gt; evaluates to &lt;code&gt;true&lt;/code&gt;, so short-circuiting doesn't prevent &lt;code&gt;commits[commitIndex].IsMerge()&lt;/code&gt; from accessing &lt;code&gt;commits[-1]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix — Option B (idiomatic):&lt;/strong&gt; Since -1 means the commit was not found, the operation should fail early or display an error rather than rebasing against a non-existent index.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That matched my conclusion exactly — and notably Gemini preferred &lt;strong&gt;Option B (guard at the caller and return an error)&lt;/strong&gt; over Option A (just add a &lt;code&gt;&amp;gt;= 0&lt;/code&gt; bound check), which is the more idiomatic, user-friendly fix and the one I shipped. Where AI got me ~90% of the way (correct root cause + the right fix shape), the remaining human work was the part it couldn't see from a snippet: that the same sentinel hit &lt;strong&gt;five&lt;/strong&gt; handlers, matching lazygit's i18n/error conventions, and proving it with an integration test that fails before and passes after.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgzd0e0ege53l8147duxs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgzd0e0ege53l8147duxs.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; a real crash in an 80k⭐ tool, fixed at the root, covered by a test that reproduces the exact panic. PR: &lt;a href="https://github.com/jesseduffield/lazygit/pull/5865" rel="noopener noreferrer"&gt;#5865&lt;/a&gt; · Issue: &lt;a href="https://github.com/jesseduffield/lazygit/issues/5802" rel="noopener noreferrer"&gt;#5802&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>go</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I made my AI agent break into its own code — and it smashed an IDOR</title>
      <dc:creator>mohammed faizan mohiuddin</dc:creator>
      <pubDate>Mon, 27 Jul 2026 09:11:41 +0000</pubDate>
      <link>https://dev.to/faizanmohiuddin482/i-made-my-ai-agent-break-into-its-own-code-and-it-smashed-an-idor-1a60</link>
      <guid>https://dev.to/faizanmohiuddin482/i-made-my-ai-agent-break-into-its-own-code-and-it-smashed-an-idor-1a60</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F53zba0diau0axu2ikmny.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F53zba0diau0axu2ikmny.png" alt=" " width="800" height="418"&gt;&lt;/a&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The scariest bugs aren't the ones that crash. They're the ones that work &lt;em&gt;perfectly&lt;/em&gt; in every demo and quietly hand your data to the wrong person.&lt;/p&gt;

&lt;p&gt;Here's one that ships constantly — I've watched AI coding agents write it, confidently, and end the turn with "Done ✅." So I built something to catch it, and this is the first bug it smashed.&lt;/p&gt;

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

&lt;p&gt;Ask any coding agent for the most boring endpoint in the world:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Add an endpoint to fetch an invoice by id."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You get this back, and it looks fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/invoice/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`SELECT * FROM invoices WHERE id = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It passes the demo. &lt;code&gt;GET /invoice/1&lt;/code&gt; returns invoice #1. Ship it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smash
&lt;/h2&gt;

&lt;p&gt;There are &lt;strong&gt;two&lt;/strong&gt; bugs hiding in those four lines, and both are invisible on the happy path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;IDOR (broken object-level authorization).&lt;/strong&gt; Nothing checks that the invoice belongs to the caller. &lt;code&gt;GET /invoice/2&lt;/code&gt; as user 1 cheerfully returns another tenant's invoice. This is &lt;a href="https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/" rel="noopener noreferrer"&gt;the #1 item on the OWASP API Security Top 10&lt;/a&gt; — and it's a one-liner to introduce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL injection.&lt;/strong&gt; &lt;code&gt;req.params.id&lt;/code&gt; is interpolated straight into the query string. &lt;code&gt;GET /invoice/1;DROP TABLE invoices--&lt;/code&gt; is now a conversation you're having with your database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither shows up when you test with your own account. That's exactly why they ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  What caught it
&lt;/h2&gt;

&lt;p&gt;I'd gotten tired of "Done ✅" on code like this, so I built a skill for my AI agent called &lt;strong&gt;the Attacker&lt;/strong&gt;. The rule is simple: after it writes anything that crosses a trust boundary — untrusted input, auth, a database query — it stops being the author and becomes the attacker. It doesn't recite an OWASP checklist; it actually tries to break &lt;em&gt;this&lt;/em&gt; code.&lt;/p&gt;

&lt;p&gt;Pointed at the endpoint above, it reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attacked:
- hit /invoice/2 as user 1 → leaked another tenant's row (IDOR) → added owner scope
- non-numeric id → 500 with a stack trace → now a clean 400
- SQL was string-interpolated → parameterized it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it fixed the bug at the boundary, not on the one path I happened to test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/invoice/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bad id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM invoices WHERE id = $1 AND owner_id = $2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;                 &lt;span class="c1"&gt;// ownership scope kills the IDOR&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// same 404 for "missing" and&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                            &lt;span class="c1"&gt;// "not yours" — don't confirm it exists&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three fixes, each aimed at the &lt;em&gt;class&lt;/em&gt; of problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parameterized query&lt;/strong&gt; → the injection is gone, not escaped-and-hoped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AND owner_id = $2&lt;/code&gt;&lt;/strong&gt; → the row is scoped to the caller, so the IDOR can't happen even if a new route forgets to check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identical 404 for "missing" vs "not yours"&lt;/strong&gt; → a subtle one: returning 403 for existing-but-forbidden ids leaks &lt;em&gt;which invoices exist&lt;/em&gt;. Same response either way closes that enumeration side channel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;400&lt;/code&gt; on a non-numeric id&lt;/strong&gt; instead of a 500 that dumps a stack trace.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The lesson (worth more than the fix)
&lt;/h2&gt;

&lt;p&gt;The fix is boring. The &lt;em&gt;reflex&lt;/em&gt; is the point:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It works" in a demo is precisely the moment these bugs ship. The only code you trust is the code you already tried to break.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;IDOR and injection don't survive because they're hard to fix — they survive because nobody attacks the happy path. Whether it's you, a teammate, or an AI agent writing the endpoint, the discipline is the same: the moment code touches untrusted input or a query, put on the black hat before someone else does.&lt;/p&gt;

&lt;p&gt;I ended up bundling the Attacker into an open-source set of "senior-dev instinct" skills for AI agents called &lt;a href="https://github.com/faizanmohiuddin482/bullpen" rel="noopener noreferrer"&gt;Bullpen&lt;/a&gt; — but you don't need any of that to steal the habit. Next time your agent says "Done ✅" on an endpoint, ask it one question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Now attack it."&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>ai</category>
      <category>security</category>
    </item>
    <item>
      <title>I gave my AI coding agent 10 senior-dev instincts</title>
      <dc:creator>mohammed faizan mohiuddin</dc:creator>
      <pubDate>Mon, 27 Jul 2026 08:56:42 +0000</pubDate>
      <link>https://dev.to/faizanmohiuddin482/i-gave-my-ai-coding-agent-10-senior-dev-instincts-47lp</link>
      <guid>https://dev.to/faizanmohiuddin482/i-gave-my-ai-coding-agent-10-senior-dev-instincts-47lp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fye06htqo99mfvxbl3uwy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fye06htqo99mfvxbl3uwy.png" alt=" " width="800" height="418"&gt;&lt;/a&gt;&lt;br&gt;
AI coding agents are like a junior who read every textbook and shipped nothing:&lt;br&gt;
fast, confident, and wrong in ways that cost you an afternoon.&lt;/p&gt;

&lt;p&gt;The senior engineers I've learned the most from aren't valuable because they type&lt;br&gt;
fast. It's their &lt;strong&gt;instincts&lt;/strong&gt; — the things they do &lt;em&gt;before&lt;/em&gt; and &lt;em&gt;after&lt;/em&gt; the code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;they push back when you ask for the wrong thing&lt;/li&gt;
&lt;li&gt;they don't say "done" until they've actually run it&lt;/li&gt;
&lt;li&gt;they try to break their own code before it ships&lt;/li&gt;
&lt;li&gt;they ask the one question that saves a week of building the wrong feature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I bottled ten of those into &lt;strong&gt;Bullpen&lt;/strong&gt; — a set of skills for AI coding agents&lt;br&gt;
(built for Claude Code). You call them in by name, at the intensity you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  The roster
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;The instinct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;skeptic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pushes back when the request is the wrong idea&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;closer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Won't claim "done" until it ran the code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;attacker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Red-teams its own code before shipping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fact-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verifies an API exists before calling it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;interrogator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Asks the few decisive questions first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stop-digging&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;After two failed fixes, re-diagnoses instead of thrashing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;doorman&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Justifies every new dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;historian&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Checks &lt;em&gt;why&lt;/em&gt; odd code exists before deleting it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chameleon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Matches the existing codebase's style&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;explainer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Splits work into reviewable commits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each has &lt;code&gt;lite&lt;/code&gt; / &lt;code&gt;full&lt;/code&gt; / &lt;code&gt;ultra&lt;/code&gt; intensity.&lt;/p&gt;

&lt;h2&gt;
  
  
  One example: /attacker
&lt;/h2&gt;

&lt;p&gt;Ask an agent for "an endpoint to fetch an invoice by id" and it'll hand you this,&lt;br&gt;
then say "Done":&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
js
app.get('/invoice/:id', async (req, res) =&amp;gt; {
  const invoice = await db.query(
    `SELECT * FROM invoices WHERE id = ${req.params.id}`);
  res.json(invoice);
});

Hit /invoice/2 as user 1 and you read another tenant's invoice. The id
concatenates straight into SQL. With /attacker, it breaks into its own code
first — and fixes it at the boundary:

app.get('/invoice/:id', async (req, res) =&amp;gt; {
  const id = Number(req.params.id);
  if (!Number.isInteger(id)) return res.status(400).json({ error: 'bad id' });
  const invoice = await db.query(
    'SELECT * FROM invoices WHERE id = $1 AND owner_id = $2', [id, req.user.id]);
  if (!invoice) return res.status(404).end();
  res.json(invoice);
});

▎ Attacked: /invoice/2 as user 1 → leaked another tenant's row, added owner filter · non-numeric id → 500 with a stack trace, now 400 · SQL parameterized, held.

It never claims "secure." It reports what it tried.

Install

/plugin marketplace add faizanmohiuddin482/bullpen
/plugin install bullpen@bullpen

Open source (MIT), one canonical SKILL.md per skill — no hooks, no bloat.

Repo → https://github.com/faizanmohiuddin482/bullpen

Inspired by Ponytail (https://github.com/DietrichGebert/ponytail) — one lazy
senior dev. Bullpen is the whole room. 🧢

What senior-dev instinct would you add?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
