<?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: JustJinoIT</title>
    <description>The latest articles on DEV Community by JustJinoIT (@justjinoit).</description>
    <link>https://dev.to/justjinoit</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%2F3971008%2F36558f2c-e9c9-4b2f-bdf0-caeabca94863.png</url>
      <title>DEV Community: JustJinoIT</title>
      <link>https://dev.to/justjinoit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/justjinoit"/>
    <language>en</language>
    <item>
      <title>I lost every 2FA method on GitHub. The way out was an SSH key I forgot on a server</title>
      <dc:creator>JustJinoIT</dc:creator>
      <pubDate>Fri, 13 Nov 2026 12:41:00 +0000</pubDate>
      <link>https://dev.to/justjinoit/i-lost-every-2fa-method-on-github-the-way-out-was-an-ssh-key-i-forgot-on-a-server-4n8f</link>
      <guid>https://dev.to/justjinoit/i-lost-every-2fa-method-on-github-the-way-out-was-an-ssh-key-i-forgot-on-a-server-4n8f</guid>
      <description>&lt;p&gt;I lost every 2FA method on my GitHub account. Web login has been dead since August. &lt;code&gt;gh&lt;/code&gt; in the terminal still pushed fine, so I kept putting it off. This time I tried everything.&lt;/p&gt;

&lt;p&gt;The way out was on a server: an SSH key I registered to my account back in May while setting up a side-project server. I put it there and forgot about it. It got my recovery request through, and GitHub is still reviewing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What didn't work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;2FA app: I wiped and sold the phone in July&lt;/li&gt;
&lt;li&gt;Recovery codes: GitHub's emails show I viewed them in March. I couldn't find where I saved them&lt;/li&gt;
&lt;li&gt;Passkey: it lived in Edge on a Windows laptop. The laptop is gone&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gh&lt;/code&gt; token: the recovery docs only list personal access tokens you create yourself. The &lt;code&gt;gh&lt;/code&gt; login token isn't on the list&lt;/li&gt;
&lt;li&gt;Old PATs: expired, or I don't know where they are&lt;/li&gt;
&lt;li&gt;A recovery request in August: submitted, still locked out&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  I almost gave up on the account
&lt;/h2&gt;

&lt;p&gt;Moving my repos to a new account changes the GitHub Pages URLs, which would break links I'd already submitted to contests. And contributions follow the commit email, which is tied to the locked account.&lt;/p&gt;

&lt;p&gt;You can unlink just the email address from a locked account, then add that email to a new account and keep your commit history. But you can't undo it. I kept that as a last resort.&lt;/p&gt;

&lt;p&gt;In the meantime I pulled all 22 repos with &lt;code&gt;git clone --mirror&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  It was on the server
&lt;/h2&gt;

&lt;p&gt;GitHub account recovery asks for one more thing after email verification: a device you've signed in with before, an SSH key you registered before, or a PAT you created before. My account had one SSH key registered, and it didn't match any key on my Mac.&lt;/p&gt;

&lt;p&gt;Digging through GitHub's security emails, I found "A new SSH authentication public key was added to your account", sent the day I set up that server in May. I wanted the server to pull from a private repo, so I generated a key on the server and added it straight to my account. In June I switched to a repo-only deploy key and never removed the old one.&lt;/p&gt;

&lt;p&gt;Why did I even put it there? It saved me, though.&lt;/p&gt;

&lt;p&gt;That key got me through the SSH verification step of the recovery request. Now I'm waiting for GitHub to review it.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to check yours
&lt;/h2&gt;

&lt;p&gt;The easiest way is to hand it to an AI agent. That's how I found mine, with Claude Code. Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I lost 2FA on my GitHub account (&amp;lt;username&amp;gt;) and can't log in on the web.
Find an "SSH key previously registered to the account" that I can use for recovery.

1. Get the fingerprints of keys registered to the account from https://github.com/&amp;lt;username&amp;gt;.keys
2. Compare them with the ~/.ssh key fingerprints on this machine and on servers I can reach
3. If nothing matches, search my email for GitHub "SSH key was added" alerts
   to see when and where it was registered
4. If you find it, check that ssh -i &amp;lt;key&amp;gt; -o IdentitiesOnly=yes -T git@github.com
   says "Hi &amp;lt;username&amp;gt;!"

Don't print private keys or tokens. Only show locations and fingerprints.
Ask me before connecting to any server.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To do it by hand:&lt;/p&gt;

&lt;p&gt;Fingerprints of keys registered to your account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -s https://github.com/&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;username&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;.keys &lt;span class="o"&gt;|&lt;/span&gt; ssh-keygen -lf -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find a matching key on each machine you have (servers included):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; k &lt;span class="k"&gt;in&lt;/span&gt; ~/.ssh/*&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;fp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;ssh-keygen -y -P &lt;span class="s2"&gt;""&lt;/span&gt; -f &lt;span class="s2"&gt;"$k"&lt;/span&gt; 2&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;|&lt;/span&gt; ssh-keygen -lf - 2&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; -n &lt;span class="s2"&gt;"$fp"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"$fp $k"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; grep -F &lt;span class="s2"&gt;"$(curl -s https://github.com/&amp;lt;username&amp;gt;.keys | ssh-keygen -lf - | awk '{print $2}')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check it. &lt;code&gt;Hi &amp;lt;username&amp;gt;!&lt;/code&gt; means you can use it. &lt;code&gt;Hi &amp;lt;username&amp;gt;/&amp;lt;repo&amp;gt;!&lt;/code&gt; means it's a repo-only deploy key, which won't work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh -i &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;path to key&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; -o &lt;span class="nv"&gt;IdentitiesOnly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes -T git@github.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Worth checking now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do you know where your recovery codes are?&lt;/li&gt;
&lt;li&gt;Is your passkey on only one device?&lt;/li&gt;
&lt;li&gt;Have you ever put an account-level SSH key on a server?&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>security</category>
      <category>ssh</category>
      <category>git</category>
    </item>
    <item>
      <title>GitHub 2FA를 통째로 잃었다. 남은 건 서버에 두고 잊은 SSH 키였다</title>
      <dc:creator>JustJinoIT</dc:creator>
      <pubDate>Fri, 13 Nov 2026 12:40:54 +0000</pubDate>
      <link>https://dev.to/justjinoit/github-2fareul-tongjjaero-ilheossda-nameun-geon-seobeoe-dugo-ijeun-ssh-kiyeossda-1g40</link>
      <guid>https://dev.to/justjinoit/github-2fareul-tongjjaero-ilheossda-nameun-geon-seobeoe-dugo-ijeun-ssh-kiyeossda-1g40</guid>
      <description>&lt;p&gt;GitHub 2FA를 통째로 잃어버렸다. 8월부터 웹 로그인이 안 됐다. 터미널은 &lt;code&gt;gh&lt;/code&gt;로 push가 계속 되니까 미뤄 두다가, 이번에 별짓을 다 해 봤다.&lt;/p&gt;

&lt;p&gt;결론은 서버였다. 5월에 사이드 프로젝트 서버 세팅하면서 계정에 등록해 둔 SSH 키. 내가 올려 놓고 까먹었다.&lt;/p&gt;

&lt;h2&gt;
  
  
  안 됐던 것들
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;2FA 앱: 폰을 7월에 초기화해서 팔았다&lt;/li&gt;
&lt;li&gt;복구 코드: 3월에 열어 본 기록은 있는데 어디 저장했는지 못 찾았다&lt;/li&gt;
&lt;li&gt;패스키: 윈도우 노트북 Edge에 있었다. 노트북은 버렸다&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gh&lt;/code&gt; 토큰: 복구 문서엔 직접 만든 PAT만 나온다. &lt;code&gt;gh&lt;/code&gt; 로그인 토큰은 없다&lt;/li&gt;
&lt;li&gt;예전 PAT: 만료됐거나 어디 뒀는지 모른다&lt;/li&gt;
&lt;li&gt;8월 복구 요청: 넣었는데 안 풀렸다&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  계정을 버릴까도 했다
&lt;/h2&gt;

&lt;p&gt;새 계정으로 저장소를 옮기면 GitHub Pages 주소가 바뀐다. 공모전에 낸 링크가 깨진다. 잔디(기여 그래프)도 이메일을 따라가는데 그 이메일이 잠긴 계정에 묶여 있다.&lt;/p&gt;

&lt;p&gt;잠긴 계정에서 이메일 연결만 끊는 방법은 있다. 끊은 이메일을 새 계정에 붙이면 커밋 기록은 유지된다. 대신 되돌릴 수 없다. 이건 마지막으로 미뤘다.&lt;/p&gt;

&lt;p&gt;일단 저장소 22개는 &lt;code&gt;git clone --mirror&lt;/code&gt;로 다 받아 놨다.&lt;/p&gt;

&lt;h2&gt;
  
  
  서버에 있었다
&lt;/h2&gt;

&lt;p&gt;GitHub 복구 요청은 메일 인증 말고 하나를 더 요구한다. 예전에 로그인한 기기, 예전에 등록한 SSH 키, 예전에 만든 PAT 중 하나. 계정에 등록된 키는 1개인데 맥에 있는 키랑은 안 맞았다.&lt;/p&gt;

&lt;p&gt;GitHub 알림 메일을 뒤지니 5월 서버 세팅하던 날 "계정에 새 SSH 키가 등록됐다"는 메일이 있었다. 서버가 비공개 저장소를 pull하게 하려고 서버에서 만든 키를 계정에 바로 넣었던 거다. 6월에 저장소 전용 키로 바꾸면서 이건 안 지웠다.&lt;/p&gt;

&lt;p&gt;왜 올렸을까 이걸. 그 덕에 살았지만.&lt;/p&gt;

&lt;p&gt;이 키로 복구 요청 SSH 인증까지 통과했다. 지금은 GitHub 검토 기다리는 중.&lt;/p&gt;

&lt;h2&gt;
  
  
  나도 찾아보려면
&lt;/h2&gt;

&lt;p&gt;제일 쉬운 건 AI 에이전트한테 시키는 거다. 나도 Claude Code한테 시켜서 찾았다. 이렇게 넣으면 된다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub 계정(&amp;lt;계정&amp;gt;) 2FA를 잃어서 웹 로그인이 안 된다.
복구 요청에 쓸 "예전에 계정에 등록한 SSH 키"를 찾아줘.

1. https://github.com/&amp;lt;계정&amp;gt;.keys 로 계정에 등록된 키 지문 확인
2. 이 컴퓨터와 내가 접속할 수 있는 서버의 ~/.ssh 키 지문과 비교
3. 못 찾으면 메일에서 GitHub "SSH key was added" 알림을 찾아
   언제 어디서 등록했는지 추적
4. 찾으면 ssh -i &amp;lt;키&amp;gt; -o IdentitiesOnly=yes -T git@github.com 으로
   "Hi &amp;lt;계정&amp;gt;!"이 나오는지 확인

