<?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>Forem: David McHale</title>
    <description>The latest articles on Forem by David McHale (@david_dev_sec).</description>
    <link>https://forem.com/david_dev_sec</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3713414%2F51539356-4221-49f2-919b-8af5d175b255.png</url>
      <title>Forem: David McHale</title>
      <link>https://forem.com/david_dev_sec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/david_dev_sec"/>
    <language>en</language>
    <item>
      <title>That Time SQLite File Existence Lied to Us</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Wed, 11 Feb 2026 16:57:00 +0000</pubDate>
      <link>https://forem.com/david_dev_sec/that-time-sqlite-file-existence-lied-to-us-4em3</link>
      <guid>https://forem.com/david_dev_sec/that-time-sqlite-file-existence-lied-to-us-4em3</guid>
      <description>&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;Our bootstrap script was killing GoPhish before database migrations could finish. Took us way too long to figure out why.&lt;/p&gt;

&lt;p&gt;Here's what we had:&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;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nv"&gt;$GOPHISH_PID&lt;/span&gt;  &lt;span class="c"&gt;# DB exists, we're good right?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nope.&lt;/p&gt;

&lt;p&gt;SQLite creates the database file the moment you open a connection. Migrations run &lt;em&gt;after&lt;/em&gt; that. We were killing the process as soon as &lt;code&gt;gophish.db&lt;/code&gt; appeared, before the schema was even built.&lt;/p&gt;

&lt;p&gt;Result: weird SQL errors about missing columns. Only in production. Fun times.&lt;/p&gt;

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

&lt;p&gt;Wait for the schema, not just the file:&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;# Wait for file&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Wait for migrations (check for a column we know should exist)&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; sqlite3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;".schema users"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"password_change_required"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pre-flight checks
&lt;/h2&gt;

&lt;p&gt;While we were in there, we added checks before starting the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;preflight_check&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;missing&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;

    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MIGRATIONS_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; missing+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"migrations directory"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFIG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; missing+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"config.json"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATIC_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; missing+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"static directory"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&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: Missing required files: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;[*]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="nb"&gt;exit &lt;/span&gt;1
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Catches broken deployments immediately instead of failing mysteriously later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging output that actually helps
&lt;/h2&gt;

&lt;p&gt;When stuff breaks, we now dump:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Directory contents&lt;/li&gt;
&lt;li&gt;Database schema (if it exists)&lt;/li&gt;
&lt;li&gt;Common causes checklist&lt;/li&gt;
&lt;li&gt;All output unbuffered with &lt;code&gt;stdbuf -oL&lt;/code&gt; so you can actually see it in real time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters a lot when you're debugging through a cloud serial console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud VM patterns we use now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Network waiting.&lt;/strong&gt; Cloud VMs don't always have network at boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wait_for_network&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;max_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; i&amp;lt;&lt;span class="o"&gt;=&lt;/span&gt;max_attempts&lt;span class="p"&gt;;&lt;/span&gt; i++&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
        if &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 5 https://example.com &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
            return &lt;/span&gt;0
        &lt;span class="k"&gt;fi
        &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;2
    &lt;span class="k"&gt;done
    return &lt;/span&gt;1
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Docker readiness.&lt;/strong&gt; Service "started" doesn't mean Docker is actually ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wait_for_docker&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; docker info &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
        &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
    &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;State files.&lt;/strong&gt; Know if this is first boot or a restart:&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;STATE_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/myapp/state"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATE_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;do_routine_restart
&lt;span class="k"&gt;else
    &lt;/span&gt;do_initial_setup
    &lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATE_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Systemd stuff
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;network-online.target&lt;/code&gt;, not &lt;code&gt;network.target&lt;/code&gt;. They're different and it matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;
&lt;span class="py"&gt;PrivateTmp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target docker.service&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  SSH key cleanup for marketplace images
&lt;/h2&gt;