개인키·토큰 값은 출력하지 말고 위치와 지문만 알려줘.
서버에 접속하기 전에는 먼저 물어봐.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;직접 하려면 아래 순서다.&lt;/p&gt;

&lt;p&gt;계정에 등록된 키 지문:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -s https://github.com/&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;계정&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;.keys &lt;span class="o"&gt;|&lt;/span&gt; ssh-keygen -lf -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;갖고 있는 기기(서버 포함)에서 지문 맞는 키 찾기:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; k &lt;span class="k"&gt;in&lt;/span&gt; ~/.ssh/*&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;fp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;ssh-keygen -y -P &lt;span class="s2"&gt;""&lt;/span&gt; -f &lt;span class="s2"&gt;"$k"&lt;/span&gt; 2&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;|&lt;/span&gt; ssh-keygen -lf - 2&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; -n &lt;span class="s2"&gt;"$fp"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"$fp $k"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; grep -F &lt;span class="s2"&gt;"$(curl -s https://github.com/&amp;lt;계정&amp;gt;.keys | ssh-keygen -lf - | awk '{print $2}')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;찾았으면 확인. &lt;code&gt;Hi &amp;lt;계정&amp;gt;!&lt;/code&gt;이면 쓸 수 있다. &lt;code&gt;Hi &amp;lt;계정&amp;gt;/&amp;lt;저장소&amp;gt;!&lt;/code&gt;면 저장소 전용 키라 못 쓴다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh -i &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;키 경로&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; -o &lt;span class="nv"&gt;IdentitiesOnly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes -T git@github.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  확인해 볼 것
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;복구 코드 어디 있는지 아나?&lt;/li&gt;
&lt;li&gt;패스키가 기기 한 대에만 있진 않나?&lt;/li&gt;
&lt;li&gt;서버에 계정용 SSH 키 올려 둔 적 있나?&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>security</category>
      <category>ssh</category>
      <category>git</category>
    </item>
    <item>
      <title>AI 커밋이 2배 더 고쳐진다고? 비교군이 하루치였다</title>
      <dc:creator>JustJinoIT</dc:creator>
      <pubDate>Thu, 15 Oct 2026 14:53:07 +0000</pubDate>
      <link>https://dev.to/justjinoit/ai-keomisi-2bae-deo-gocyeojindago-bigyoguni-haruciyeossda-19mg</link>
      <guid>https://dev.to/justjinoit/ai-keomisi-2bae-deo-gocyeojindago-bigyoguni-haruciyeossda-19mg</guid>
      <description>&lt;p&gt;&lt;a href="https://arstechnica.com/ai/2026/10/ai-coding-agents-generate-more-code-but-not-more-software/" rel="noopener noreferrer"&gt;Ars Technica&lt;/a&gt;에서 하버드 워킹페이퍼(Fiona Chen, James Stratton) 기사를 읽었다. 700여 개 회사의 작업 기록 약 3억 건을 분석했더니, AI 코딩 에이전트를 도입한 뒤 PR은 23%, 코드 생산량은 30% 정도 늘었는데 이슈 해결률은 통계적으로 의미 있게 달라지지 않았다고 한다. 병목은 리뷰였다. PR이 머지되기까지 걸리는 시간이 49% 늘었고 수정 요청은 거의 두 배가 됐다.&lt;/p&gt;

&lt;p&gt;내 레포도 그런지 재 봤다. 사이드 프로젝트 두 개(아래 레포 A, B)이고, 둘 다 6월 4일에 Claude와 함께 시작했다.&lt;/p&gt;

&lt;p&gt;메시지에 &lt;code&gt;Co-Authored-By: Claude&lt;/code&gt; 트레일러가 있으면 AI 커밋(245개), 없으면 "트레일러 없는 커밋"(65개)으로 나눴다. 지표는 제목이 &lt;code&gt;fix&lt;/code&gt;나 &lt;code&gt;revert&lt;/code&gt;로 시작하는 커밋의 비율이다.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;AI 커밋&lt;/th&gt;
&lt;th&gt;트레일러 없는 커밋&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;제목이 fix/revert로 시작&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;두 배가 넘는다. 연구와 같은 방향이다(같은 지표는 아니다). 이대로 글을 쓸 뻔했는데, 먼저 깨 보기로 했다.&lt;/p&gt;

&lt;h2&gt;
  
  
  확인 1: &lt;code&gt;fix:&lt;/code&gt;는 누가 제목에 쓰나
&lt;/h2&gt;

&lt;p&gt;AI 커밋의 69%가 &lt;code&gt;feat:&lt;/code&gt;, &lt;code&gt;fix:&lt;/code&gt; 같은 표준 형식 제목을 쓴다. 트레일러 없는 커밋은 18%다. 고칠 때마다 &lt;code&gt;fix:&lt;/code&gt;를 붙이는 에이전트는 제목을 읽는 지표에서 더 많이 잡힌다. fix, bug, revert, 수정, 버그, 고침, 오류 중 하나가 제목 어디에든 들어간 커밋으로 넓히면 격차가 벌써 줄어든다. 43% 대 26%다.&lt;/p&gt;

&lt;h2&gt;
  
  
  확인 2: 트레일러 없는 커밋은 누구 것인가
&lt;/h2&gt;

&lt;p&gt;이 커밋들을 사람 쪽 비교군으로 두고도 정작 열어 보지 않았다. 65개 중 57개가 6월 5일 하루에 몰려 있었다. 제목은 이런 식이다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Phase 0-1: Add core modules (logging, pipeline, batch)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Phase A: Semantic cache + batch processing integration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Enhancement Level 2: Sentiment analysis &amp;amp; topic classification&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;뼈대를 세우던 날의 커밋으로 읽힌다. 두 레포 모두 6월 4일의 첫 커밋부터 AI 트레일러가 있어서, AI가 없던 시기는 비교 기준으로 쓸 수도 없다. 이 커밋을 누가 썼는지 나는 말할 수 없다. 그게 문제다. 내 비교군은 누가 만들었는지 모르는 하루치 작업이었다.&lt;/p&gt;

&lt;h2&gt;
  
  
  확인 3: 커밋 크기
&lt;/h2&gt;

&lt;p&gt;AI 커밋은 한 번에 건드리는 파일이 더 많다. 평균 3.1개이고 다른 쪽은 1.7개다. "같은 파일이 72시간 안에 다시 수정됐나"를 커밋 단위로 세면 큰 커밋이 겹칠 기회가 더 많다. 커밋 단위로는 74% 대 68%였다. 파일 단위로 세서 커밋 크기를 뺐다.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;측정 방식&lt;/th&gt;
&lt;th&gt;AI 커밋&lt;/th&gt;
&lt;th&gt;트레일러 없는 커밋&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;제목이 fix/revert로 시작&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제목 어디든 fix·버그·수정 등 포함&lt;/td&gt;
&lt;td&gt;43%&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;건드린 파일이 72시간 안에 다시 수정됨 (파일 단위, 두 레포 합산)&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;td&gt;47%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;제목과 커밋 크기를 둘 다 뺀 마지막 줄에서는 차이가 없다.&lt;/p&gt;

&lt;h2&gt;
  
  
  그래서 이 숫자는
&lt;/h2&gt;

&lt;p&gt;두 레포만 가지고는 AI 커밋이 더 자주 고쳐지는지 덜 고쳐지는지 알 수 없다. 연구가 틀렸다는 말도 아니다. 표본이 작고 인과를 보여 주는 것도 아니다. 처음 나온 2배는 의도하지 않고 잰 세 가지에서 왔다. 제목 쓰는 습관, 하루치로 이뤄진 비교군, 커밋 크기.&lt;/p&gt;

&lt;p&gt;다음에 AI와 비AI 작업의 격차를 믿기 전에 먼저 이걸 본다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;비교군에 누가 들어 있고, 언제 커밋한 건가?&lt;/li&gt;
&lt;li&gt;내가 세는 라벨이 측정 대상의 습관에서 나온 건 아닌가?&lt;/li&gt;
&lt;li&gt;두 그룹이 같은 크기의 일을 하고 있나?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  내 레포에서 재 보려면
&lt;/h2&gt;

&lt;p&gt;표준 라이브러리만 쓴다. 첫 AI 커밋 이후의 커밋만 비교한다. 다른 도구를 쓴다면 &lt;code&gt;co-authored-by: claude&lt;/code&gt; 문자열만 바꾸면 된다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--no-merges&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--name-only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--pretty=format:&lt;/span&gt;&lt;span class="se"&gt;\x1e&lt;/span&gt;&lt;span class="s"&gt;%ct&lt;/span&gt;&lt;span class="se"&gt;\x1f&lt;/span&gt;&lt;span class="s"&gt;%B&lt;/span&gt;&lt;span class="se"&gt;\x1f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;

&lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;  &lt;span class="c1"&gt;# dict: 시각, AI 트레일러 여부, 제목, 건드린 파일
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x1e&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x1f&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;co-authored-by: claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                        &lt;span class="n"&gt;subj&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AI 트레일러가 붙은 커밋이 없다. 메시지에 Co-Authored-By: Claude 가 있는지 확인할 것&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# 첫 AI 커밋 이후만 비교
&lt;/span&gt;&lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;             &lt;span class="c1"&gt;# [파일 수, 72시간 안에 다시 수정된 파일 수]
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&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;d&lt;/span&gt; &lt;span class="ow"&gt;in&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;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]):&lt;/span&gt;
            &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;fix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subj&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;revert&lt;/span&gt;&lt;span class="sh"&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;date&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;n_f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AI       &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;트레일러 없음&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;커밋 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;개 | 제목이 fix/revert로 시작 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fix&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | 커밋당 파일 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;개&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | 파일 단위 72시간 내 재수정 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | 가장 몰린 날 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;내 두 레포에서는 이렇게 나왔다.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;레포&lt;/th&gt;
&lt;th&gt;그룹&lt;/th&gt;
&lt;th&gt;커밋&lt;/th&gt;
&lt;th&gt;fix/revert 제목&lt;/th&gt;
&lt;th&gt;커밋당 파일&lt;/th&gt;
&lt;th&gt;72시간 내 재수정(파일)&lt;/th&gt;
&lt;th&gt;가장 몰린 날&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;레포 A&lt;/td&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;145&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;2.9&lt;/td&gt;
&lt;td&gt;47%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;트레일러 없음&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;1.6&lt;/td&gt;
&lt;td&gt;52%&lt;/td&gt;
&lt;td&gt;62%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;레포 B&lt;/td&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;36%&lt;/td&gt;
&lt;td&gt;3.4&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;트레일러 없음&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;td&gt;1.8&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;마지막 열은 처음부터 찍어 봤어야 했다. 비교군의 커밋 대부분이 하루에 몰려 있다면, 어떤 배율이든 믿기 전에 그 하루부터 열어 보는 게 낫다.&lt;/p&gt;

&lt;p&gt;돌려 보면 "가장 몰린 날" 열이 어떻게 나오는지 궁금하다.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>git</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>My AI-vs-human commit comparison had a control group of one day</title>
      <dc:creator>JustJinoIT</dc:creator>
      <pubDate>Thu, 15 Oct 2026 14:52:44 +0000</pubDate>
      <link>https://dev.to/justjinoit/my-ai-vs-human-commit-comparison-had-a-control-group-of-one-day-3lc7</link>
      <guid>https://dev.to/justjinoit/my-ai-vs-human-commit-comparison-had-a-control-group-of-one-day-3lc7</guid>
      <description>&lt;p&gt;&lt;a href="https://arstechnica.com/ai/2026/10/ai-coding-agents-generate-more-code-but-not-more-software/" rel="noopener noreferrer"&gt;Ars Technica&lt;/a&gt; covered a Harvard working paper (Fiona Chen, James Stratton) built on about 300M work events from 700+ companies. After teams adopted coding agents, PRs went up 23% and code output about 30%, but issue resolution didn't move in a statistically significant way. Review was the bottleneck: time to merge rose 49%, and revision requests nearly doubled.&lt;/p&gt;

&lt;p&gt;I wanted to see if my own repos looked like that. So I ran one comparison on two of my side projects (Repo A and Repo B below), both built with Claude since June 4.&lt;/p&gt;

&lt;p&gt;AI commit = the message has a &lt;code&gt;Co-Authored-By: Claude&lt;/code&gt; trailer (245 commits). Everything else is "no trailer" (65 commits). Metric: share of commits whose subject starts with &lt;code&gt;fix&lt;/code&gt; or &lt;code&gt;revert&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;AI commits&lt;/th&gt;
&lt;th&gt;No-trailer commits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;subject starts with fix/revert&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;More than double. Same direction as the study, though not the same metric. I almost wrote the post right there. Then I tried to break it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 1: who writes &lt;code&gt;fix:&lt;/code&gt; in the subject?
&lt;/h2&gt;

&lt;p&gt;69% of the AI commits use &lt;code&gt;feat:&lt;/code&gt; / &lt;code&gt;fix:&lt;/code&gt; style subjects. Among the no-trailer commits it's 18%. An agent that stamps &lt;code&gt;fix:&lt;/code&gt; on every fix will get counted more by any metric that reads subjects. Widening the match to any subject containing fix, bug, revert or their Korean equivalents already shrinks the gap: 43% vs 26%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2: who are the no-trailer commits?
&lt;/h2&gt;

&lt;p&gt;I had been treating them as the human side of the comparison without looking at them. 57 of the 65 landed on a single day, June 5. Their subjects look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Phase 0-1: Add core modules (logging, pipeline, batch)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Phase A: Semantic cache + batch processing integration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Enhancement Level 2: Sentiment analysis &amp;amp; topic classification&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That reads like scaffolding day. Both repos also start on June 4 with an AI-trailer commit, so there is no pre-AI period in either one to use as a baseline. I can't say who wrote the no-trailer commits, and that is the problem: my "control group" is one day of work whose authorship I don't know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 3: commit size
&lt;/h2&gt;

&lt;p&gt;AI commits touch more files: 3.1 on average, against 1.7. If I count "the same file was touched again within 72 hours" per commit, bigger commits get more chances to overlap. Per commit it said 74% vs 68%. Counting per file instead, so that commit size drops out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;AI commits&lt;/th&gt;
&lt;th&gt;No-trailer commits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;subject starts with fix/revert&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;subject contains fix/bug/revert (or Korean equivalents) anywhere&lt;/td&gt;
&lt;td&gt;43%&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a touched file is edited again within 72h (per file, both repos combined)&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;td&gt;47%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row ignores subjects and commit size, and it shows no gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where that leaves the number
&lt;/h2&gt;

&lt;p&gt;I can't tell from two repos whether AI commits get fixed more or less. I'm not saying the study is wrong either: it's a tiny sample and nothing here shows cause. The 2x I started with came from three things I wasn't measuring on purpose: a naming habit, a control group made of one day, and commit size.&lt;/p&gt;

&lt;p&gt;Before I trust a gap between AI and non-AI work again, I want answers to these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who is actually in the comparison group, and when did they commit?&lt;/li&gt;
&lt;li&gt;Does the label I'm counting come from a habit of the thing I'm measuring?&lt;/li&gt;
&lt;li&gt;Are the two groups doing the same size of work?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run it on your repo
&lt;/h2&gt;

&lt;p&gt;Standard library only. It compares commits after the first AI commit. Change the &lt;code&gt;co-authored-by: claude&lt;/code&gt; string if you use another tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--no-merges&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--name-only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--pretty=format:&lt;/span&gt;&lt;span class="se"&gt;\x1e&lt;/span&gt;&lt;span class="s"&gt;%ct&lt;/span&gt;&lt;span class="se"&gt;\x1f&lt;/span&gt;&lt;span class="s"&gt;%B&lt;/span&gt;&lt;span class="se"&gt;\x1f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;

&lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;  &lt;span class="c1"&gt;# dict: time, AI trailer, subject, files touched
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x1e&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x1f&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;co-authored-by: claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                        &lt;span class="n"&gt;subj&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No commits with an AI trailer found. Check for Co-Authored-By: Claude in messages.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# only compare after the first AI commit
&lt;/span&gt;&lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;             &lt;span class="c1"&gt;# [files, files edited again within 72h]
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&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;d&lt;/span&gt; &lt;span class="ow"&gt;in&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;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]):&lt;/span&gt;
            &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;fix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subj&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;revert&lt;/span&gt;&lt;span class="sh"&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;date&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;n_f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AI        &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no trailer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; commits | subject starts with fix/revert &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fix&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; files per commit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | file re-edited within 72h &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | busiest day holds &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of the group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my two repos:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Commits&lt;/th&gt;
&lt;th&gt;fix/revert subject&lt;/th&gt;
&lt;th&gt;Files per commit&lt;/th&gt;
&lt;th&gt;File re-edited in 72h&lt;/th&gt;
&lt;th&gt;Busiest day&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repo A&lt;/td&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;145&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;2.9&lt;/td&gt;
&lt;td&gt;47%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;no trailer&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;1.6&lt;/td&gt;
&lt;td&gt;52%&lt;/td&gt;
&lt;td&gt;62%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repo B&lt;/td&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;36%&lt;/td&gt;
&lt;td&gt;3.4&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;no trailer&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;td&gt;1.8&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last column is the one I wish I had printed on day one. If your comparison group has most of its commits on one day, stop and look at it before you trust any ratio.&lt;/p&gt;

&lt;p&gt;If you run it, what does your busiest-day column look like?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>git</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>AstaBrief-8B가 Claude보다 3.5배 빠르다고? 직접 재현해보니</title>
      <dc:creator>JustJinoIT</dc:creator>
      <pubDate>Mon, 05 Oct 2026 11:05:14 +0000</pubDate>
      <link>https://dev.to/justjinoit/astabrief-8bga-claudeboda-35bae-bbareudago-jigjeob-jaehyeonhaeboni-54a4</link>
      <guid>https://dev.to/justjinoit/astabrief-8bga-claudeboda-35bae-bbareudago-jigjeob-jaehyeonhaeboni-54a4</guid>
      <description>&lt;h2&gt;
  
  
  왜 재봤나
&lt;/h2&gt;

&lt;p&gt;AllenAI 블로그가 AstaBrief-8B를 소개하며 "기존보다 3.5배 빨라져서 리포트 하나에 50초면 된다"고 썼다(&lt;a href="https://www.threads.com/@dogfootbro.ai/post/DeBqvojEw2m" rel="noopener noreferrer"&gt;Threads 소개 글&lt;/a&gt;). 같은 하드웨어·같은 분량 기준 비교였는지 &lt;a href="https://www.threads.com/@voidlight00/post/DeDuvAXj_Sx" rel="noopener noreferrer"&gt;의문&lt;/a&gt;이 제기됐는데, 실제로 블로그 원문엔 테스트 조건(하드웨어, 쿼리 수, 비교 대상)이 전혀 안 적혀 있었다. 무료 Colab T4로 직접 재봤다.&lt;/p&gt;

&lt;h2&gt;
  
  
  처음엔 틀렸다
&lt;/h2&gt;

&lt;p&gt;첫 시도는 그냥 AstaBrief랑 Claude를 같은 프롬프트로 돌려서 시간 재는 거였다. 근데 두 가지가 비교를 왜곡했다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;비교 대상이 잘못됐었다&lt;/strong&gt;: AstaBrief는 사실 Qwen3-8B를 파인튜닝한 모델이다(블로그에 "We started from Qwen3-8B"라고 적혀있다). 파인튜닝 자체의 속도 영향을 보려면 베이스 모델이랑 비교해야 의미가 있다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;출력 길이를 안 고정했었다&lt;/strong&gt;: AstaBrief는 1024토큰 상한까지 꽉 채워 답했고 Qwen은 367토큰에서 자연 종료됐다. 그 상태로 wall-clock을 그대로 비교하니 "AstaBrief가 4.29배 느리다"는 결론이 나왔는데, 이것도 틀린 비교였다 — 더 긴 글을 썼으니 더 오래 걸린 것뿐이다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  다시 맞춰서 잰 결과
&lt;/h2&gt;

&lt;p&gt;출력 토큰을 200으로 고정하고(&lt;code&gt;min_new_tokens=max_new_tokens=200&lt;/code&gt;), 같은 T4에서 3회씩 돌려 평균 냈다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AstaBrief-8B: 19.62초 (10.20 tok/s)&lt;/li&gt;
&lt;li&gt;Qwen3-8B(파인튜닝 전 베이스): 19.64초 (10.19 tok/s)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;배율: 1.00배&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;파인튜닝은 속도에 아무 영향이 없었다. 그럼 블로그의 "3.5배"는 어디서 나온 숫자였나 — AstaBrief 단일 모델 호출(Fast mode)과, Claude 기반 멀티스텝 에이전트 파이프라인(Thinking mode: 검색+클러스터링+섹션별 생성 등 여러 단계)을 비교한 수치였다. 느린 건 그 파이프라인 구조 때문이지, AstaBrief가 일반 8B 모델보다 빠른 게 아니다. 원문에도 이 부분은 적혀 있다 — "대부분의 학습·평가는 2025년에 끝났고, 최신 프론티어 모델로 재평가하진 않았다"고.&lt;/p&gt;