&lt;p&gt;If you're publishing VM images, clean up your dev keys:&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="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_&lt;span class="k"&gt;*&lt;/span&gt; ~/.ssh/&lt;span class="k"&gt;*&lt;/span&gt;.pub ~/.ssh/known_hosts ~/.ssh/config
find ~/.ssh &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;

&lt;span class="c"&gt;# Verify it worked&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;find ~/.ssh &lt;span class="nt"&gt;-type&lt;/span&gt; f 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&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;"WARNING: SSH files still present!"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Don't trust file existence as a completion signal&lt;/li&gt;
&lt;li&gt;Pre-flight checks save debugging time&lt;/li&gt;
&lt;li&gt;Cloud VMs need patience (network, Docker, everything)&lt;/li&gt;
&lt;li&gt;Unbuffer your output (&lt;code&gt;stdbuf -oL&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Verify your cleanup actually worked&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These changes took us from "why does this randomly break" to "oh, it failed because X." That's the goal.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>sqlite</category>
      <category>debugging</category>
      <category>linux</category>
    </item>
    <item>
      <title>We Shipped 79 PRs in a Few Weeks. Claude Code Did Most of the Work.</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Wed, 28 Jan 2026 15:01:00 +0000</pubDate>
      <link>https://forem.com/david_dev_sec/we-shipped-79-prs-in-a-few-weeks-claude-code-did-most-of-the-work-35j0</link>
      <guid>https://forem.com/david_dev_sec/we-shipped-79-prs-in-a-few-weeks-claude-code-did-most-of-the-work-35j0</guid>
      <description>&lt;p&gt;We maintain &lt;a href="https://github.com/HailBytes/gophish" rel="noopener noreferrer"&gt;HailBytes GoPhish&lt;/a&gt;, a fork of the open-source phishing simulation toolkit. We wanted to add a bunch of enterprise features (MFA, SSO, encryption at rest, audit logging, white-labeling) and honestly didn't have the bandwidth to do it the traditional way.&lt;/p&gt;

&lt;p&gt;So we tried something different. Claude Code is now our most prolific contributor.&lt;/p&gt;

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

&lt;p&gt;GoPhish is kind of a pain to work on. Go backend, JavaScript frontend, systemd services, bash deployment scripts, SQL migrations, webpack. When you touch one thing, you often need to touch five others.&lt;/p&gt;

&lt;p&gt;We had a feature list that would take a small team months. We have... not that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;Here's a real morning:&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;$ &lt;/span&gt;claude
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Fix the bootstrap script killing gophish before migrations &lt;span class="nb"&gt;complete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude reads the bash scripts, traces the systemd dependency chain, finds the race condition, fixes it, commits. Done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;commit cd29bc7
Author: Claude &amp;lt;noreply@anthropic.com&amp;gt;

    Fix bootstrap killing gophish before migrations complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Real Examples
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Service debugging:&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;&amp;gt; Fix Linux services not starting after VM image restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude traced through the systemd units, found the missing dependencies, fixed the boot sequence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend bugs:&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;&amp;gt; Fix privacy settings not saving due to deprecated jQuery methods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our jQuery upgrade broke &lt;code&gt;.attr()&lt;/code&gt; on checkboxes (should be &lt;code&gt;.prop()&lt;/code&gt;). Claude found all the occurrences and fixed them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Big refactors:&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;&amp;gt; Update bootstrap script with patterns from cloud_tools and scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;666-line diff. Network waiting, readiness checks, state file tracking, proper error handling. One commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Describe the problem, not the solution.&lt;/strong&gt; "Fix the bug where X happens" beats "change line 47 to Y"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let it explore.&lt;/strong&gt; Claude reads related files and often finds issues you didn't know about&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the diff.&lt;/strong&gt; It commits with clear messages. You review, test, merge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You still own the final call.&lt;/strong&gt; We run tests and do manual QA. Fast doesn't mean careless&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Surprised Us
&lt;/h2&gt;

&lt;p&gt;The bash scripts. We expected Claude to be good at Go and JavaScript. We didn't expect it to nail systemd services and deployment scripts. It gets the whole stack.&lt;/p&gt;

&lt;p&gt;Cross-cutting changes too. Adding SSO touched Go handlers, SQL migrations, JavaScript, CSS, docs. Claude handled the full vertical.&lt;/p&gt;

&lt;p&gt;And bug hunting. "The sidebar is pushing down the main content" and Claude reads the CSS, finds the &lt;code&gt;position: fixed&lt;/code&gt; conflict, fixes it, explains why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;From our recent git log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15 Claude-authored commits in the last two weeks&lt;/li&gt;
&lt;li&gt;Changes across Go, JavaScript, Bash, SQL, CSS, systemd&lt;/li&gt;
&lt;li&gt;Features enterprise customers are paying for&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you maintain an open-source project and your issue backlog haunts you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @anthropic-ai/claude-code
&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with a real bug. Something annoying that's been sitting there forever. See what happens.&lt;/p&gt;




&lt;p&gt;We're &lt;a href="https://hailbytes.com" rel="noopener noreferrer"&gt;HailBytes&lt;/a&gt;. We build security tools for pentesters and security awareness teams. GoPhish 0.14.2 ships this month.&lt;/p&gt;




&lt;p&gt;What's your experience using AI on real projects? I'm curious what's working for people.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>We Hardened Ubuntu 24.04 for Security Tools (And Broke Everything First)</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Wed, 21 Jan 2026 16:50:00 +0000</pubDate>
      <link>https://forem.com/david_dev_sec/we-hardened-ubuntu-2404-for-security-tools-and-broke-everything-first-kf2</link>
      <guid>https://forem.com/david_dev_sec/we-hardened-ubuntu-2404-for-security-tools-and-broke-everything-first-kf2</guid>
      <description>&lt;h2&gt;
  
  
  We Hardened Ubuntu 24.04 for Security Tools (And Broke Everything First)
&lt;/h2&gt;

&lt;p&gt;We maintain &lt;a href="https://github.com/HailBytes/rengine-ng-rc" rel="noopener noreferrer"&gt;HailBytes reNgine&lt;/a&gt;, a web recon platform. Wanted to deploy hardened golden images to AWS and Azure following industry benchmarks.&lt;/p&gt;

&lt;p&gt;Should be simple. Run hardening script, create AMI, done.&lt;/p&gt;

&lt;p&gt;Except our scanning tools immediately stopped working. 🔥&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;# Our "secure" kernel settings:&lt;/span&gt;
kernel.unprivileged_bpf_disabled &lt;span class="o"&gt;=&lt;/span&gt; 1  &lt;span class="c"&gt;# Block BPF&lt;/span&gt;
net.core.bpf_jit_harden &lt;span class="o"&gt;=&lt;/span&gt; 2           &lt;span class="c"&gt;# Maximum BPF hardening&lt;/span&gt;

&lt;span class="c"&gt;# What our tools actually needed:&lt;/span&gt;
&lt;span class="c"&gt;# BPF access. For packet capture. For scanning. For everything.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the thing: &lt;strong&gt;security tools often need the exact privileges that security hardening removes.&lt;/strong&gt; Fun paradox.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Claude Code Helped
&lt;/h2&gt;

&lt;p&gt;We used &lt;a href="https://docs.anthropic.com/en/docs/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; to iterate on the hardening scripts. Three problems it helped us solve:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Azure VMs Were Detected as AWS
&lt;/h3&gt;

&lt;p&gt;Both clouds use the same metadata IP. Our detection was just checking if the endpoint responded.&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;# This is wrong&lt;/span&gt;
&lt;span class="nv"&gt;METADATA_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://169.254.169.254"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$METADATA_URL&lt;/span&gt;&lt;span class="s2"&gt;/latest/meta-data/"&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;"AWS!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix: actually validate the response format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;detect_cloud_provider&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;# Check Azure FIRST (it has a specific field)&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;azure_check&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;-H&lt;/span&gt; &lt;span class="s2"&gt;"Metadata:true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"http://169.254.169.254/metadata/instance?api-version=2021-02-01"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--connect-timeout&lt;/span&gt; 2 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$azure_check&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'"azEnvironment"'&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;"azure"&lt;/span&gt;
        &lt;span class="k"&gt;return
    fi&lt;/span&gt;

    &lt;span class="c"&gt;# Then check AWS (validate instance ID format)&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;aws_check&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="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"http://169.254.169.254/latest/meta-data/instance-id"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--connect-timeout&lt;/span&gt; 2 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$aws_check&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^i-[a-f0-9]+ &lt;span class="o"&gt;]]&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;"aws"&lt;/span&gt;
        &lt;span class="k"&gt;return
    fi

    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"generic"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Azure returns JSON with &lt;code&gt;azEnvironment&lt;/code&gt;. AWS returns plain text. Check the actual content, not just connectivity.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Ubuntu 24.04 Renamed the SSH Service
&lt;/h3&gt;

&lt;p&gt;Worked fine on 22.04. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl restart sshd  &lt;span class="c"&gt;# "Unit sshd.service not found" 💀&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ubuntu 24.04 changed it from &lt;code&gt;sshd&lt;/code&gt; to &lt;code&gt;ssh&lt;/code&gt;. Cool. Cool cool cool.&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;if &lt;/span&gt;systemctl restart ssh 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; systemctl restart sshd 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;log &lt;span class="s2"&gt;"SSH hardened"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;error &lt;span class="s2"&gt;"Failed to restart SSH"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Finding the Right Security Tradeoffs
&lt;/h3&gt;

&lt;p&gt;This was the real work. Hardening that doesn't break the app:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;CIS Says&lt;/th&gt;
&lt;th&gt;What We Used&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unprivileged_bpf_disabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 (blocked)&lt;/td&gt;
&lt;td&gt;0 (allowed)&lt;/td&gt;
&lt;td&gt;Scanning needs packet capture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bpf_jit_harden&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 (max)&lt;/td&gt;
&lt;td&gt;1 (moderate)&lt;/td&gt;
&lt;td&gt;Performance matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AppArmor&lt;/td&gt;
&lt;td&gt;enforce all&lt;/td&gt;
&lt;td&gt;complain for Docker&lt;/td&gt;
&lt;td&gt;Containers need flexibility&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each deviation is documented. Auditors will ask. Have your answers ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Auto-detect cloud provider&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./harden_ubuntu_2404.sh

&lt;span class="c"&gt;# Or force it&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./harden_ubuntu_2404.sh &lt;span class="nt"&gt;--cloud&lt;/span&gt; azure
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./harden_ubuntu_2404.sh &lt;span class="nt"&gt;--cloud&lt;/span&gt; aws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH hardening (Ed25519, modern ciphers only)&lt;/li&gt;
&lt;li&gt;UFW firewall (ports 22, 443, 8082)&lt;/li&gt;
&lt;li&gt;Kernel hardening (ASLR, SYN cookies, anti-spoofing)&lt;/li&gt;
&lt;li&gt;Fail2Ban&lt;/li&gt;
&lt;li&gt;Auditd logging&lt;/li&gt;
&lt;li&gt;Auto security updates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Still runs Docker and security tools&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test on the actual target platform.&lt;/strong&gt; Ubuntu version differences will get you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud metadata APIs are tricky.&lt;/strong&gt; Validate the response format, not just reachability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document your security tradeoffs.&lt;/strong&gt; Future you and your auditors will need this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI assistants catch edge cases.&lt;/strong&gt; Claude flagged the SSH rename before we hit production.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Grab It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/HailBytes/rengine-ng-rc
&lt;span class="nb"&gt;cd &lt;/span&gt;rengine-ng-rc/scripts
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./harden_ubuntu_2404.sh &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works for any Ubuntu 24.04 server, not just reNgine.&lt;/p&gt;

&lt;p&gt;How do you handle the security vs. functionality tradeoff? Would love to hear what's worked for you. Update incoming this week.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;#security #devops #ubuntu #cloudcomputing #opensource&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>ubuntu</category>
      <category>ai</category>
    </item>
    <item>
      <title>Our Godot Game Only Crashed on Expensive PCs (Here's Why)</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Fri, 16 Jan 2026 21:02:35 +0000</pubDate>
      <link>https://forem.com/david_dev_sec/our-godot-game-only-crashed-on-expensive-pcs-heres-why-40jl</link>
      <guid>https://forem.com/david_dev_sec/our-godot-game-only-crashed-on-expensive-pcs-heres-why-40jl</guid>
      <description>&lt;p&gt;Players started reporting that our game was freezing during boss fights. No crash logs. No Sentry reports. Just "Not Responding" and then... nothing.&lt;/p&gt;

&lt;p&gt;The weird part? It was happening on RTX 4090s while working fine on older hardware. Yeah.&lt;/p&gt;

&lt;p&gt;My brother and I run Lost Rabbit Digital together, and we've been building Starbrew Station (a space coffee shop idle game, it's a whole thing) in Godot 4.5. Here's what we found and how we fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Problems
&lt;/h2&gt;

&lt;p&gt;We tracked it down to four separate issues that were all making things worse together.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Loading Resources During Gameplay
&lt;/h3&gt;

&lt;p&gt;This looks harmless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;start_boss_fight&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;boss_scene&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"res://scenes/InspectionBossFight.tscn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;boss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boss_scene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instantiate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not. That &lt;code&gt;load()&lt;/code&gt; call freezes everything for 100-500ms while it reads from disk. Combine that with shader compilation and you hit Windows TDR (Timeout Detection and Recovery). Windows kills any process that doesn't respond to the GPU driver within ~2 seconds. No crash report, just dead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix: Preload at startup instead.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;InspectionBossFightScene&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"res://scenes/InspectionBossFight.tscn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;start_boss_fight&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;boss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InspectionBossFightScene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instantiate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# instant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same thing for shader materials. Don't create them at runtime, create templates at startup and duplicate them.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Awaits That Wait Forever
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_on_achievement_unlocked&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;game_saved&lt;/span&gt;
    &lt;span class="n"&gt;show_notification&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the save fails and never emits that signal? This waits forever. The game just hangs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix: Fire and forget.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_on_achievement_unlocked&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_save&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;show_notification&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# don't wait for confirmation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Tweens Piling Up
&lt;/h3&gt;

&lt;p&gt;Every UI animation created a new tween. We never killed the old ones. After a few hours of gameplay, thousands of dead tween references just sitting in memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix: Track them and kill the old one before making a new one.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;_active_tweens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Dictionary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;fade_ambient_light&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;_active_tweens&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ambient_fade"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;_active_tweens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ambient_fade"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_valid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;_active_tweens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ambient_fade"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tween&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_tween&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;_active_tweens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ambient_fade"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tween&lt;/span&gt;
    &lt;span class="n"&gt;tween&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tween_property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;light&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"energy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Loops With No Safety Limits
&lt;/h3&gt;

&lt;p&gt;Our fighter cleanup loop could churn through thousands of invalid entries in one frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;cleanup_fighters&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fighters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&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="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="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="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;is_instance_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fighters&lt;/span&gt;&lt;span class="p"&gt;[&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;fighters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&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;Fix: Add iteration limits.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MAX_ITERATIONS_PER_FRAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;cleanup_fighters&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fighters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&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="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="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="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;iterations&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="n"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_ITERATIONS_PER_FRAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&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;is_instance_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fighters&lt;/span&gt;&lt;span class="p"&gt;[&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;fighters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For bigger operations (like applying buffs to 100+ units at once), chunk it across frames:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;apply_cascade_inspiration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# 10 per frame&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply_inspiration&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;get_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;process_frame&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Preload resources at startup.&lt;/strong&gt; Runtime &lt;code&gt;load()&lt;/code&gt; on Windows can trigger GPU driver timeouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't await signals that might never fire.&lt;/strong&gt; Fire and forget instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track your tweens.&lt;/strong&gt; Kill old ones before creating new ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put limits on loops.&lt;/strong&gt; Especially ones processing dynamic arrays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunk big operations across frames.&lt;/strong&gt; Your players will thank you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The kicker: high-end PCs hit these bugs &lt;em&gt;faster&lt;/em&gt; because they ran more game loops per second. Sometimes the best hardware finds the worst problems.&lt;/p&gt;




&lt;p&gt;We're Lost Rabbit Digital on &lt;a href="https://github.com/Lost-Rabbit-Digital" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Starbrew Station is built with Godot 4.5.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>performance</category>
      <category>programming</category>
    </item>
    <item>
      <title>Run Phishing Simulations for $37/Month Instead of $30,000/Year</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Thu, 15 Jan 2026 23:39:34 +0000</pubDate>
      <link>https://forem.com/david_dev_sec/run-phishing-simulations-for-37month-instead-of-30000year-nig</link>
      <guid>https://forem.com/david_dev_sec/run-phishing-simulations-for-37month-instead-of-30000year-nig</guid>
      <description>&lt;p&gt;Most enterprise phishing simulation tools charge $3-5 per user per year. For a 10,000 person company, that's $30,000-50,000 annually.&lt;/p&gt;

&lt;p&gt;We run unlimited simulations on a $37/month Azure VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tool: GoPhish
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/gophish/gophish" rel="noopener noreferrer"&gt;GoPhish&lt;/a&gt; is an open-source phishing simulation framework. It's been around for 10+ years, has 10,000+ installations, and is MIT licensed. &lt;/p&gt;

&lt;p&gt;I've supported it for the last 8 years or so, since early 2018, maintaining the core repo and answering issues as they've popped up.&lt;/p&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create realistic phishing campaigns&lt;/li&gt;
&lt;li&gt;Track who opens, clicks, and submits credentials&lt;/li&gt;
&lt;li&gt;Measure improvement over time&lt;/li&gt;
&lt;li&gt;Import thousands of targets via CSV&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem? Vanilla GoPhish lacks enterprise basics: no MFA, no encryption at rest, no audit logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Added
&lt;/h2&gt;

&lt;p&gt;We forked GoPhish and added what production environments actually need:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MFA/TOTP&lt;/td&gt;
&lt;td&gt;Your admin panel shouldn't be a security hole&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSO (Google/Microsoft)&lt;/td&gt;
&lt;td&gt;One-click login for your team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-256 encryption&lt;/td&gt;
&lt;td&gt;Stored credentials aren't plaintext anymore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit logging&lt;/td&gt;
&lt;td&gt;SIEM export for compliance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White-label branding&lt;/td&gt;
&lt;td&gt;Your logo, not ours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-click deployment&lt;/td&gt;
&lt;td&gt;Azure/AWS in ~5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Quick Start (Azure)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create Ubuntu 24.04 VM from GoPhish 0.14.2 public image on Azure (Standard_B2s = $37/month)&lt;/li&gt;
&lt;li&gt;Get your auto-generated admin password from Azure Serial Console&lt;/li&gt;
&lt;li&gt;Login at &lt;a href="https://your-ip:3333" rel="noopener noreferrer"&gt;https://your-ip:3333&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The setup script handles systemd services, TLS certificates, and Ubuntu hardening. &lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;th&gt;10,000 Users/Year&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;KnowBe4&lt;/td&gt;
&lt;td&gt;~$30,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proofpoint&lt;/td&gt;
&lt;td&gt;~$40,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud-hosted GoPhish&lt;/td&gt;
&lt;td&gt;~$3,600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted GoPhish&lt;/td&gt;
&lt;td&gt;~$360&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same capabilities. Fraction of the cost. Your data stays on your infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/HailBytes/gophish" rel="noopener noreferrer"&gt;github.com/HailBytes/gophish&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Marketplace:&lt;/strong&gt; Search "GoPhish" or "HailBytes"&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Questions? Cheers, drop them in the comments.&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>azure</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