&lt;p&gt;그럼 파인튜닝은 뭘 위한 거였나 싶어서 찾아보니, ScholarQA-CS2 기준 인용 정확도가 베이스 모델 대비 올라가 있었다(90.5 vs 76.2). 아마 이게 진짜 목적이었을 거다 — 속도가 아니라 품질. &lt;a href="https://www.threads.com/@dogfootbro.ai/post/DeE8rUdEwrG" rel="noopener noreferrer"&gt;Threads에도 이 검증 결과를 공유&lt;/a&gt;했다.&lt;/p&gt;

&lt;h2&gt;
  
  
  한계
&lt;/h2&gt;

&lt;p&gt;GPU 한 종류(T4), 양자화 한 방식(4bit), 쿼리 하나로만 잰 결과다. 다른 환경에서는 숫자가 달라질 수 있다. "3.5배는 과장이다"라고 단정하긴 어렵고, "3.5배가 가리키는 게 모델 자체의 속도가 아니라 파이프라인 구조 차이"라는 정도까지만 말할 수 있다.&lt;/p&gt;




&lt;p&gt;원문: &lt;a href="https://allenai.org/blog/astabrief" rel="noopener noreferrer"&gt;AstaBrief-8B — AllenAI Blog&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>benchmark</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>"이제 완전히 고쳐졌나?"에 정직한 답은 없다 — 작은 서버 이야기</title>
      <dc:creator>JustJinoIT</dc:creator>
      <pubDate>Sun, 21 Jun 2026 02:43:30 +0000</pubDate>
      <link>https://dev.to/justjinoit/ije-wanjeonhi-gocyeojyeossnae-jeongjighan-dabeun-eobsda-jageun-seobeo-iyagi-4lih</link>
      <guid>https://dev.to/justjinoit/ije-wanjeonhi-gocyeojyeossnae-jeongjighan-dabeun-eobsda-jageun-seobeo-iyagi-4lih</guid>
      <description>&lt;p&gt;1GB 프리티어 VPS 하나로 사이드 프로젝트를 돌린다. 작은 서버, 서비스 몇 개.&lt;/p&gt;

&lt;p&gt;지난주에 인프라를 손봤다. 고칠 때마다 같은 걸 물었다. 이제 완전히 고쳐졌나? 다시 안 터지나? 정직한 답은 계속 "아니"였다. 심지어 고친 것 때문에 다음 문제가 터지기도 했다.&lt;/p&gt;

&lt;p&gt;그 질문을 그만 묻게 만든 사건을 적어둔다.&lt;/p&gt;

&lt;h2&gt;
  
  
  사건: 모니터링이 멀쩡한 서버를 죽일 뻔했다
&lt;/h2&gt;

&lt;p&gt;구성은 이랬다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;본체: 1GB VPS, API 하나&lt;/li&gt;
&lt;li&gt;watchdog: 다른 서버에서 5분마다 &lt;code&gt;/health&lt;/code&gt; 체크. 죽으면 알림, 계속 죽어있으면 클라우드 API로 강제 재부팅.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;안전하다고 느꼈다. 자동 복구까지 되니까.&lt;/p&gt;

&lt;p&gt;그러다 본체에서 &lt;code&gt;ffmpeg&lt;/code&gt; 작업을 돌렸다. 1코어라 CPU를 통째로 먹었고, 그 사이 &lt;code&gt;/health&lt;/code&gt;가 10초 안에 응답을 못 했다. watchdog은 "응답 없음 = 죽음"으로 읽었다. 한 번만 더 실패했으면 멀쩡한 프로덕션 서버를 재부팅할 차례였다.&lt;/p&gt;

&lt;p&gt;서버는 안 죽었다. 내가 돌린 작업이 느리게 만들었을 뿐인데, 지키려고 만든 게 죽일 뻔했다.&lt;/p&gt;

&lt;h2&gt;
  
  
  "완전히 고쳐졌나"에 정직한 yes가 없는 이유
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. "고쳐졌나"는 닫힌 질문, 시스템은 열려있다.&lt;/strong&gt;&lt;br&gt;
막을 수 있는 건 이미 본 실패뿐이다. 변경할 때마다 새 표면적이 생긴다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. 작은 서버는 결합도가 높다.&lt;/strong&gt;&lt;br&gt;
1코어 1기가. 무거운 작업 하나가 나머지를 굶긴다. 격리가 안 되니 "이건 안전"이 옆을 건드린다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. 고치는 행위도 변수다.&lt;/strong&gt;&lt;br&gt;
모니터링도 시스템의 일부고, 이번엔 그게 장애원이었다. 같은 세션에서 알림 스크립트에 실행권한을 준 게 가짜 알림의 원인이 되기도 했다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. 자신만만한 답은 싸다.&lt;/strong&gt;&lt;br&gt;
"됐어, 안전해"는 파급을 확인하기 전에 말하기 쉽다. 도구든, AI든, 나든. 내가 계속 "고쳐졌나"를 물은 건 그 yes를 못 믿어서였고, 안 믿길 잘했다.&lt;/p&gt;
&lt;h2&gt;
  
  
  진짜 도움이 된 것
&lt;/h2&gt;

&lt;p&gt;보장이 아니라 감지와 되돌리기.&lt;/p&gt;

&lt;p&gt;watchdog 수정은 "완벽하게"가 아니었다. 한 번 실패에 단정하지 않기. 몇 번 더 보고 다 실패하면 그때 죽음으로 친다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"000"&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in &lt;/span&gt;1 2 3&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 10 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 3 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;10
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="c"&gt;# 세 번 다 실패해야 진짜 죽음&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;순간 스파이크는 30초 안에 회복되니까, 세 번 다 실패면 진짜 죽은 거다. 무거운 작업은 굶길 서버에서 안 돌린다.&lt;/p&gt;

&lt;h2&gt;
  
  
  결론
&lt;/h2&gt;

&lt;p&gt;"완전히 고쳐졌나"는 어떤 정직한 시스템도 yes라고 못 하는 질문이다. 쓸모 있는 버전은 더 좁다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;어떤 알려진 실패를 막았고, 어떻게 검증했나?&lt;/li&gt;
&lt;li&gt;못 막는 건 뭐고, 어떻게 감지하고 되돌릴 건가?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;뭔가 "네, 완전히 처리됐어요"라고 답하면, 보통 그때가 더 들여다볼 때다.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>sre</category>
      <category>lessons</category>
    </item>
    <item>
      <title>Why "is it fully fixed?" has no honest answer — a small-server story</title>
      <dc:creator>JustJinoIT</dc:creator>
      <pubDate>Sun, 21 Jun 2026 02:24:14 +0000</pubDate>
      <link>https://dev.to/justjinoit/why-is-it-fully-fixed-has-no-honest-answer-a-small-server-story-5g2</link>
      <guid>https://dev.to/justjinoit/why-is-it-fully-fixed-has-no-honest-answer-a-small-server-story-5g2</guid>
      <description>&lt;p&gt;I run a side project on a 1GB free-tier VPS. Small box, a few services, nothing fancy.&lt;/p&gt;

&lt;p&gt;Last week I spent a session hardening its infra. After each fix I asked the same thing: is it fully fixed now? Will this stop happening? The honest answer kept coming back no, and sometimes the fix itself caused the next problem.&lt;/p&gt;

&lt;p&gt;Here is the incident that made me retire that question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The incident: my monitoring almost killed a healthy server
&lt;/h2&gt;

&lt;p&gt;The setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Main box: 1GB VPS, one API service.&lt;/li&gt;
&lt;li&gt;Watchdog: a second box pings &lt;code&gt;/health&lt;/code&gt; every 5 minutes. If it looks down, it alerts. If it stays down, it calls the cloud API to hard-reboot the main box.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Felt safe. Self-healing, even.&lt;/p&gt;

&lt;p&gt;Then I ran an &lt;code&gt;ffmpeg&lt;/code&gt; job on the main box. One core, so ffmpeg took the whole CPU. During that window, &lt;code&gt;/health&lt;/code&gt; could not answer within the 10s timeout. The watchdog read "no response" as "dead." One more failed check and it would have rebooted a perfectly healthy production box.&lt;/p&gt;

&lt;p&gt;The server never went down. A job I started made it slow, and the thing I built to protect it almost rebooted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "is it fully fixed?" has no honest yes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. "Fixed" is a closed question; the system is open.&lt;/strong&gt;&lt;br&gt;
You can only defend against failures you have already seen. Every change adds new surface. "Will this never happen again" has no honest yes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Small servers are tightly coupled.&lt;/strong&gt;&lt;br&gt;
One core, one gig. A heavy job starves everything else. No isolation, so "this part is safe" can quietly break the part next to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The fix is a variable too.&lt;/strong&gt;&lt;br&gt;
Monitoring is part of the system. This time it was the failure. Earlier in the same session, making an alert script executable turned it into a source of false alarms. Every change made to harden things added a new way to fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Confident answers are cheap.&lt;/strong&gt;&lt;br&gt;
"Done, safe" is easy to say before checking the blast radius, whether it comes from a tool, an AI agent, or me. The reason I kept asking "is it fixed?" was that part of me did not believe the yes. I was right not to.&lt;/p&gt;
&lt;h2&gt;
  
  
  What actually helps
&lt;/h2&gt;

&lt;p&gt;Not guarantees. Detection and reversibility.&lt;/p&gt;

&lt;p&gt;The watchdog fix was not "make it perfect." It was: stop trusting a single failed check. Retry a few times before declaring death.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"000"&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in &lt;/span&gt;1 2 3&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 10 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 3 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;10
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="c"&gt;# only "dead" if all three fail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A transient spike recovers within 30 seconds, so three misses in a row means it is really down. And heavy jobs do not run on the box they would starve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;"Is it fully fixed?" is a question no honest system can answer yes to. The useful version is narrower:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which known failures did you close, and how did you verify them?&lt;/li&gt;
&lt;li&gt;What can you not prevent, and how will you detect and roll it back?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When something answers "yes, it is fully handled," that is usually the moment to look closer.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>sre</category>
      <category>lessons</category>
    </item>
    <item>
      <title>같은 게시물을 세 번 올리고 나서야 이유를 알았다</title>
      <dc:creator>JustJinoIT</dc:creator>
      <pubDate>Fri, 19 Jun 2026 09:49:20 +0000</pubDate>
      <link>https://dev.to/justjinoit/instagram-graph-apineun-geojismaleul-handa-jadonghwa-paipeuraineseo-haru-nalrin-iyagi-1ojd</link>
      <guid>https://dev.to/justjinoit/instagram-graph-apineun-geojismaleul-handa-jadonghwa-paipeuraineseo-haru-nalrin-iyagi-1ojd</guid>
      <description>&lt;h1&gt;
  
  
  같은 게시물을 세 번 올리고 나서야 이유를 알았다
&lt;/h1&gt;

&lt;p&gt;자동화 봇을 테스트하면서 limit이 자꾸 걸렸다. AI한테 물어봤더니 이런 말이 돌아왔다.&lt;/p&gt;

&lt;p&gt;"앱이 개발 중 상태라 제약이 있어요. 24-48시간 기다리면 풀릴 거예요."&lt;/p&gt;

&lt;p&gt;그럴싸했다. 믿었다.&lt;/p&gt;




&lt;h2&gt;
  
  
  Meta 콘솔 토끼굴
&lt;/h2&gt;

&lt;p&gt;기다렸다. 또 안 됐다.&lt;/p&gt;

&lt;p&gt;그럼 앱 게시 문제인가 싶었다. Meta 개발자 콘솔 들어갔다. AI가 알려준 버튼이 없었다. 메뉴가 달랐다. 다른 경로로 들어갔다. 권한 항목 찾아서 추가했다. 앱 검수 신청 화면 들어갔다. 뭔가 테스트를 완료해야 한다고 해서 실행해봤다. 다시 권한 뺐다가 넣었다. 토큰 재발급 받았다. 또 테스트.&lt;/p&gt;

&lt;p&gt;인증 화면까지 들어갔더니 사업자 등록 정보를 넣으라고 했다. 거기서 멈췄다. 이건 아니다 싶었다.&lt;/p&gt;

&lt;p&gt;Graph API Explorer 열어서 직접 뒤적이기 시작했다. 뭔가 나오긴 했는데 뭘 보고 있는 건지도 불분명했다.&lt;/p&gt;




&lt;h2&gt;
  
  
  "중복 자료가 왜 이렇게 많냐"
&lt;/h2&gt;

&lt;p&gt;그러다 인스타그램을 직접 열어봤다. 같은 게시물이 세 개였다. 피드 전체 보니까 12개. 여러 기사에서 중복이 각각 쌓여있었다.&lt;/p&gt;

&lt;p&gt;코드 흐름을 다시 따라갔다. 403 받으면 실패로 처리하고, DB 업데이트 안 하고, 다음 스케줄에 재시도. 그 재시도도 403. 또 재시도. 근데 인스타에는 매번 올라갔다. 몇 사이클 돌고 나니 이렇게 됐다.&lt;/p&gt;

&lt;p&gt;받은 에러:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application request limit reached"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OAuthException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error_subcode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2207051&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"is_transient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rate limit이겠지 했다. 확인해봤다:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GRAPH_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/content_publishing_limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config,quota_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;quota_usage: 0&lt;/code&gt;. 한도엔 전혀 안 걸려있었다. &lt;code&gt;x-app-usage&lt;/code&gt; 헤더도 0%. rate limit이 아니었다.&lt;/p&gt;

&lt;p&gt;행동 차단인가. 근데 행동 차단이 됐으면 어딘가 뜰 거 아냐? 앱, Business Suite, 개발자 콘솔. 아무것도 없었다. 경고도, 배너도, 표시 하나도.&lt;/p&gt;

&lt;p&gt;행동 차단은 어디에도 안 뜬다. 에러 응답의 &lt;code&gt;is_transient: false&lt;/code&gt;가 유일한 신호다. rate limit은 보통 &lt;code&gt;is_transient: true&lt;/code&gt;에 24시간 후 풀린다. &lt;code&gt;false&lt;/code&gt;면 기다린다고 해결 안 된다. 이게 구분 기준이었다.&lt;/p&gt;

&lt;p&gt;원인은 테스트하면서 1시간 안에 게시물 13개를 올린 거였다. 일일 한도 25개엔 한참 못 미치는데. 단시간에 몰아 올리면 자동화 패턴으로 감지돼서 별도 제한이 걸린다. 문서엔 없다.&lt;/p&gt;

&lt;p&gt;이 차단 상태에서 &lt;code&gt;media_publish&lt;/code&gt;는 403을 뱉는데 게시물은 올라간다. 코드가 403을 실패로만 처리했으니 재시도했고, 재시도마다 중복이 쌓였다.&lt;/p&gt;

&lt;p&gt;이렇게 고쳤다:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_publish_container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;container_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GRAPH_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;IG_USER_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/media_publish&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;creation_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;container_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IG_TOKEN&lt;/span&gt;&lt;span class="p"&gt;},&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_success&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;resp&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GRAPH_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;IG_USER_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/media&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id,timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IG_TOKEN&lt;/span&gt;&lt;span class="p"&gt;},&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;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;check&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&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;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&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;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;403 받으면 바로 최근 미디어 조회. 60초 이내에 올라온 게 있으면 성공으로 친다.&lt;/p&gt;




&lt;h2&gt;
  
  
  "추가했는데 해볼래?" 그리고 또 실패
&lt;/h2&gt;

&lt;p&gt;중복 12개 지워야 했다. 삭제엔 &lt;code&gt;instagram_manage_contents&lt;/code&gt; 권한이 필요한데 내 토큰엔 없었다.&lt;/p&gt;

&lt;p&gt;권한 추가. 확인. 삭제 시도.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(#10) Insufficient permissions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;실패. 다시. 같은 에러. 콘솔엔 분명히 있는데.&lt;/p&gt;

&lt;p&gt;OAuth 토큰은 발급 시점의 scope를 고정으로 가진다. 앱에 권한 추가해도 기존 토큰은 그대로다. 새로 발급받아야 한다.&lt;/p&gt;

&lt;p&gt;토큰이 실제로 뭘 갖고 있는지 보려면:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://graph.facebook.com/v22.0/me/permissions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;granted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permission&lt;/span&gt;&lt;span class="sh"&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;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&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;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;granted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;instagram_manage_contents&lt;/code&gt;가 없으면 Graph API Explorer에서 scope 체크하고 재발급. 그 다음엔 됐다.&lt;/p&gt;




&lt;h2&gt;
  
  
  진짜 원인 조합
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;신호&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;quota_usage: 0&lt;/code&gt;인데 403&lt;/td&gt;
&lt;td&gt;행동 차단 (rate limit 아님)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_transient: false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기다려도 안 풀림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;403인데 인스타에 게시됨&lt;/td&gt;
&lt;td&gt;차단 상태에서도 게시는 통과함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;행동 차단이 UI에 표시됨&lt;/td&gt;
&lt;td&gt;안 됨, 어디에도&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;API 동작은 문서에 없고, AI는 자신 있게 틀린 경로를 알려줬고, 그 경로의 콘솔 UI는 또 바뀌어있었다. 어디서부터 잘못됐는지 찾는 게 제일 어려웠다.&lt;/p&gt;

&lt;p&gt;AI가 알려주는 콘솔 메뉴는 실제랑 다를 수 있다. API가 이상하다 싶으면 UI 말고 응답 필드 먼저 봐라. &lt;code&gt;is_transient&lt;/code&gt; 하나가 대시보드보다 정직했다.&lt;/p&gt;

&lt;p&gt;오후 하나 날렸다. 중복 12개, Meta 토끼굴, 권한 삽질. 그나마 건진 건 이 글이다.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Instagram Graph API v22.0. 카드뉴스 자동 발행 봇 만들면서 겪은 내용. 봇은 &lt;a href="https://www.instagram.com/dogfootbro.ai" rel="noopener noreferrer"&gt;@dogfootbro.ai&lt;/a&gt;에 올라간다.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>instagram</category>
      <category>python</category>
      <category>automation</category>
      <category>api</category>
    </item>
    <item>
      <title>I published the same post three times before I understood what was happening</title>
      <dc:creator>JustJinoIT</dc:creator>
      <pubDate>Fri, 19 Jun 2026 09:49:04 +0000</pubDate>
      <link>https://dev.to/justjinoit/instagram-graph-api-lies-to-you-3-things-that-broke-my-automation-12n2</link>
      <guid>https://dev.to/justjinoit/instagram-graph-api-lies-to-you-3-things-that-broke-my-automation-12n2</guid>
      <description>&lt;h1&gt;
  
  
  I published the same post three times before I understood what was happening
&lt;/h1&gt;

&lt;p&gt;I was testing an automation bot and kept hitting limits. Asked an AI assistant what was going on.&lt;/p&gt;

&lt;p&gt;"The app is in development mode, so there are restrictions. Wait 24-48 hours and it should clear up."&lt;/p&gt;

&lt;p&gt;Sounded reasonable. I believed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Meta console rabbit hole
&lt;/h2&gt;

&lt;p&gt;Waited. Still broken.&lt;/p&gt;

&lt;p&gt;Maybe it's an app publishing issue, I thought. Went into the Meta developer console. The button the AI told me about wasn't there. The menu was different. Found another path. Located the permissions section and added what I needed. Got into the app review screen. It said I had to complete some tests first, so I ran them. Removed permissions, added them back. Got a new token. Tested again.&lt;/p&gt;

&lt;p&gt;Eventually got into the verification flow and it asked for business registration info. That's where I stopped. This isn't right.&lt;/p&gt;

&lt;p&gt;Opened Graph API Explorer and started poking around directly. Something was coming back but I wasn't sure what I was looking at.&lt;/p&gt;




&lt;h2&gt;
  
  
  "Why are there so many duplicates?"
&lt;/h2&gt;

&lt;p&gt;At some point I just opened Instagram directly. Same post, three times. Looked at the whole feed: 12 duplicates across multiple articles, each one stacked up from previous runs.&lt;/p&gt;

&lt;p&gt;Traced through the code. On 403, treat as failure, skip DB update, retry next cycle. That retry also 403'd. Retried again. But Instagram got the post every time. A few cycles in and this is what happened.&lt;/p&gt;

&lt;p&gt;The error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application request limit reached"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OAuthException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error_subcode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2207051&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"is_transient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rate limit, obviously. Checked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GRAPH_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/content_publishing_limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config,quota_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;quota_usage: 0&lt;/code&gt;. Not even close to the cap. &lt;code&gt;x-app-usage&lt;/code&gt; header was 0% too. Not a rate limit.&lt;/p&gt;

&lt;p&gt;Maybe a behavior block then. But if you're behavior-blocked, wouldn't something show up somewhere? Checked the app, Business Suite, developer console. Nothing. No warning, no banner, no indicator.&lt;/p&gt;

&lt;p&gt;Behavior blocks don't show up anywhere. The only signal is &lt;code&gt;is_transient: false&lt;/code&gt; in the error response. Rate limits are usually &lt;code&gt;is_transient: true&lt;/code&gt; and reset after 24 hours. &lt;code&gt;false&lt;/code&gt; means waiting won't help. That's the only way to tell them apart.&lt;/p&gt;

&lt;p&gt;The cause: 13 posts within one hour during testing. Well under the documented daily limit of 25, but posting that fast triggers a separate undocumented restriction. Instagram sees the pattern as automated. Nothing in the docs about this.&lt;/p&gt;

&lt;p&gt;In this blocked state, &lt;code&gt;media_publish&lt;/code&gt; returns 403 but the post goes through. Code treated 403 as failure, retried, and each retry stacked another duplicate.&lt;/p&gt;

&lt;p&gt;Fixed it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_publish_container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;container_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GRAPH_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;IG_USER_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/media_publish&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;creation_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;container_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IG_TOKEN&lt;/span&gt;&lt;span class="p"&gt;},&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_success&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;resp&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GRAPH_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;IG_USER_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/media&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id,timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IG_TOKEN&lt;/span&gt;&lt;span class="p"&gt;},&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;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;check&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&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;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&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;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a 403, immediately check recent media. If something posted within 60 seconds, call it a success.&lt;/p&gt;




&lt;h2&gt;
  
  
  "I added the permission. Try it now." Still failing.
&lt;/h2&gt;

&lt;p&gt;Had to delete the 12 duplicates. Deletion requires &lt;code&gt;instagram_manage_contents&lt;/code&gt;. My token only had &lt;code&gt;instagram_content_publish&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Added the permission. Confirmed it in the console. Tried the delete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(#10) Insufficient permissions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failed. Tried again. Same error. The permission was right there in the console.&lt;/p&gt;

&lt;p&gt;OAuth tokens carry a fixed scope from when they were issued. Adding a permission to your app doesn't update existing tokens. Had to generate a new one.&lt;/p&gt;

&lt;p&gt;To see what a token actually has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://graph.facebook.com/v22.0/me/permissions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;granted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permission&lt;/span&gt;&lt;span class="sh"&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;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&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;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;granted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;instagram_manage_contents&lt;/code&gt; isn't there, go to Graph API Explorer, check the scope, regenerate. After that it worked.&lt;/p&gt;




&lt;h2&gt;
  
  
  What was actually going on
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;quota_usage: 0&lt;/code&gt; but still 403&lt;/td&gt;
&lt;td&gt;Behavior block, not rate limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_transient: false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Waiting won't fix it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;403 on publish but post appears&lt;/td&gt;
&lt;td&gt;Block lets posts through anyway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behavior block visible in any UI&lt;/td&gt;
&lt;td&gt;No, nowhere&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The API behavior wasn't documented. The AI confidently pointed me in the wrong direction. The console UI it described had changed. Finding where things actually went wrong was the hardest part.&lt;/p&gt;

&lt;p&gt;AI-suggested console paths may not match what's actually there. If the API is acting strange, check the response fields before the UI. &lt;code&gt;is_transient&lt;/code&gt; was more honest than any dashboard.&lt;/p&gt;

&lt;p&gt;Lost an afternoon. 12 duplicates, one Meta rabbit hole, one permission dead end. This post is what I got out of it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Instagram Graph API v22.0. Built while automating a card news account. Bot posts to &lt;a href="https://www.instagram.com/dogfootbro.ai" rel="noopener noreferrer"&gt;@dogfootbro.ai&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>instagram</category>
      <category>python</category>
      <category>automation</category>
      <category>api</category>
    </item>
    <item>
      <title>I Audited My Own Side Projects for Security Issues — Here's What I Found</title>
      <dc:creator>JustJinoIT</dc:creator>
      <pubDate>Tue, 16 Jun 2026 11:24:39 +0000</pubDate>
      <link>https://dev.to/justjinoit/i-audited-my-own-side-projects-for-security-issues-heres-what-i-found-1ahb</link>
      <guid>https://dev.to/justjinoit/i-audited-my-own-side-projects-for-security-issues-heres-what-i-found-1ahb</guid>
      <description>&lt;p&gt;I recently did a full security audit across all my side projects — FastAPI backends, Telegram bots, a PWA, and a Streamlit app. I wasn't expecting much. I thought I'd been careful.&lt;/p&gt;

&lt;p&gt;I was wrong.&lt;/p&gt;

&lt;p&gt;Here's every issue I found, why I made each mistake, and how to fix them. This isn't a theoretical checklist — these are bugs I actually shipped to production.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Authentication Bypass via Empty Secret (Critical)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What I wrote
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_API_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;API_SECRET_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_api_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;''&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;_API_SECRET&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;x_api_key&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;_API_SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# ← the bug
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the condition: &lt;code&gt;if _API_SECRET and ...&lt;/code&gt;. If &lt;code&gt;API_SECRET_KEY&lt;/code&gt; isn't set in the environment, &lt;code&gt;_API_SECRET&lt;/code&gt; is an empty string — falsy — so the entire check is &lt;strong&gt;skipped&lt;/strong&gt;. Every request passes as authenticated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I wrote it this way
&lt;/h3&gt;

&lt;p&gt;I wanted graceful fallback during local development so I wouldn't need to set env vars every time. The problem: that same "graceful fallback" made it to production, and when I forgot to set &lt;code&gt;API_SECRET_KEY&lt;/code&gt; on the server, the entire API was open.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_API_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;API_SECRET_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_api_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;_API_SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;API_SECRET_KEY not configured&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_API_SECRET&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Unauthorized&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Missing secret = &lt;strong&gt;500 error&lt;/strong&gt;, not open access. Also switched to &lt;code&gt;secrets.compare_digest()&lt;/code&gt; to prevent timing attacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: Never make authentication conditional on whether the secret is configured. Missing config should be a hard failure.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Secrets Committed to Git History (Critical)
&lt;/h2&gt;

&lt;p&gt;I found real API keys in git history — not in the current code, but in commits from months ago when I was "just testing."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# How to check&lt;/span&gt;
git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"sk-ant-api03-[A-Za-z0-9_-]{20,}"&lt;/span&gt;
git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"AIzaSy[A-Za-z0-9]{20,}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this happens
&lt;/h3&gt;

&lt;p&gt;Early in a project, you hardcode a key to test quickly, commit it, then move it to &lt;code&gt;.env&lt;/code&gt; in a later commit thinking the problem is solved. But git keeps every commit forever. Anyone with repo access (or if you ever make the repo public) can find these old keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove a file from entire history&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;git-filter-repo
git-filter-repo &lt;span class="nt"&gt;--path&lt;/span&gt; .env &lt;span class="nt"&gt;--invert-paths&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt;
git push &lt;span class="nt"&gt;--force-with-lease&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also: immediately &lt;strong&gt;revoke and reissue&lt;/strong&gt; any exposed key. Cleaning git history doesn't undo any exposure that already happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: Treat any key committed to git as compromised, even if you "fixed it" in a later commit.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Debug Endpoints in Production (High)
&lt;/h2&gt;

&lt;p&gt;I had endpoints like this deployed to my production server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/debug/config&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;debug_config&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;supabase_url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supabase_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;environment&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;connected_services&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/debug/db-test&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;debug_db&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# runs a live query and returns raw results
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this happens
&lt;/h3&gt;

&lt;p&gt;Debug endpoints are lifesavers during development. You add them when you're stuck, solve the problem, and forget to remove them. They don't cause errors, so nothing reminds you they exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Delete them. If you need runtime introspection, put it behind authentication or use a proper admin interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check for common debug patterns before every deploy&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'@app.get.*debug\|@app.post.*debug\|@app.get.*admin/exec'&lt;/span&gt; app/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: Add debug endpoint removal to your deployment checklist. Or better — never create them in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Error Messages Leaking Internal Details (High)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What I had
&lt;/span&gt;&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FATAL: password authentication failed for user "postgres"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[Errno 2] No such file or directory: '/home/ubuntu/app/config.json'&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Module 'xyz' version 1.2.3 has no attribute 'connect'&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An attacker can use this to understand your infrastructure, figure out which libraries you're using, and target known CVEs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I wrote it this way
&lt;/h3&gt;

&lt;p&gt;Same reason as #1 — debug convenience. &lt;code&gt;str(e)&lt;/code&gt; gives you instant visibility when testing. The mistake was not adding a layer between internal errors and HTTP responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Scout error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# full details in logs
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;internal server error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log everything internally, return nothing externally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: Your logs are for you. HTTP error responses are for the client. Keep these completely separate.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. XSS via Unescaped innerHTML (High)
&lt;/h2&gt;

&lt;p&gt;In my frontend, I was doing:&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="c1"&gt;// Rendering data from an API&lt;/span&gt;
&lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`
        &amp;lt;h3&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/h3&amp;gt;
        &amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/p&amp;gt;
        &amp;lt;a href="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;Read more&amp;lt;/a&amp;gt;
    `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a malicious article gets into the database with a title like &lt;code&gt;&amp;lt;script&amp;gt;stealCookies()&amp;lt;/script&amp;gt;&lt;/code&gt;, it executes in every user's browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this happens
&lt;/h3&gt;

&lt;p&gt;Template literals feel like string formatting, not HTML rendering. When you write &lt;code&gt;${article.title}&lt;/code&gt;, it doesn't feel dangerous — it's just a variable. But the browser sees HTML and executes whatever it finds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;esc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;||&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;amp;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;amp;&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;lt;&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;gt;&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/"/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;quot;&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;safeUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/^https&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;||&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;u&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Now safe&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;h3&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/p&amp;gt;
    &amp;lt;a href="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;safeUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;" rel="noopener noreferrer"&amp;gt;Read more&amp;lt;/a&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or better — use &lt;code&gt;textContent&lt;/code&gt; and &lt;code&gt;createElement&lt;/code&gt; instead of &lt;code&gt;innerHTML&lt;/code&gt; where possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: Every time you write &lt;code&gt;innerHTML&lt;/code&gt;, mentally replace it with "I am executing arbitrary code." Then decide if you've sanitized properly.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Missing Rate Limits on AI-Powered Endpoints (High)
&lt;/h2&gt;

&lt;p&gt;I had endpoints that call Claude/Gemini directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/analyze&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verify_api_key&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;  &lt;span class="c1"&gt;# $0.015 per call
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No rate limiting. An attacker with a stolen API key (or even someone who finds the endpoint through enumeration) could make thousands of calls and rack up a serious bill.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slowapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Limiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_rate_limit_exceeded_handler&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slowapi.util&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_remote_address&lt;/span&gt;

&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Limiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_remote_address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;limiter&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/analyze&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10/minute&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verify_api_key&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: Authentication prevents unauthorized access. Rate limiting prevents authorized-but-abusive access. You need both.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. CORS Wildcard in Production (Medium)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CORSMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;allow_origins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# ← allows any website
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this is risky even with authentication
&lt;/h3&gt;

&lt;p&gt;If your API key is stored in frontend JavaScript (which it often is for SPAs), a malicious website could use XSS on &lt;em&gt;any&lt;/em&gt; site to steal the key from the user's browser and call your API. CORS is your browser-level firewall.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;ALLOWED_ORIGINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ALLOWED_ORIGINS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CORSMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;allow_origins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ALLOWED_ORIGINS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;allow_methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;allow_headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env (production)&lt;/span&gt;
&lt;span class="nv"&gt;ALLOWED_ORIGINS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://yourapp.vercel.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: &lt;code&gt;allow_origins=["*"]&lt;/code&gt; is fine for local dev. Never ship it.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Temporary Files Not Cleaned Up (Medium)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Original code
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.xlsx&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uploaded_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;

&lt;span class="nf"&gt;process_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# if this throws, tmp file leaks forever
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;process_file()&lt;/code&gt; raises an exception, the temp file is never deleted. On a long-running server, these accumulate. Worse — if the files contain sensitive user data, they sit on disk indefinitely.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.xlsx&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uploaded_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;process_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;finally&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;tmp_path&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: Any code path that creates a file must also be responsible for deleting it. &lt;code&gt;finally&lt;/code&gt; guarantees cleanup even on exception.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Checklist I Now Follow
&lt;/h2&gt;

&lt;p&gt;After this audit, I created a mandatory checklist that applies to every project from day one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before writing any code:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;.gitignore&lt;/code&gt; created with &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;*.key&lt;/code&gt;, &lt;code&gt;sessions/&lt;/code&gt;, &lt;code&gt;credentials.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;.env.example&lt;/code&gt; created (no real values)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For every endpoint:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Authentication added (never conditional on secret being present)&lt;/li&gt;
&lt;li&gt;[ ] Error responses return generic messages, not &lt;code&gt;str(e)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Rate limits on AI-powered or expensive operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For every frontend:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] All &lt;code&gt;innerHTML&lt;/code&gt; usage uses escaped values&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;href&lt;/code&gt; values go through URL validation&lt;/li&gt;
&lt;li&gt;[ ] External links have &lt;code&gt;rel="noopener noreferrer"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Before every commit:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"sk-ant-api03|AIzaSy|password&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*=&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*[^&lt;/span&gt;&lt;span class="se"&gt;\$\{&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;
git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;env"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Before every deploy:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip-audit &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
npm audit  &lt;span class="c"&gt;# if applicable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Did I Miss All This?
&lt;/h2&gt;

&lt;p&gt;Honestly: &lt;strong&gt;I treated security as a separate phase rather than a built-in constraint.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pattern was always the same:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get the feature working quickly&lt;/li&gt;
&lt;li&gt;Add a TODO comment: "clean this up later"&lt;/li&gt;
&lt;li&gt;Later never comes&lt;/li&gt;
&lt;li&gt;Deploy with the "temporary" solution&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Security issues aren't added intentionally — they're the residue of "I'll fix it when I have time." The only way I've found to actually prevent them is to make security checks happen at natural checkpoints: before commit, before deploy, and at project start.&lt;/p&gt;

&lt;p&gt;The bugs are boring. The cost of fixing them after the fact is not.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Running FastAPI or similar backends? I'd recommend checking your own projects with these patterns. The auth bypass one (&lt;code&gt;if _SECRET and key != _SECRET&lt;/code&gt;) is surprisingly common.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Removing API Keys from Git History: BFG + Force Push (A Security Incident Response)</title>
      <dc:creator>JustJinoIT</dc:creator>
      <pubDate>Sun, 07 Jun 2026 23:53:20 +0000</pubDate>
      <link>https://dev.to/justjinoit/removing-api-keys-from-git-history-bfg-force-push-a-security-incident-response-46d</link>
      <guid>https://dev.to/justjinoit/removing-api-keys-from-git-history-bfg-force-push-a-security-incident-response-46d</guid>
      <description>&lt;h1&gt;
  
  
  Removing API Keys from Git History: BFG + Force Push (A Security Incident Response)
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Published on&lt;/strong&gt;: 2026-06-06&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Reading time&lt;/strong&gt;: 5 min&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Tags&lt;/strong&gt;: #security #git #devops #secrets&lt;/p&gt;
&lt;h2&gt;
  
  
  The Situation
&lt;/h2&gt;

&lt;p&gt;API keys were committed to &lt;code&gt;.env&lt;/code&gt; file in Git history. Anyone with repo access could do &lt;code&gt;git log&lt;/code&gt; and see them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--full-history&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt;
&lt;span class="c"&gt;# abc1234 Remove .env from version control&lt;/span&gt;
&lt;span class="c"&gt;# def5678 Initial commit: Contest Agent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Immediate risk&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clone repo → check git history → steal API keys&lt;/li&gt;
&lt;li&gt;If pushed to GitHub, keys are potentially searchable/cached&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution: BFG Repo-Cleaner
&lt;/h2&gt;

&lt;p&gt;Better than &lt;code&gt;git filter-branch&lt;/code&gt; (faster, safer):&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install BFG
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;bfg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Delete from History
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
bfg &lt;span class="nt"&gt;--delete-files&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt; &lt;span class="nt"&gt;--no-blob-protection&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deleted files:
  Deleted 4 commits
  Deleted 12 blob(s)
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Clean Up
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git reflog expire &lt;span class="nt"&gt;--expire&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;now &lt;span class="nt"&gt;--all&lt;/span&gt;
git gc &lt;span class="nt"&gt;--prune&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;now &lt;span class="nt"&gt;--aggressive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Verify
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--full-history&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt;
&lt;span class="c"&gt;# (no output = success)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Force Push
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push origin main &lt;span class="nt"&gt;--force-with-lease&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Before &amp;amp; After
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before BFG&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;span class="go"&gt;abc1234 Remove .env from version control
def5678 Enable SSL certificate verification
ghi9012 Standardize dependencies
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After BFG&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;span class="go"&gt;xyz3456 Remove duplicate service definition
uvw7890 Standardize dependencies
rst1234 Enable SSL certificate verification
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unnecessary commits removed, history cleaned.&lt;/p&gt;

&lt;h2&gt;
  
  
  BFG vs git filter-branch
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;git filter-branch&lt;/th&gt;
&lt;th&gt;BFG&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Slow (1000+ commits)&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safety&lt;/td&gt;
&lt;td&gt;Complex, error-prone&lt;/td&gt;
&lt;td&gt;Simple, clear feedback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usability&lt;/td&gt;
&lt;td&gt;Requires scripting&lt;/td&gt;
&lt;td&gt;One-liner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recommendation&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  CRITICAL: Rotate API Keys
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cleaning git history is step 1.&lt;/strong&gt; Step 2 is mandatory:&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeline: Do This Immediately
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Within 1 hour&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Anthropic&lt;/span&gt;
&lt;span class="c"&gt;# → https://console.anthropic.com/account/keys&lt;/span&gt;
&lt;span class="c"&gt;# → Delete old key, generate new one&lt;/span&gt;

&lt;span class="c"&gt;# Supabase&lt;/span&gt;
&lt;span class="c"&gt;# → https://app.supabase.com → Settings → API&lt;/span&gt;
&lt;span class="c"&gt;# → Click "Reset secret key"&lt;/span&gt;

&lt;span class="c"&gt;# Telegram&lt;/span&gt;
&lt;span class="c"&gt;# → Open @BotFather&lt;/span&gt;
&lt;span class="c"&gt;# → /token → select bot → /revoke&lt;/span&gt;
&lt;span class="c"&gt;# → Generate new token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Within 24 hours&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update all deployed servers with new keys&lt;/li&gt;
&lt;li&gt;Test that services still work&lt;/li&gt;
&lt;li&gt;Monitor logs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best Practices Going Forward
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Initialize Correctly from Day One
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# First thing in new project:&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore
git add .gitignore
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add .env to .gitignore"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Create &lt;code&gt;.env.example&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env.example
&lt;/span&gt;&lt;span class="py"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-key-here&lt;/span&gt;
&lt;span class="py"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-url-here&lt;/span&gt;
&lt;span class="py"&gt;TELEGRAM_BOT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-token-here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Use GitHub Actions Secrets
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy.yml&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
  &lt;span class="na"&gt;SUPABASE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SUPABASE_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Add Pre-Commit Hook (Optional)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# .git/hooks/pre-commit&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'\.env'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"❌ Error: Attempting to commit .env file"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real Impact (After Remediation)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before:
- .env in git history
- 79 commits total
- History size: 50MB

After BFG:
- .env removed from all commits
- 75 commits total (4 cleaned)
- History size: 42MB (-16%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Execution Checklist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: Immediate (Next 1 hour)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Run BFG: &lt;code&gt;bfg --delete-files ".env" .&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Clean git: &lt;code&gt;git reflog expire ... &amp;amp;&amp;amp; git gc ...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Force push: &lt;code&gt;git push origin main --force-with-lease&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: Rotate (Next 24 hours)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Anthropic API key&lt;/li&gt;
&lt;li&gt;[ ] Supabase Secret Key&lt;/li&gt;
&lt;li&gt;[ ] Telegram Bot token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: Prevention&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Update deployed servers with new keys&lt;/li&gt;
&lt;li&gt;[ ] Create &lt;code&gt;.env.example&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Update &lt;code&gt;.gitignore&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Add GitHub Actions secrets&lt;/li&gt;
&lt;li&gt;[ ] Notify team (force push happened)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 4: Future&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Set up pre-commit hooks&lt;/li&gt;
&lt;li&gt;[ ] Monthly secret audit&lt;/li&gt;
&lt;li&gt;[ ] Use Secret scanning tools (GitHub, GitLab)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;API key exposure is a security incident, not a "mistake."&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Act fast (BFG)&lt;/li&gt;
&lt;li&gt;Rotate keys (mandatory)&lt;/li&gt;
&lt;li&gt;Document the response&lt;/li&gt;
&lt;li&gt;Prevent recurrence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your security posture will be better for it—and you'll have a good story for the next security audit.&lt;/p&gt;

</description>
      <category>git</category>
      <category>security</category>
      <category>tooling</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Multi-Cloud Deployment in Production: Cloud Run, Railway, Oracle Cloud</title>
      <dc:creator>JustJinoIT</dc:creator>
      <pubDate>Sun, 07 Jun 2026 23:39:30 +0000</pubDate>
      <link>https://dev.to/justjinoit/multi-cloud-deployment-in-production-cloud-run-railway-oracle-cloud-3-month-report-2pdp</link>
      <guid>https://dev.to/justjinoit/multi-cloud-deployment-in-production-cloud-run-railway-oracle-cloud-3-month-report-2pdp</guid>
      <description>&lt;h1&gt;
  
  
  Multi-Cloud Deployment in Production: Cloud Run, Railway, Oracle Cloud
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Published on&lt;/strong&gt;: 2026-06-06&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Reading time&lt;/strong&gt;: 10 min&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Tags&lt;/strong&gt;: #devops #cloud #fastapi #production&lt;/p&gt;
&lt;h2&gt;
  
  
  Situation
&lt;/h2&gt;

&lt;p&gt;I deployed 3 FastAPI projects to 3 different clouds. Here's &lt;strong&gt;what actually happened&lt;/strong&gt; (not marketing speak):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contest-agent      → Google Cloud Run
ai-insight-curator → Railway
ai-lifelogger      → Oracle Cloud Always Free
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Google Cloud Run: 20+ Deployments Before Discovering the Real Problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue: "Container Failed to Start"
&lt;/h3&gt;

&lt;p&gt;Deployed 20+ times, same error every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build: SUCCESS ✅
Push: SUCCESS ✅
Start: TIMEOUT ❌
Port 8080 binding: TIMEOUT ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Root cause&lt;/strong&gt;: FastAPI startup was blocking port binding with I/O operations&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ❌ Problem code (startup blocks port binding)
&lt;/span&gt;&lt;span class="nd"&gt;@asynccontextmanager&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;telegram_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Starting...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# I/O blocking
&lt;/span&gt;    &lt;span class="n"&gt;db_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;              &lt;span class="c1"&gt;# I/O blocking  
&lt;/span&gt;    &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                                   &lt;span class="c1"&gt;# Heavy init
&lt;/span&gt;    &lt;span class="k"&gt;yield&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloud Run waits for port binding to complete before health checks. Startup blocking = timeout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution: Lazy Loading
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ✅ Fixed code (startup returns immediately)
&lt;/span&gt;&lt;span class="n"&gt;_initialized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lazy_init&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_initialized&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_initialized&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;_initialized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;telegram_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Started&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/webhook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;lazy_init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Init on first actual request
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Startup 100ms (was 60s+ timeout), port binding immediate, health check passes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Lesson: Start Minimal
&lt;/h3&gt;

&lt;p&gt;Don't deploy a complex system all at once. Lessons learned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Phase 1: Just "/" endpoint
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# → Deploy, test, pass ✅
&lt;/span&gt;
&lt;span class="c1"&gt;# Phase 2: Add health check
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;healthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# → Deploy, test, pass ✅
&lt;/span&gt;
&lt;span class="c1"&gt;# Phase 3-N: Gradually add features
# Each phase = one deployment test
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Railway: The "Simple" Illusion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Advantages
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Git push → auto-deploy (very fast)&lt;/li&gt;
&lt;li&gt;PostgreSQL, Redis built-in&lt;/li&gt;
&lt;li&gt;Intuitive dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reality Check
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cost surprises&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected: $10/month
Actual: $25/month (250% overage)

Reason:
- 1 vCPU + 512MB RAM always running
- No cold start = memory always consumed
- Bandwidth costs added up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Memory leak detection is hard&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hour 1: 150MB ✅
Hour 2: 180MB
Hour 3: 220MB
Hour 4: 260MB (OOM incoming)

Cause: RSS feed crawler not releasing memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Auto-deploy is a double-edged sword&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Con: Changes go live without testing&lt;/li&gt;
&lt;li&gt;Con: Need fast rollback procedure&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How I Actually Operate It
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before pushing to main:&lt;/span&gt;
pytest              &lt;span class="c"&gt;# Run tests&lt;/span&gt;
pylint             &lt;span class="c"&gt;# Lint check&lt;/span&gt;
docker build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker run  &lt;span class="c"&gt;# Local test&lt;/span&gt;

&lt;span class="c"&gt;# Only push after passing:&lt;/span&gt;
git push origin main  &lt;span class="c"&gt;# Auto-deploys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Oracle Cloud Always Free: Free but Demanding
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Advantages
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Completely free (4 CPU, 24GB RAM, 200GB storage)&lt;/li&gt;
&lt;li&gt;No limits&lt;/li&gt;
&lt;li&gt;Full SSH control&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Real Problems
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem #1: 1GB instance, pip install fails&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MemoryError during pip install

Reason: 1GB RAM instance can't handle 
all packages at once
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add swap&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;fallocate &lt;span class="nt"&gt;-l&lt;/span&gt; 8G /swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;mkswap /swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;swapon /swapfile

&lt;span class="c"&gt;# Or: Install only essentials&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; anthropic supabase python-telegram-bot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem #2: Docker vs Local Mismatch&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Local: anthropic==0.40.0 (already installed)
Docker: Fresh install reads requirements.txt
  - anthropic==0.40.0
  - langchain-anthropic needs anthropic&amp;gt;=0.41.0
  → pip can't resolve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Remove version pins, let pip resolve&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DON'T: anthropic==0.40.0, supabase==2.0.0, ...
DO: anthropic, supabase (let pip figure it out)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem #3: SSH Deployment Needs Automation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Manual (every time):&lt;/span&gt;
ssh oracle@your-ip
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/ai-lifelogger
git pull &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; systemctl restart

&lt;span class="c"&gt;# Better (automated via GitHub Actions):&lt;/span&gt;
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt; oracle@&lt;span class="nv"&gt;$ip&lt;/span&gt; &lt;span class="s2"&gt;"cd /opt &amp;amp;&amp;amp; git pull &amp;amp;&amp;amp; systemctl restart"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance Comparison (3-Month Data)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Cloud Run&lt;/th&gt;
&lt;th&gt;Railway&lt;/th&gt;
&lt;th&gt;Oracle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deploy time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2-3 min&lt;/td&gt;
&lt;td&gt;30 sec&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cold start&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3-5 sec&lt;/td&gt;
&lt;td&gt;0 sec&lt;/td&gt;
&lt;td&gt;&amp;lt;1 sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monthly cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;td&gt;$25&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU limit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2 cores&lt;/td&gt;
&lt;td&gt;1 core&lt;/td&gt;
&lt;td&gt;4 cores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAM limit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2GB&lt;/td&gt;
&lt;td&gt;512MB&lt;/td&gt;
&lt;td&gt;24GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Solid&lt;/td&gt;
&lt;td&gt;⚠️ Memory issues&lt;/td&gt;
&lt;td&gt;✅ Solid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Practical Advice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Start Minimal, Add Gradually
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Deploy "/" endpoint first&lt;/li&gt;
&lt;li&gt;Test, pass, add next feature&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Always Test Locally
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; myapp &lt;span class="nb"&gt;.&lt;/span&gt;
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Choose Based on Use Case
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High traffic&lt;/strong&gt;: Cloud Run (autoscales)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium traffic&lt;/strong&gt;: Railway (simple)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low traffic&lt;/strong&gt;: Oracle (free)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Monitoring is Non-Negotiable
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cloud Run: GCP Logs + Cloud Monitoring
Railway: Built-in dashboard (limited)
Oracle: SSH → journalctl + tail -f
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;There's no "perfect" platform.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloud Run: startup timeout (solvable with lazy loading)&lt;/li&gt;
&lt;li&gt;Railway: memory leaks (code issue, not platform)&lt;/li&gt;
&lt;li&gt;Oracle: operational overhead (worth it for free tier)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The real skill&lt;/strong&gt;: Understanding each platform's constraints and designing around them.&lt;/p&gt;

&lt;p&gt;The 20+ Cloud Run deployment failures? They taught me more than 10 successful deployments would have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Deployment Architecture (June 7, 2026)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Production Status
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🦅 Oracle Cloud (Always Free Tier)
├─ ai-lifelogger (port 8000)
│  ├─ FastAPI + APScheduler
│  ├─ Daily summaries: 05:00 KST
│  ├─ Weekly reviews: Sunday 08:00 KST
│  └─ Memory: 111MB / 954MB
│
└─ ai-insight-curator (port 8001)
   ├─ FastAPI + Telegram Bot
   ├─ RSS collection: Daily 06:00 KST
   ├─ Auto-summarization (Claude/Gemini/Groq fallback)
   └─ Memory: 22MB / 954MB

🌐 Vercel (Free Hosting)
└─ Curator Web Dashboard
   ├─ React + Vite frontend
   ├─ Article search &amp;amp; filtering
   ├─ Image downloads
   └─ https://curator-web-ui.vercel.app

📊 Total Memory: 537MB / 954MB (56% usage, 44% available)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Changed
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Initial Plan&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contest-agent → Cloud Run ❌ (dependency conflicts)
ai-insight-curator → Railway ❌ (over-engineered)
ai-lifelogger → Oracle Cloud ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Actual Production&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ai-lifelogger → Oracle Cloud ✅ (running)
ai-insight-curator → Oracle Cloud ✅ (1 instance = better)
Curator Web UI → Vercel ✅ (new, auto-deployed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Insight&lt;/strong&gt;: Single server + Web UI &amp;gt; Multi-cloud complexity&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Metrics
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Response Times:
- Lifelogger /health: &amp;lt; 50ms ✅
- Curator /api/v1/articles: &amp;lt; 100ms ✅
- Curator /api/v1/insights: &amp;lt; 100ms ✅

System Health:
- Memory: 537MB (56%) - 417MB free for scaling
- Availability: 99.9%
- Uptime: Continuous (Always Free tier)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cost Analysis (Final)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Oracle Cloud&lt;/td&gt;
&lt;td&gt;$0/month&lt;/td&gt;
&lt;td&gt;✅ Always Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;$0/month&lt;/td&gt;
&lt;td&gt;✅ Free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase DB&lt;/td&gt;
&lt;td&gt;$0/month&lt;/td&gt;
&lt;td&gt;✅ Free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude API&lt;/td&gt;
&lt;td&gt;Needs reset*&lt;/td&gt;
&lt;td&gt;⚠️ Using Gemini/Groq backup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TOTAL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0/month&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Forever Free&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*Anthropic tokens exhausted → fallback to Gemini/Groq working&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Multi-cloud Isn't Always Better
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Cloud Run: Good for high-traffic APIs&lt;/li&gt;
&lt;li&gt;Railway: Convenient but expensive&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Oracle: Best for low-traffic, cost-sensitive projects&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Single Server Wins Here
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;2 concurrent FastAPI services&lt;/li&gt;
&lt;li&gt;Database included (PostgreSQL via Supabase)&lt;/li&gt;
&lt;li&gt;Web dashboard on separate CDN (Vercel)&lt;/li&gt;
&lt;li&gt;Total cost: &lt;strong&gt;$0&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Design Around Constraints
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Memory: 954MB available → deployed with 537MB usage&lt;/li&gt;
&lt;li&gt;Can still run 300MB+ additional services&lt;/li&gt;
&lt;li&gt;Monitoring via SSH (not ideal, but works)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Don't chase multi-cloud complexity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The optimal deployment turned out to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 Oracle Cloud instance (FastAPI services)&lt;/li&gt;
&lt;li&gt;1 CDN (Vercel for web)&lt;/li&gt;
&lt;li&gt;1 Database (Supabase)&lt;/li&gt;
&lt;li&gt;Everything free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt;: $0/month ✅&lt;br&gt;
&lt;strong&gt;Reliability&lt;/strong&gt;: 99.9% ✅&lt;br&gt;
&lt;strong&gt;Maintainability&lt;/strong&gt;: Simple ✅&lt;/p&gt;

&lt;p&gt;Sometimes simpler is better.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cloud</category>
      <category>fastapi</category>
      <category>production</category>
    </item>
  </channel>
</rss>
