<?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: christine</title>
    <description>The latest articles on DEV Community by christine (@cseeman).</description>
    <link>https://dev.to/cseeman</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%2F77243%2F5dbbe7a7-3411-4644-b415-cc14b80d7fbc.jpg</url>
      <title>DEV Community: christine</title>
      <link>https://dev.to/cseeman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cseeman"/>
    <language>en</language>
    <item>
      <title>Patch Your Rails: 21 Security Fixes in 8.0.5.2 and 8.1.4.1</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Wed, 04 Nov 2026 16:51:06 +0000</pubDate>
      <link>https://dev.to/cseeman/patch-your-rails-21-security-fixes-in-8052-and-8141-1i7c</link>
      <guid>https://dev.to/cseeman/patch-your-rails-21-security-fixes-in-8052-and-8141-1i7c</guid>
      <description>&lt;p&gt;&lt;a href="https://rubyonrails.org/2026/11/4/Rails-8.0.5.2-and-8.1.4.1-have-been-released" rel="noopener noreferrer"&gt;Rails 8.0.5.2 and 8.1.4.1&lt;/a&gt; came out today with fixes for 21 security issues: 1 critical, 4 high, 13 medium, and 3 low. If your app accepts file uploads and uses ImageMagick, start with the critical one, because attaching a crafted file is enough to trigger it.&lt;/p&gt;

&lt;p&gt;If you're on 8.0, there's also a deadline. Security support for the 8.0 series ends on November 7, and the &lt;a href="https://rubyonrails.org/maintenance" rel="noopener noreferrer"&gt;maintenance policy&lt;/a&gt; lists 8.1 as supported until October 10, 2027. This batch has no 7.2 release, and the announcement recommends moving to at least 8.0. Take the patch now and plan the move to 8.1.&lt;/p&gt;

&lt;p&gt;The issues are identified by GHSA IDs (GitHub Security Advisory numbers). None of the 21 had a CVE ID (the usual public vulnerability number) when I wrote this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five That Need a Closer Look
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Critical: File Write and Read in Active Storage Image Analysis
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-f3w6-w445-fwh9" rel="noopener noreferrer"&gt;GHSA-f3w6-w445-fwh9&lt;/a&gt; (CVSS severity score: 9.8 out of 10). An unauthenticated attacker may be able to write arbitrary files on the server. When attachments use the Disk service (Active Storage saving files to local disk), they may also be able to read any file the app process can read, including &lt;code&gt;secret_key_base&lt;/code&gt; and other credentials.&lt;/p&gt;

&lt;p&gt;ImageMagick picks a coder (its handler for a file format) based on the file's content, and some coders act on other files named inside the image. Active Storage didn't restrict them. The ImageMagick analyzer runs at attach time, so no variant (a resized copy of an image), preview, or &lt;code&gt;analyze&lt;/code&gt; call is needed.&lt;/p&gt;

&lt;p&gt;You're affected if all three are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your app sets &lt;code&gt;config.active_storage.variant_processor = :mini_magick&lt;/code&gt;, which is the default on &lt;code&gt;load_defaults 6.1&lt;/code&gt; or earlier.&lt;/li&gt;
&lt;li&gt;Your ImageMagick security policy doesn't already deny these coders. The policies packaged by Debian and Ubuntu don't.&lt;/li&gt;
&lt;li&gt;Untrusted users can upload files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To check the first two:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s1"&gt;'variant_processor|load_defaults'&lt;/span&gt; config
identify &lt;span class="nt"&gt;-list&lt;/span&gt; policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the second command somewhere that matches production, like inside your production image. Look for rules denying &lt;code&gt;MSL&lt;/code&gt; (ImageMagick's scripting format) and Ghostscript coders like &lt;code&gt;PDF&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In July's &lt;a href="https://christine-seeman.com/cve-2026-66066-active-storage/" rel="noopener noreferrer"&gt;CVE-2026-66066&lt;/a&gt;, the exposed apps were the ones using libvips, the other image library Active Storage supports. This one affects apps using ImageMagick.&lt;/p&gt;

&lt;h3&gt;
  
  
  High: File Read and Possible RCE in Video Previews and Analysis
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-v924-7rp8-4mxh" rel="noopener noreferrer"&gt;GHSA-v924-7rp8-4mxh&lt;/a&gt; (CVSS 8.8). Active Storage kept the uploaded file's extension on the tempfile it handed to &lt;code&gt;ffprobe&lt;/code&gt;, &lt;code&gt;ffmpeg&lt;/code&gt;, and ImageMagick, and those tools can choose a decoder from the extension. So whoever uploads the file can influence which decoder runs. On an FFmpeg built with support for AviSynth (a video scripting tool), a &lt;code&gt;.avs&lt;/code&gt; file gets evaluated as a script, which could disclose files or lead to remote code execution (RCE, running the attacker's code on your server).&lt;/p&gt;

&lt;p&gt;You're affected if you accept untrusted uploads and either analyze video or audio, generate video previews, or process images with ImageMagick. Active Storage analyzes video and audio on attach by default, so moving to libvips doesn't rule you out here.&lt;/p&gt;

&lt;p&gt;The AviSynth attack also needs FFmpeg built with &lt;code&gt;--enable-avisynth&lt;/code&gt; plus the AviSynthPlus runtime. The advisory says upstream FFmpeg and the common Linux distribution and Homebrew packages don't enable it, while the GPL builds from ffmpeg.org do. It also warns that other decoders may be reachable without AviSynth. To see how yours was built:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-version&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; enable-avisynth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No output means it isn't enabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  High: Strong Parameters Bypass in &lt;code&gt;each_key&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-wm3x-v3qw-4jh3" rel="noopener noreferrer"&gt;GHSA-wm3x-v3qw-4jh3&lt;/a&gt; (CVSS 7.5). &lt;code&gt;ActionController::Parameters#each_key&lt;/code&gt; with a block returned the raw internal hash, which mass assignment (setting many attributes at once from a hash) accepts without a permit check. This only matters if your code passes that return value to &lt;code&gt;new&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, or &lt;code&gt;assign_attributes&lt;/code&gt;, and Rails itself never does. It's a quick search:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'each_key'&lt;/span&gt; app lib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most hits will have nothing to do with params. You're looking for a call on &lt;code&gt;params&lt;/code&gt; whose result gets used.&lt;/p&gt;

&lt;h3&gt;
  
  
  High: Crash via &lt;code&gt;safe_constantize&lt;/code&gt; and Action Cable
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-8474-xrp6-wh69" rel="noopener noreferrer"&gt;GHSA-8474-xrp6-wh69&lt;/a&gt; (CVSS 7.5). This one reminded me of the &lt;a href="https://christine-seeman.com/ruby-regexp-gitlab-cves/" rel="noopener noreferrer"&gt;Ruby regexp bugs from last month&lt;/a&gt;. When a constant lookup raised &lt;code&gt;LoadError&lt;/code&gt;, &lt;code&gt;safe_constantize&lt;/code&gt; built a regular expression from the constant name without escaping it. A crafted pattern can trigger defects in Ruby's regexp compiler and possibly crash the server process. The advisory doesn't name those bugs or say whether a patched Ruby prevents this crash, so I'd upgrade Rails either way.&lt;/p&gt;

&lt;p&gt;Action Cable passes the &lt;code&gt;channel&lt;/code&gt; value from each &lt;code&gt;subscribe&lt;/code&gt; command to &lt;code&gt;safe_constantize&lt;/code&gt;, and new Rails apps mount Action Cable at &lt;code&gt;/cable&lt;/code&gt; by default. If Action Cable is mounted and you haven't added a check to &lt;code&gt;ApplicationCable::Connection#connect&lt;/code&gt;, an unauthenticated client can reach this code, even if you never built a channel.&lt;/p&gt;

&lt;h3&gt;
  
  
  High: IP Spoofing in &lt;code&gt;remote_ip&lt;/code&gt; (8.1 Only)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-2gff-2mmg-m73p" rel="noopener noreferrer"&gt;GHSA-2gff-2mmg-m73p&lt;/a&gt; (CVSS 7.4). Since 8.1.0 under Rack 3, &lt;code&gt;request.remote_ip&lt;/code&gt; prefers the &lt;code&gt;Forwarded&lt;/code&gt; header over &lt;code&gt;X-Forwarded-For&lt;/code&gt;. If your proxy passes a client-supplied &lt;code&gt;Forwarded&lt;/code&gt; header through unchanged, the client chooses its own IP. That can get around IP-based access rules, falsify logs, and bypass &lt;code&gt;rate_limit&lt;/code&gt;, whose default &lt;code&gt;by:&lt;/code&gt; uses the client address.&lt;/p&gt;

&lt;p&gt;Upgrading doesn't fully close this. The fix only detects spoofing when the proxy also sets &lt;code&gt;X-Forwarded-For&lt;/code&gt;, so it's worth configuring your proxy to remove or overwrite &lt;code&gt;Forwarded&lt;/code&gt; either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Other 16
&lt;/h2&gt;

&lt;p&gt;Each row is what your app has to be doing to be exposed. XSS is cross-site scripting (an attacker's script running in another user's browser), DoS is denial of service (making the app unavailable), and proxy mode is when Active Storage streams files through your app instead of redirecting to the storage service.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Advisory&lt;/th&gt;
&lt;th&gt;You're exposed if&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-2p2f-4rq4-gjwc" rel="noopener noreferrer"&gt;XSS in text helpers&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You use the HTML5 sanitizer and pass untrusted text to &lt;code&gt;highlight&lt;/code&gt;, or to &lt;code&gt;simple_format&lt;/code&gt; with &lt;code&gt;html_options&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-v6rj-xm57-3p59" rel="noopener noreferrer"&gt;XSS in &lt;code&gt;highlight&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A custom &lt;code&gt;:highlighter&lt;/code&gt; puts a placeholder inside an attribute value, and users control the text or phrases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-rq8q-p985-fjq7" rel="noopener noreferrer"&gt;XSS in &lt;code&gt;strip_tags&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You render &lt;code&gt;strip_tags&lt;/code&gt; output from untrusted input inside an HTML attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-68p9-83h6-5wpj" rel="noopener noreferrer"&gt;XSS in tag helpers&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Users can specify custom HTML attribute names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-2w9f-fxwj-w2qf" rel="noopener noreferrer"&gt;XSS in MessagePack serializer&lt;/a&gt; (8.1 only)&lt;/td&gt;
&lt;td&gt;You use a MessagePack serializer and store a &lt;code&gt;SafeBuffer&lt;/code&gt; (Rails' HTML-safe string type) mutated in place with untrusted input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-rwvh-fw6r-pwvp" rel="noopener noreferrer"&gt;DoS in Action Controller Live&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You stream with &lt;code&gt;ActionController::Live&lt;/code&gt; or &lt;code&gt;send_stream&lt;/code&gt;, or serve large files through Active Storage proxy mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-4v2w-w6g6-f8f8" rel="noopener noreferrer"&gt;DoS in proxy streaming&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You store files on S3, an S3-compatible service, or Google Cloud Storage and use Active Storage's default routes, which are on unless you've turned them off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-p2pm-g2h6-7j5j" rel="noopener noreferrer"&gt;DoS in filename handling&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Users can upload attachments and you're on a database that accepts NUL bytes, like SQLite or MySQL/MariaDB. PostgreSQL isn't affected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-3w93-6hmw-whh4" rel="noopener noreferrer"&gt;Unsigned Content-Type on GCS&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You use Google Cloud Storage with &lt;code&gt;cache_control&lt;/code&gt; configured and accept direct uploads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-hfr4-8rjc-97m2" rel="noopener noreferrer"&gt;File size bypass on GCS&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You use Google Cloud Storage and accept direct uploads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-2586-8ghr-69fj" rel="noopener noreferrer"&gt;Destroyed sessions restored&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You use &lt;code&gt;config.session_store :cache_store&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-x6mj-8jx9-qhqv" rel="noopener noreferrer"&gt;Session leak in proxy mode&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You serve files through the proxy controllers, have a cookie rotation or &lt;code&gt;:hybrid&lt;/code&gt; serializer migration configured, and run behind a cache that stores &lt;code&gt;Set-Cookie&lt;/code&gt; on public responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-r9jg-fmfx-2gv6" rel="noopener noreferrer"&gt;Cache poisoning&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You render responses whose format depends on request headers from &lt;code&gt;public&lt;/code&gt; actions behind a shared cache that honors &lt;code&gt;Vary&lt;/code&gt;, such as Thruster or &lt;code&gt;Rack::Cache&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-3f4q-8x5m-76v8" rel="noopener noreferrer"&gt;Local file disclosure via FFmpeg&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You accept untrusted uploads and preview or analyze video or audio with FFmpeg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-5957-c7qm-m469" rel="noopener noreferrer"&gt;Mailgun signature replay&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You receive inbound email through the Action Mailbox Mailgun ingress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rails/rails/security/advisories/GHSA-gmj8-hvj3-49mw" rel="noopener noreferrer"&gt;Missing SameSite&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You use a session store other than &lt;code&gt;CookieStore&lt;/code&gt; and don't pass &lt;code&gt;same_site:&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What Might Break When You Upgrade
&lt;/h2&gt;

&lt;p&gt;These are the behavior changes I found in the advisories that may need action from you:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If your app…&lt;/th&gt;
&lt;th&gt;Then…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Uses &lt;code&gt;:mini_magick&lt;/code&gt; as the variant processor&lt;/td&gt;
&lt;td&gt;You need mini_magick 5.1 or later, or the app raises at boot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Renders PDF or PostScript through ImageMagick&lt;/td&gt;
&lt;td&gt;Those are now denied. Re-allow the &lt;code&gt;gs&lt;/code&gt; delegate (how ImageMagick calls Ghostscript, its PDF and PostScript tool) and the coders you use in the host's &lt;code&gt;policy.xml&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replaces &lt;code&gt;MiniMagick.cli_env&lt;/code&gt; after boot, clears the environment with &lt;code&gt;MiniMagick.cli_prefix&lt;/code&gt;, installs its own &lt;code&gt;MiniMagick::Shell&lt;/code&gt;, or uses GraphicsMagick&lt;/td&gt;
&lt;td&gt;The shipped policy doesn't reach you, so the rules need to go in ImageMagick's system policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uses browser direct uploads to Google Cloud Storage&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;x-goog-content-length-range&lt;/code&gt; to &lt;code&gt;responseHeader&lt;/code&gt; in the bucket's CORS config (its rules for browser requests from other sites), or uploads fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Previews or analyzes formats like MXF, DV, or AC3&lt;/td&gt;
&lt;td&gt;FFmpeg can now only open formats in &lt;code&gt;config.active_storage.ffmpeg_allowed_formats&lt;/code&gt;, and other files lose previews and metadata. Add them with &lt;code&gt;config.active_storage.ffmpeg_allowed_formats += %w[ mxf ]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uses &lt;code&gt;strip_tags&lt;/code&gt; output outside HTML, like JSON or text templates&lt;/td&gt;
&lt;td&gt;Quotes now appear as &lt;code&gt;"&lt;/code&gt; and &lt;code&gt;'&lt;/code&gt;. &lt;code&gt;CGI.unescapeHTML&lt;/code&gt; decodes them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compares sanitized HTML as strings, including in tests&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;&lt;/code&gt; and &lt;code&gt;&amp;gt;&lt;/code&gt; in attribute values are now escaped, because the release requires rails-html-sanitizer 1.7.2 and Nokogiri 1.19.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is on 8.1 behind proxies that report the client differently in &lt;code&gt;Forwarded&lt;/code&gt; and &lt;code&gt;X-Forwarded-For&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Requests now raise &lt;code&gt;IpSpoofAttackError&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is on 8.1 and stores unsafe &lt;code&gt;SafeBuffer&lt;/code&gt;s with MessagePack&lt;/td&gt;
&lt;td&gt;During a rolling deploy, processes not yet upgraded may fail to load those cookies or cache entries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uses the Mailgun ingress with a clock over 2 minutes behind Mailgun's&lt;/td&gt;
&lt;td&gt;Genuine webhooks get rejected, so sync the clock&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Shared caches can also store more variants than before of responses whose format depends on request headers, because &lt;code&gt;Vary&lt;/code&gt; now lists more headers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aside: The One Thing CI Caught
&lt;/h3&gt;

&lt;p&gt;CI went fine for one app's upgrade to these releases, with one exception. Reek, a tool that flags code smells, reported six warnings on a class that had no entry in the project's baseline of known warnings. Most of them shared one cause: the class was handling a job that belonged elsewhere. A small refactor cleared them. It was weird that this came up during the Rails upgrade and not earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Upgrade to 8.0.5.2 or 8.1.4.1. Run &lt;code&gt;bundle update rails --conservative&lt;/code&gt;, which limits what else changes in &lt;code&gt;Gemfile.lock&lt;/code&gt;. Then confirm the lockfile shows the new Rails version and Nokogiri 1.19.5 or later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you use &lt;code&gt;:mini_magick&lt;/code&gt; and &lt;code&gt;bundle info mini_magick&lt;/code&gt; shows a version below 5.1, run &lt;code&gt;bundle update mini_magick&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rotate secrets if you were exposed. The critical advisory says to do this if you met its conditions and store attachments with the Disk service. The video advisory says to do it if you meet its AviSynth requirements. That covers &lt;code&gt;secret_key_base&lt;/code&gt;, the master key, everything in &lt;code&gt;config/credentials.yml.enc&lt;/code&gt;, and storage, database, and third-party credentials, and the advisories say not to keep an exposed secret as a fallback. Changing &lt;code&gt;secret_key_base&lt;/code&gt; expires active sessions and invalidates signed and encrypted cookies, signed global IDs, and Active Storage URLs, so users will need to log in again.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you're on 8.1 behind a proxy, set it to remove or overwrite &lt;code&gt;Forwarded&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you're on 8.0, put the move to 8.1 on the calendar.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  If You Can't Upgrade Yet
&lt;/h2&gt;

&lt;p&gt;Each advisory links to patches for 8.0 and 8.1 where they apply, and many list workarounds. For the top five:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ImageMagick (critical):&lt;/strong&gt; copy the rules from the policy Rails now ships, &lt;code&gt;activestorage/lib/active_storage/image_magick/policy.xml&lt;/code&gt;, into your system policy, such as &lt;code&gt;/etc/ImageMagick-7/policy.xml&lt;/code&gt;. The advisory's minimum is below, and it blocks Ghostscript, so it also affects PDF and PostScript rendering.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;policy&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"coder"&lt;/span&gt; &lt;span class="na"&gt;rights=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;pattern=&lt;/span&gt;&lt;span class="s"&gt;"MSL"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;policy&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"delegate"&lt;/span&gt; &lt;span class="na"&gt;rights=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;pattern=&lt;/span&gt;&lt;span class="s"&gt;"gs"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;policy&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"coder"&lt;/span&gt; &lt;span class="na"&gt;rights=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;pattern=&lt;/span&gt;&lt;span class="s"&gt;"{PS,PS2,PS3,EPS,EPS2,EPS3,EPI,EPSF,EPSI,EPT,EPT2,EPT3,PDF,AI,EPDF,PDFA,POCKETMOD,XPS,PCL}"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Video previews and analysis:&lt;/strong&gt; remove &lt;code&gt;ActiveStorage::Previewer::VideoPreviewer&lt;/code&gt; from &lt;code&gt;config.active_storage.previewers&lt;/code&gt;, and &lt;code&gt;ActiveStorage::Analyzer::VideoAnalyzer&lt;/code&gt; and &lt;code&gt;ActiveStorage::Analyzer::AudioAnalyzer&lt;/code&gt; from &lt;code&gt;config.active_storage.analyzers&lt;/code&gt;. Switching to an FFmpeg without AviSynth closes that attack but not the decoders ImageMagick picks from the extension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;each_key&lt;/code&gt;:&lt;/strong&gt; don't use its return value. Pass the permitted &lt;code&gt;Parameters&lt;/code&gt; instance to mass assignment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;safe_constantize&lt;/code&gt;:&lt;/strong&gt; if you don't use Action Cable, set &lt;code&gt;config.action_cable.mount_path = nil&lt;/code&gt;. If you do, call &lt;code&gt;reject_unauthorized_connection&lt;/code&gt; in &lt;code&gt;connect&lt;/code&gt; for unauthenticated clients, which limits exposure to logged-in users but doesn't remove it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;remote_ip&lt;/code&gt;:&lt;/strong&gt; if your proxy sets &lt;code&gt;X-Forwarded-For&lt;/code&gt;, add &lt;code&gt;Rack::Request.forwarded_priority = [:x_forwarded]&lt;/code&gt; to an initializer. Rack will then also ignore &lt;code&gt;Forwarded&lt;/code&gt; for the request's scheme, host, and port.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're on 7.2 or older, there's no release for you in this batch. Most of the advisories list every version below 8.0.5.2 as affected, so moving to a supported series is the fix. The advisory patches may help in the meantime, though they target 8.0 and 8.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  More Information
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rubyonrails.org/2026/11/4/Rails-8.0.5.2-and-8.1.4.1-have-been-released" rel="noopener noreferrer"&gt;Rails release announcement&lt;/a&gt;, with all 21 advisories and gem checksums&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rubyonrails.org/maintenance" rel="noopener noreferrer"&gt;Rails maintenance policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/v8.1.4.1/activestorage/CHANGELOG.md" rel="noopener noreferrer"&gt;Active Storage changelog for 8.1.4.1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>activestorage</category>
      <category>rails</category>
      <category>ruby</category>
      <category>security</category>
    </item>
    <item>
      <title>AI for everything besides coding</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Wed, 28 Oct 2026 23:48:46 +0000</pubDate>
      <link>https://dev.to/cseeman/ai-for-everything-besides-coding-4hen</link>
      <guid>https://dev.to/cseeman/ai-for-everything-besides-coding-4hen</guid>
      <description>&lt;p&gt;I use Claude Code all day for my job. That part of my AI use is easy to describe: there's a repo, a ticket, a diff at the end.&lt;/p&gt;

&lt;p&gt;Chats are different. When I open a chat, it's usually because I want to search for something but I already know the search will go badly. The question is a little odd, or very specific to me, or I want the answer from one particular angle. So I wondered: if AI is improving my life anywhere outside of work, it's probably in those chats. Did they save me time I spent on something better, or did it go to mindless browsing (which I am trying to cut down on)?&lt;/p&gt;

&lt;p&gt;I went back through everything from mid-August, around when I updated our family trip plan, to last weekend. That's August 15 to October 25, 2026: 20 conversations.&lt;/p&gt;

&lt;p&gt;The short version: the chats that helped most went past the question I typed, to what I was actually trying to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually asked
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;th&gt;Chats&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trips&lt;/td&gt;
&lt;td&gt;Orlando family trip plan, Orlando packing list, Epic Universe itinerary, Rocky Mountain Ruby hotel comparison, an easy hike in Boulder, finding a cruise travel agent for my mom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health&lt;/td&gt;
&lt;td&gt;A treatment plan and tracker for my eyes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recipe tweaks and odd asks&lt;/td&gt;
&lt;td&gt;Flax and chia crackers, anti-inflammatory swaps for the week's meals plus an Aldi list, a cabbage side for pierogi, the best value on a restaurant wine list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;House and family&lt;/td&gt;
&lt;td&gt;A cat food smell in the kitchen, piano tuition reminders, a reminder for a cat photo contest deadline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hobbies and volunteering&lt;/td&gt;
&lt;td&gt;Orchestra Omaha concert program and intro speech, a fragrance hunt, copying files on a Steam Deck&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Work, but not code&lt;/td&gt;
&lt;td&gt;Catching up on a week of Slack, summarizing a tech industry lawsuit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Almost none of these would have gone well as a search.&lt;/p&gt;

&lt;h2&gt;
  
  
  I asked about price and got a perfumer
&lt;/h2&gt;

&lt;p&gt;The best result came from a fragrance chat. In September I asked for the best price on Hermès Un Jardin sur le Nil, then the per-ounce cost at FragranceX, then: if I really like this one, what else should I try?&lt;/p&gt;

&lt;p&gt;It answered by the perfumer instead of the brand. Un Jardin sur le Nil is a Jean-Claude Ellena fragrance, and the thing I liked about it is his style: watery, green, light. So it pointed me to six more in that style that FragranceX carries.&lt;/p&gt;

&lt;p&gt;I bought two from FragranceX: Le Jardin de Monsieur Li, 3.3 oz for $66.45, and a 1.7 oz Un Jardin sur le Nil for $71.87. I wouldn't have bought from that site at all without the recommendation and the price breakdown. They're my new go-to fragrances.&lt;/p&gt;

&lt;p&gt;A search for "perfumes like Un Jardin sur le Nil" would have given me a listicle. I got an explanation of why I liked the one I liked, and that's what made the next purchase easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  I asked for an air freshener and got sent to a cabinet
&lt;/h2&gt;

&lt;p&gt;I have three cats, and our open kitchen and living room had a lingering cat food smell. I asked for the best air freshener strategy and told it to ask me as many questions as it needed. After a few rounds, its answer was that an air freshener should be the last step.&lt;/p&gt;

&lt;p&gt;The smell was a constant haze, worse on warm days, with no obvious source. That pattern pointed to something soaking up odor and slowly releasing it. The likely culprit was the cabinet that holds the recycling bin. Rinsed cans still carry fish oil, and it guessed the inside was bare wood that had been absorbing it for months. The second suspect was the gasket on the microchip feeders (bowls that only open for the cat whose chip they read), which I had never taken out. The plan: scrub and seal the cabinet with a shellac primer, take the feeders apart, run the range hood at feeding time, and only then add scent. It also warned that essential oil diffusers aren't safe around cats.&lt;/p&gt;

&lt;p&gt;Then it corrected itself. When I asked whether its air purifier pick was the most recommended, it said no. It had given me the Austin Air HealthMate, an odor specialist that only covers about 234 square feet. For an open floor plan it suggested two quieter Alen BreatheSmart 75i units instead.&lt;/p&gt;

&lt;p&gt;I had never looked inside that cabinet. It turned out to be finished, so the sealing step wasn't needed. I didn't do the whole plan either. I deep cleaned the trash cans and the cabinet and put a box of baking soda in the back. It helped, and I never bought an air freshener.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same thing, on a smaller scale
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eyes.&lt;/strong&gt; I have meibomian gland dysfunction, where the oil glands in your eyelids don't work well, and wanted a routine that balanced heat against inflammation. It built a daily log I keep on my phone: redness, burning, whether I did the heat routine, glasses or contacts. Once it has three days on each side, it compares contact days against glasses days, so I'll have a real number for my eye doctor. When I asked for a nightly reminder, it said a phone alarm would be more reliable than anything it could set up, and to label the alarm with the actual steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boulder.&lt;/strong&gt; For Rocky Mountain Ruby, a Ruby conference in Boulder where I was speaking, I asked for hotels within a 20-minute walk of eTown Hall, under $750. I stayed at its pick, Basecamp Boulder, and so did another speaker. The walk was easy. It also told me to skip Mount Sanitas on day one, since I'd be going from Omaha's elevation to 5,430 feet. When I asked what coffee to bring because hotel coffee is awful, it checked first: Basecamp puts a kettle and pour-over in most rooms. Heads up for coffee snobs: the in-room pour-over is not good. Skip it and go out for coffee in Boulder instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orlando.&lt;/strong&gt; The night before Epic Universe I asked for early entry times. It gave me 9am early admission and 10am open, and said the times came from TouringPlans because Universal's own hours page wouldn't load. That's the answer to "can I trust this," which I hadn't asked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack.&lt;/strong&gt; The Monday after Orlando, I asked how to catch up on a week of Slack. It worked out the approach with me before running anything, so I got a to-do list instead of a channel-by-channel summary. In 23 minutes I had the two threads where I still owed an answer, and draft replies for both.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When it just answered the question
&lt;/h2&gt;

&lt;p&gt;The rest of the chats did exactly what I asked, and that was fine. The cracker chat took five minutes: more flax and chia, crackers because I had Parmesan, and measurements fixed after it called for 0.8 cup of something. The anti-inflammatory dinner swaps ended with Barilla Protein+ because I don't like whole wheat pasta, and an Aldi list in my email. The piano studio's tuition policy became a year of calendar reminders with each month's amount. Orchestra Omaha, where I play viola and help out on the board, got a concert program and a 90-second intro with a Huskers joke. At Talus, a restaurant in Omaha, it picked the best buy on the wine list, Louis Jadot Mâcon-Villages at $9 a glass. I ordered it, a nice light white, and it was better than the cheese board it was supposed to pair with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patterns
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I lead with my constraints.&lt;/strong&gt; "I do not wear shorts." "I have Parmesan." "3 cats." "I'm not on my laptop that late." That's what gives it enough to notice when I'm asking the wrong question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I edit replies like a draft.&lt;/strong&gt; "Reword shorter." "Funnier." "Just give bullet points." "Normal cups." The first answer is raw material.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The useful ones end somewhere else.&lt;/strong&gt; An email, a calendar entry, a Slack draft, a tracker on my phone, a bottle on my dresser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I start new chats instead of going back.&lt;/strong&gt; I asked about an easy Boulder hike twice, a month apart. When I did stay in one thread, like the Orlando packing list, it already had the clothing list and park days when I asked for a weather check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They happen at the edges of my day.&lt;/strong&gt; Early mornings, like the concert program at 7:41, the Steam Deck at 7:51, and the dinner swaps at 8:03. And evenings, like Talus at 6:08, the orchestra intro at 6:14 for a speech I gave at 7 (I asked for the final bullet points at 6:58), fragrances at 7:12, and packing at 8:26.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Did it save me time?
&lt;/h2&gt;

&lt;p&gt;For the chats where I could compare, here's the time in the chat against how long the task usually takes me:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;In the chat&lt;/th&gt;
&lt;th&gt;Usually&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catching up on a week of Slack&lt;/td&gt;
&lt;td&gt;23 min&lt;/td&gt;
&lt;td&gt;1 to 2 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concert program&lt;/td&gt;
&lt;td&gt;3 min&lt;/td&gt;
&lt;td&gt;45 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concert intro speech&lt;/td&gt;
&lt;td&gt;44 min&lt;/td&gt;
&lt;td&gt;1 hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A year of piano tuition reminders&lt;/td&gt;
&lt;td&gt;8 min&lt;/td&gt;
&lt;td&gt;20 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packing list&lt;/td&gt;
&lt;td&gt;about 20 min&lt;/td&gt;
&lt;td&gt;20 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dinner swaps and the Aldi list&lt;/td&gt;
&lt;td&gt;46 min&lt;/td&gt;
&lt;td&gt;30 to 45 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's roughly an hour and a half to almost three hours saved, over ten weeks. Most of it came from two chats, Slack and the concert program.&lt;/p&gt;

&lt;p&gt;Meh, kinda. &lt;/p&gt;

&lt;p&gt;The fragrance chat was 37 minutes of pure rabbit hole, and I loved it. Over-analyzing something before I buy it is one of my favorite things. I do a lot of Amazon window shopping (&lt;a href="https://christine-seeman.com/prompt-engineering-prime-day/" rel="noopener noreferrer"&gt;I've written about that cart before&lt;/a&gt;): things go into the cart or save-for-later and never come out. This time I actually bought something, and I'm happy with it, because I smell great!&lt;/p&gt;

&lt;p&gt;The best chats answered the question I should have asked, and no search would have gotten me there.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Not Letting PHP Have All the Fun: Ruby’s Regexp Fixes for GitLab’s CVEs</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Fri, 23 Oct 2026 15:44:51 +0000</pubDate>
      <link>https://dev.to/cseeman/not-letting-php-have-all-the-fun-rubys-regexp-fixes-for-gitlabs-cves-4gai</link>
      <guid>https://dev.to/cseeman/not-letting-php-have-all-the-fun-rubys-regexp-fixes-for-gitlabs-cves-4gai</guid>
      <description>&lt;p&gt;The day after I wrote about &lt;a href="https://christine-seeman.com/php2shell-wordpress/" rel="noopener noreferrer"&gt;PHP2Shell&lt;/a&gt;, Ruby posted &lt;a href="https://www.ruby-lang.org/en/news/2026/10/23/regexp-fixes-for-gitlab-cves/" rel="noopener noreferrer"&gt;regexp compilation fixes&lt;/a&gt; tied to four GitLab CVEs: CVE-2026-89078, CVE-2026-93577, CVE-2026-97229, and CVE-2026-110264. GitLab found that specially crafted regular expressions could corrupt memory while Ruby compiles them, and that this could be used for remote code execution on GitLab servers. GitLab rated two of them 10.0 out of 10 (CVSS 3.1), the two that needed no login.&lt;/p&gt;

&lt;h2&gt;
  
  
  Am I Affected?
&lt;/h2&gt;

&lt;p&gt;You're affected if your app builds a regular expression from input you don't control. Per the Ruby advisory, that means passing it to &lt;code&gt;Regexp.new&lt;/code&gt;, &lt;code&gt;Regexp.union&lt;/code&gt;, or &lt;code&gt;Regexp.compile&lt;/code&gt;, or interpolating it into a regexp literal like &lt;code&gt;/#{input}/&lt;/code&gt;. If every regexp in your app was written by a developer, the advisory says you're not affected.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ruby series&lt;/th&gt;
&lt;th&gt;Fixed in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;4.0.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.4&lt;/td&gt;
&lt;td&gt;3.4.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.3&lt;/td&gt;
&lt;td&gt;3.3.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.2 and older&lt;/td&gt;
&lt;td&gt;No fix (end of life)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ruby 3.3 is in security maintenance and got these fixes as an exception, because GitLab assigned CVEs to the issues. Ruby 3.2 and older get no fix. &lt;a href="https://www.ruby-lang.org/en/news/2026/03/27/ruby-3-2-11-released/" rel="noopener noreferrer"&gt;Ruby 3.2.11&lt;/a&gt; in March was the final 3.2 release, with no further security fixes, and Ruby recommends moving to 3.4 or 4.0.&lt;/p&gt;

&lt;p&gt;The CVE IDs are GitLab's. Ruby hasn't requested its own CVEs for these yet and says it's still discussing whether to treat them as vulnerabilities, since Ruby has long considered compiling regexps from untrusted input unsafe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking Your App
&lt;/h2&gt;

&lt;p&gt;Ruby's version is often set in more than one place, so check what production actually runs. &lt;code&gt;RUBY_VERSION&lt;/code&gt; in a Rails console on the server tells you. To find what you've pinned, check your version manager file (&lt;code&gt;.ruby-version&lt;/code&gt; or &lt;code&gt;.tool-versions&lt;/code&gt;), the &lt;code&gt;ruby&lt;/code&gt; line in your &lt;code&gt;Gemfile&lt;/code&gt; if there is one, your Docker base image tag, and any CI image you build separately.&lt;/p&gt;

&lt;p&gt;To find places that compile regexps at runtime, a rough search is a good start:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s1"&gt;'Regexp\.(new|union|compile)|/[^/]*#\{'&lt;/span&gt; app lib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect some noise from the interpolation pattern. For each hit, the question is whether any part of the pattern comes from a user, a request param, or a database field users can edit.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Found in My Own App
&lt;/h3&gt;

&lt;p&gt;I ran this check on a Rails app I work on. Ruby was set in three places, each needing a different fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The version manager file pinned an older 4.0 patch release, so that needed a manual bump.&lt;/li&gt;
&lt;li&gt;The production Dockerfile used a floating tag like &lt;code&gt;ruby:4.0-alpine&lt;/code&gt;. A rebuild picks up 4.0.9 once the official image includes it, but it's worth confirming the patch version inside the image after the rebuild.&lt;/li&gt;
&lt;li&gt;CI ran on a separately built image that only updates when someone reruns its build workflow. That one is easy to forget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The search turned up three spots that build regexps from runtime strings. Patterns came from data import files and an admin form, and one used interpolation with values that looked internal, though I didn't trace every caller. None of them were reachable from a public API, so the risk was low, but anyone who can upload an import file controls those patterns. Upgrading Ruby is the fix either way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aside: The Upgrade Broke CI
&lt;/h3&gt;

&lt;p&gt;Bumping to 4.0.9 the day it came out broke our CI in two ways. Jobs running in our CI Docker image failed because the build asked for a 4.0.9 image that couldn't exist until Docker Hub published &lt;code&gt;ruby:4.0.9-alpine&lt;/code&gt;. Jobs using &lt;code&gt;ruby/setup-ruby&lt;/code&gt; failed with "Unknown version 4.0.9", because each setup-ruby release only knows the Ruby versions that existed when it shipped, and ours was pinned to an older one. The fix was bumping the setup-ruby pin to v1.334.0, released that morning with 4.0.9 support, and keeping the image tag at &lt;code&gt;4.0&lt;/code&gt; to rebuild once the official image caught up.&lt;/p&gt;

&lt;p&gt;The lesson is the order: wait for the base image, bump setup-ruby, then change &lt;code&gt;.tool-versions&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the Risk?
&lt;/h2&gt;

&lt;p&gt;The bugs are in Onigmo, the regular expression engine built into Ruby. One is a double free (memory released twice, which can corrupt it) when parsing fails after an alternation (a &lt;code&gt;|&lt;/code&gt; between options in a pattern). The others are integer overflows (a number too large for its type, wrapping around) in the size of the compiled pattern and in internal IDs for patterns with many repeats. Corrupted memory during compilation is what turns a bad pattern into code execution (&lt;a href="https://www.ruby-lang.org/en/news/2026/10/23/regexp-fixes-for-gitlab-cves/" rel="noopener noreferrer"&gt;Ruby advisory&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;GitLab patched the Ruby it ships in &lt;a href="https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-4-1-released/" rel="noopener noreferrer"&gt;19.4.1&lt;/a&gt;, &lt;a href="https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-4-2-released/" rel="noopener noreferrer"&gt;19.4.2&lt;/a&gt;, and &lt;a href="https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-5-1-released/" rel="noopener noreferrer"&gt;19.5.1&lt;/a&gt;, with backports to older GitLab versions. GitLab.com and GitLab Dedicated are already patched. If you self-host GitLab, update GitLab itself. Its packages ship their own patched Ruby, and source installs also need the Ruby upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Upgrade to Ruby 4.0.9, 3.4.12, or 3.3.13 or later, in every place you found above: version manager file, &lt;code&gt;Gemfile&lt;/code&gt;, Docker base image, and CI image.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you self-host GitLab, update to a patched GitLab release.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Look at any regexp built from user input and ask whether it needs to be a regexp at all. A plain string match is often enough.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  If You Can't Upgrade Yet
&lt;/h2&gt;

&lt;p&gt;The advisory's only mitigation is to avoid compiling regular expressions from untrusted input. It also rules out two things you might reach for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Regexp.timeout&lt;/code&gt; doesn't help. It limits how long matching runs, and these bugs happen while the pattern compiles.&lt;/li&gt;
&lt;li&gt;Limiting pattern length doesn't reliably help, because some of the bugs trigger with short patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The advisory doesn't say whether escaping input with &lt;code&gt;Regexp.escape&lt;/code&gt; is enough, so upgrading is the safe answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  More Information
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.ruby-lang.org/en/news/2026/10/23/regexp-fixes-for-gitlab-cves/" rel="noopener noreferrer"&gt;Ruby advisory: Regexp compilation fixes related to GitLab's CVEs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-4-1-released/" rel="noopener noreferrer"&gt;GitLab 19.4.1 patch release&lt;/a&gt; (CVE-2026-89078, CVE-2026-93577)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-4-2-released/" rel="noopener noreferrer"&gt;GitLab 19.4.2 patch release&lt;/a&gt; (CVE-2026-97229)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-5-1-released/" rel="noopener noreferrer"&gt;GitLab 19.5.1 patch release&lt;/a&gt; (CVE-2026-110264)&lt;/li&gt;
&lt;li&gt;Ruby fixes: &lt;a href="https://github.com/ruby/ruby/pull/13332" rel="noopener noreferrer"&gt;#13332&lt;/a&gt;, &lt;a href="https://github.com/ruby/ruby/pull/19020" rel="noopener noreferrer"&gt;#19020&lt;/a&gt;, &lt;a href="https://github.com/ruby/ruby/pull/16801" rel="noopener noreferrer"&gt;#16801&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ruby-lang.org/en/master/Regexp.html#class-Regexp-label-Timeouts" rel="noopener noreferrer"&gt;Regexp timeouts in the Ruby docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ruby</category>
      <category>security</category>
      <category>cve</category>
      <category>gitlab</category>
    </item>
    <item>
      <title>Patch Your PHP: What PHP2Shell Means for WordPress Sites</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:53:00 +0000</pubDate>
      <link>https://dev.to/cseeman/patch-your-php-what-php2shell-means-for-wordpress-sites-2akn</link>
      <guid>https://dev.to/cseeman/patch-your-php-what-php2shell-means-for-wordpress-sites-2akn</guid>
      <description>&lt;p&gt;PHP released &lt;a href="https://www.php.net/archive/2026.php#2026-10-21-5" rel="noopener noreferrer"&gt;8.5.12, 8.4.27, 8.3.36, and 8.2.35&lt;/a&gt; on October 21 to fix two critical vulnerabilities, CVE-2026-109618 and CVE-2026-109619, nicknamed PHP2Shell. Both are rated 9.2 out of 10 (CVSS v4.0) and let an attacker run code on your server without logging in. The bugs are in PHP itself, the runtime underneath WordPress, so updating WordPress or your plugins won't fix them.&lt;/p&gt;

&lt;p&gt;I have WordPress sites on SiteGround, and when this news hit, my only question was whether I was vulnerable. I hadn't heard anything from SiteGround, and working out which PHP version my sites actually run took longer than I wanted it to. This is the quick writeup I was looking for, for developers who know their stack and just want the answer without spending an afternoon on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Am I Affected?
&lt;/h2&gt;

&lt;p&gt;Any PHP that serves web requests is affected, however it connects to the web server: PHP-FPM, Apache mod_php, php-cgi, FrankenPHP, and LiteSpeed. mod_php (PHP running inside Apache itself) is the worst case, because the demonstrated exploit runs before any WordPress code loads. The leaked video of the exploit tool showed it running code on WordPress 7.1.3, alongside Drupal, Joomla, Magento, and Laravel (&lt;a href="https://arstechnica.com/security/2026/10/php-maintainers-tell-web-devs-to-prepare-themselves-for-critical-security-patches/" rel="noopener noreferrer"&gt;Ars Technica&lt;/a&gt;), so a fully updated WordPress install is still exposed if its PHP is not patched.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;Fixed in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8.5&lt;/td&gt;
&lt;td&gt;8.5.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.4&lt;/td&gt;
&lt;td&gt;8.4.27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.3&lt;/td&gt;
&lt;td&gt;8.3.36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.2&lt;/td&gt;
&lt;td&gt;8.2.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.1, 8.0, 7.4&lt;/td&gt;
&lt;td&gt;No release. Source backports, or patched Linux or LTS (long-term support) packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7.3 and older&lt;/td&gt;
&lt;td&gt;No fix&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;PHP 7.4 through 8.1 are past end of life, so instead of releases, the PHP security team published minimal source backports: just the fix, which you apply and compile yourself. If you don't build PHP from source, get a patched package from your Linux distribution or an LTS (long-term support) vendor. PHP 7.3 and older get nothing, so moving to a supported version is the only option there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking Your PHP Version
&lt;/h2&gt;

&lt;p&gt;The quickest built-in way is to go to &lt;code&gt;/wp-admin/site-health.php?tab=debug&lt;/code&gt;, expand &lt;strong&gt;Server&lt;/strong&gt;, and read &lt;strong&gt;PHP version&lt;/strong&gt;. If you'd like it on the dashboard, the &lt;a href="https://wordpress.org/plugins/display-php-version/" rel="noopener noreferrer"&gt;Display PHP Version&lt;/a&gt; plugin adds it to the At a Glance widget. I liked this option.&lt;/p&gt;

&lt;p&gt;One catch: Linux distributions often add the fix to their existing package without changing the PHP version number. If you install PHP from Debian, Ubuntu, or RHEL packages, check the package changelog for the CVE IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the Risk?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CVE-2026-109618:&lt;/strong&gt; when a request runs out of memory, PHP can abandon it partway through setting up request data, and the cleanup code then treats that half-built data as valid, which leads to code execution. Attackers can trigger it with crafted form uploads, and on mod_php the attack completes before WordPress loads (&lt;a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" rel="noopener noreferrer"&gt;advisory&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CVE-2026-109619:&lt;/strong&gt; a fatal error can leave a PHP worker holding a pointer into memory that's already been freed, and the next request on that worker uses it (a use-after-free), leading to code execution or leaked memory contents. Confirmed triggers include session upload progress, which is on by default, and &lt;code&gt;php_value&lt;/code&gt; lines in &lt;code&gt;.htaccess&lt;/code&gt; on mod_php (&lt;a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" rel="noopener noreferrer"&gt;advisory&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Details leaked two days before the planned disclosure, and Amazon reported an exploitation attempt as early as October 17 (&lt;a href="https://www.tenable.com/blog/php2shell-cve-2026-109618-cve-2026-109619-frequently-asked-questions-about-critical-php-remote" rel="noopener noreferrer"&gt;Tenable's timeline&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Update PHP to a fixed version. On managed hosting, confirm your host has patched. On your own server, update through your Linux distribution's packages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;While you're there, move to PHP 8.3 or newer. That's WordPress's recommended minimum, and 8.2 reaches end of life on December 31, 2026.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hosts handled the news differently. I used to work at WP Engine and still have a test site there, and WP Engine emailed customers to say the patch was applied and that network and server mitigations were in place before PHP's release. SiteGround didn't email me, but its &lt;a href="https://www.siteground.com/blog/critical-php-vulnerabilities-october-2026" rel="noopener noreferrer"&gt;blog post&lt;/a&gt; says it added server-level mitigations after PHP's October 20 notice and applied the official fixes on October 21. Site Health on my site now shows PHP 8.2.35, the fixed release for that branch, though since 8.2 is nearing end of life, I'm planning to move to 8.3. If you haven't heard from your host, check its blog or status page, then confirm the version yourself in Site Health.&lt;/p&gt;

&lt;h2&gt;
  
  
  If You Can't Upgrade Yet
&lt;/h2&gt;

&lt;p&gt;PHP says there are no complete mitigations, but these settings reduce exposure to the confirmed attack paths. They must go in &lt;code&gt;php.ini&lt;/code&gt;, the PHP-FPM pool config, or the web server config, because &lt;code&gt;ini_set()&lt;/code&gt; and &lt;code&gt;.user.ini&lt;/code&gt; apply too late. On shared hosting, that usually means asking your host when they'll patch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;memory_limit=-1&lt;/code&gt; with &lt;code&gt;php_admin_value&lt;/code&gt; so applications can't override it, and keep &lt;code&gt;post_max_size&lt;/code&gt; low (8M, or 32M if you need uploads).&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;session.upload_progress.enabled=0&lt;/code&gt;, and on PHP 8.5, &lt;code&gt;fatal_error_backtraces=0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Remove obsolete or invalid &lt;code&gt;php_value&lt;/code&gt; lines from &lt;code&gt;.htaccess&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Restart every PHP worker after any change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The advisories cover the rest, including &lt;a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" rel="noopener noreferrer"&gt;why not to cap memory with &lt;code&gt;RLIMIT_AS&lt;/code&gt; or &lt;code&gt;RLIMIT_DATA&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  More Information
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.php.net/archive/2026.php#2026-10-21-5" rel="noopener noreferrer"&gt;PHP security advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" rel="noopener noreferrer"&gt;GHSA-w8hw-w9fx-j79w (CVE-2026-109618)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" rel="noopener noreferrer"&gt;GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arstechnica.com/security/2026/10/php-maintainers-tell-web-devs-to-prepare-themselves-for-critical-security-patches/" rel="noopener noreferrer"&gt;Ars Technica coverage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.tenable.com/blog/php2shell-cve-2026-109618-cve-2026-109619-frequently-asked-questions-about-critical-php-remote" rel="noopener noreferrer"&gt;Tenable FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://make.wordpress.org/core/2026/05/22/php-support-clarification-2026/" rel="noopener noreferrer"&gt;WordPress PHP support policy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>wordpress</category>
      <category>cve</category>
    </item>
    <item>
      <title>Ontological Shock at Altitude</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Tue, 06 Oct 2026 03:39:41 +0000</pubDate>
      <link>https://dev.to/cseeman/ontological-shock-at-altitude-2jp2</link>
      <guid>https://dev.to/cseeman/ontological-shock-at-altitude-2jp2</guid>
      <description>&lt;p&gt;Travis Turner closed the first day of &lt;a href="https://rockymtnruby.dev/schedule/" rel="noopener noreferrer"&gt;Rocky Mountain Ruby&lt;/a&gt; with a term I hadn't heard before: ontological shock. It's what happens when something new fundamentally disturbs your sense of being, and he argued that programmers are feeling it right now. The worries about job duties, tooling, and job security sit downstream of it. The next afternoon, Alan Ridlehoover used the same phrase in the closing keynote to describe his own work. By then it felt like the name for the whole conference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two days, one track, one former church
&lt;/h2&gt;

&lt;p&gt;Rocky Mountain Ruby is two days and a single track in Boulder, Colorado. This year, and in previous ones, it has been at &lt;a href="https://www.etown.org/etown-hall/" rel="noopener noreferrer"&gt;eTown Hall&lt;/a&gt;, a multi-purpose building in the heart of downtown Boulder, a short walk from Pearl Street. It's a former church, and it just reeks of cool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdw82wfhdibg50kok1883.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdw82wfhdibg50kok1883.jpg" alt="Rocky Mountain Ruby organizers welcoming everyone on the eTown Hall stage, with a Conversation Starters slide" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most of the talks were about AI in some way, and they kept coming back to people. Andrea told us not to let an agent get to know our team for us. Giovanni said the biggest quality win in his RAG app was string handling. That's the thread I followed through the schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting to basecamp
&lt;/h2&gt;

&lt;p&gt;I flew Southwest from Omaha to Denver, then hopped on the bus from Denver to Boulder. I love that option. The bus is very convenient, I love making use of public transit, and it's so much less stress than renting a car. Once I got to my basecamp (a wink at my actual hotel, Basecamp Boulder), I hit the Chautauqua Trail and the meadow. So pretty. So scenic. And wow, the Flatirons.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmwr1akf9sauy5lnr1ld1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmwr1akf9sauy5lnr1ld1.jpg" alt="Christine smiling in a ball cap on the Chautauqua meadow, with pines and mountains behind" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are many, many coffee shops around the venue. I tried six different ones in four days, and none of them were a big chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building agents
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.drbragg.dev/" rel="noopener noreferrer"&gt;Drew Bragg&lt;/a&gt; does more than game shows, though his game shows are awesome. &lt;em&gt;Your own AI Agent in ~400 lines of Ruby&lt;/em&gt; was live coding, with webhooks galore. The code is on &lt;a href="https://github.com/DRBragg/ruby_agent" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; if you want to build along.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2swoh6cdcnx6vrh8xj72.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2swoh6cdcnx6vrh8xj72.jpg" alt="Drew Bragg at the podium with his title slide, Your own AI Agent in ~400 lines of Ruby" width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://panasiti.me" rel="noopener noreferrer"&gt;Giovanni Panasiti&lt;/a&gt; showed &lt;em&gt;Building an Agentic RAG in the Rails Way with RubyLLM&lt;/em&gt;, and the whole AI stack fits in a Gemfile. The part that most improves answer quality is plain string handling: chunking that respects headers, article numbers, and tables. As he asked the room, do you think you need Python or some fancy language to handle strings? Rails developers are not second-class citizens in the AI conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working alongside agents
&lt;/h2&gt;

&lt;p&gt;It's always nice to see &lt;a href="https://kevinjmurphy.com/" rel="noopener noreferrer"&gt;Kevin Murphy&lt;/a&gt;'s &lt;a href="https://kevinjmurphy.com/posts/instillment-of-successful-practices-in-an-agentic-world/" rel="noopener noreferrer"&gt;InstiLLMent of Successful Practices in an Agentic World&lt;/a&gt; again. I loved it at Blue Ridge Ruby, and I'm always happy to be part of Hours Unlimited, the fictional company in his talk where we all start as new hires. Kevin wrote his own &lt;a href="https://kevinjmurphy.com/posts/rocky-mountain-ruby-2026-recap/" rel="noopener noreferrer"&gt;Rocky Mountain Ruby recap&lt;/a&gt; too, so go read his take.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fegbki8nor5dgrj0i18az.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fegbki8nor5dgrj0i18az.jpg" alt="Kevin Murphy on stage in front of a slide listing 1. Artificial General Intelligence and 233. Remote collaboration" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://afomera.dev/" rel="noopener noreferrer"&gt;Andrea Fomera&lt;/a&gt; brought slick HTML slides to &lt;em&gt;High-Octane Onboarding: Ramping into a Rails Codebase with AI Agents&lt;/em&gt; and one big question: how do you onboard quickly and become effective, and can AI speed that up? Her answer: onboarding may be the most useful moment to have an AI agent, so use it for that. Just don't let it do the part where you get to know your team.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://inveterateliterate.github.io/" rel="noopener noreferrer"&gt;Ifat Ribon&lt;/a&gt;'s &lt;a href="https://inveterateliterate.github.io/testing-philosophy/" rel="noopener noreferrer"&gt;Fewer Tests, More Confidence: A Rails Testing Philosophy for the AI Era&lt;/a&gt; started from a simple point: AI can generate more tests, but not always better tests. Every test has a cost, in slower feedback loops, heavier CI usage, and more cognitive load for the developers who have to understand, maintain, and trust the suite. Her first principle is to test behavior, not implementation: skip testing library and language code, focus on public methods and their results, and make sure tests still pass after a refactor. Then she turned to the agent itself. Bolster the AI harness with rules that codify hard constraints and conventions, and with deterministic tools like code scanning and coverage that objectively verify what the suite actually catches. Great slides, too, and so easy to follow along with.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favnbtfctzvwc0hjnw8vq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favnbtfctzvwc0hjnw8vq.jpg" alt="Ifat Ribon presenting a slide titled Bolster the AI Harness, on deterministic tools" width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Naming the shock
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://evilmartians.com/martians/travis-turner" rel="noopener noreferrer"&gt;Travis Turner&lt;/a&gt;'s &lt;em&gt;Got AI ontological shock? No worries: UFOs, Theology, and Psychedelics with Ruby-Colored Reverence&lt;/em&gt; closed day one. It was a manic pixie dream guy of a talk. His premise, from the &lt;a href="https://www.rubyevents.org/talks/got-ai-ontological-shock-no-worries-ufos-theology-and-psychedelics-with-ruby-colored-reverence" rel="noopener noreferrer"&gt;abstract&lt;/a&gt;, is that we're all coming to terms with this shock in different ways and at different times, so we're not on the same page. He named the shared experience, the blocks to coping with it, and some frameworks for working through it. Yes, it got deep.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz9r5cpkaycjmbm4toxz8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz9r5cpkaycjmbm4toxz8.jpg" alt="Travis Turner at the podium with his title slide on theology, UFOs and psychedelics" width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Rails craft
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/nerlichman" rel="noopener noreferrer"&gt;Nicolas Erlichman&lt;/a&gt;'s &lt;em&gt;Modeling Class Hierarchies in Active Record&lt;/em&gt; made the case for delegated types, which still aren't widely known (by humans, anyway). Before them, Active Record offered two ways to model inheritance: single-table inheritance and polymorphic associations. Delegated types give you unified queries, clean schemas with no table bloat, and a model that scales naturally as new types appear.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/ferperales" rel="noopener noreferrer"&gt;Fernando Perales&lt;/a&gt; opened &lt;em&gt;Slowly We Rot: signs of a Rails app's decay&lt;/em&gt; with a question. When was the last time you ran your entire test suite locally? All of it. His takeaways: rot is gradual, not catastrophic. Preserve your ability to change the system. Make trade-offs explicit, and revisit them. And in high school we were allowed to ignore friction, but software teams don't get that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marcoroth.dev/" rel="noopener noreferrer"&gt;Marco Roth&lt;/a&gt; has so, so many gems. In &lt;a href="https://speakerdeck.com/marcoroth/herb-in-rails-8-dot-2-your-erb-views-now-html-aware-at-rocky-mountain-ruby-2026-boulder-colorado" rel="noopener noreferrer"&gt;Herb in Rails 8.2: Your ERB views, now HTML-aware&lt;/a&gt;, he walked a full room through Herb's path to becoming an opt-in view engine in Rails 8.2: how the integration came together, how ActionView was adapted and extended, and the toolchain and dev tools every Rails developer gets out of the box as a result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6gdmqxpi0iie2mrxyw54.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6gdmqxpi0iie2mrxyw54.jpg" alt="Marco Roth in front of a full room, with a slide for herb-tools.dev and reactionview.dev" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  People and careers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://sonja.codes/" rel="noopener noreferrer"&gt;Sonja Peterson&lt;/a&gt;'s &lt;em&gt;Confident On-Call: Building Incident Response Skills as a Ruby Engineer&lt;/em&gt; argued that being confident on-call is a learnable skill, not a personality trait. Incident response takes a lot of different forms, but certain skills and knowledge always stay relevant. Do a daily review so you know what normal looks like in your system, and get comfortable with what you can observe from the command line.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hawksley.org/" rel="noopener noreferrer"&gt;Joel Hawksley&lt;/a&gt;'s &lt;em&gt;Beyond Senior: Consider the staff path!&lt;/em&gt; says your path to staff should begin with staff behaviors long before you go up for promotion. My favorite part was the rope analogy GitHub uses for technical depth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Junior: learns about rope&lt;/li&gt;
&lt;li&gt;Mid: can tie basic knots&lt;/li&gt;
&lt;li&gt;Senior: calculates rope strength, knows a lot about knots&lt;/li&gt;
&lt;li&gt;Staff: understands rope making&lt;/li&gt;
&lt;li&gt;Principal: knows more about rope than you ever will&lt;/li&gt;
&lt;li&gt;Distinguished: invented nylon&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;His &lt;a href="https://hawksley.org/2026/01/14/beyond-senior.html" rel="noopener noreferrer"&gt;transcript&lt;/a&gt; and &lt;a href="https://github.com/joelhawksley/identity/blob/main/talks/2026-beyond-senior-consider-the-staff-path/slides.key" rel="noopener noreferrer"&gt;slides&lt;/a&gt; are both online.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xcrventures.com" rel="noopener noreferrer"&gt;Ben Reubenstein&lt;/a&gt;'s &lt;em&gt;Commit for Good&lt;/em&gt; covered his adventures as a consultant and the software he has built for nonprofits along the way. He shared a quote from Tom Kimball: "At some point in each of our lives we get exposed to the importance of getting involved and giving back." For Byrne &amp;amp; Denver Urban Scholars, from 2011 to 2014, that looked like registration, payments, team pages, and leaderboards. His pitch: write some Ruby, help a nonprofit, be part of your community, and help build a better spreadsheet. You will get more out of it than you give.&lt;/p&gt;

&lt;p&gt;I gave &lt;a href="https://speakerdeck.com/tech_christine/optimize-your-mindset-without-overclocking-36c475cb-3c5f-4da6-9a11-1488a34c3243" rel="noopener noreferrer"&gt;Optimize Your Mindset (Without Overclocking)&lt;/a&gt; on day two. It tells the story of four people and four research-backed techniques to level up your work and life: mindfulness for breaking thought loops and managing stress, growth mindset for embracing challenges and continuous learning, habit building for sustainable change, and deep work strategies for keeping focus in a distracted world. None of them involve code. With so much going on right now, having ways to cope helps. The &lt;a href="https://christine-seeman.com/optimize-your-mindset-references/" rel="noopener noreferrer"&gt;references&lt;/a&gt; are up if you want to dig in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lightning talks
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.chael.codes/" rel="noopener noreferrer"&gt;Rachael Wright-Munn&lt;/a&gt; talked about &lt;a href="https://www.rubyevents.org/talks/rubyevents-on-a-mission-to-index-all-ruby-events" rel="noopener noreferrer"&gt;RubyEvents&lt;/a&gt; and its mission to index all Ruby events. RubyEvents keeps its event and talk data in YAML files, which made "It's YAML!" the running joke. Marco came back for one more, &lt;a href="https://speakerdeck.com/marcoroth/insta-snapshot-testing-for-ruby-at-rocky-mountain-ruby-2026-boulder-colorado" rel="noopener noreferrer"&gt;Insta: Snapshot Testing for Ruby&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Indispensable
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://the.codegardener.com/" rel="noopener noreferrer"&gt;Alan Ridlehoover&lt;/a&gt; closed the conference with &lt;em&gt;Indispensable: What Human Programmers Can Learn from Human Computers&lt;/em&gt;, and he started somewhere uncomfortable. His company asked its engineers to stop writing code by hand. Alan thinks of code as something you cultivate, more like gardening than building a bridge, and he was devastated. In his words, "it was a bit of an ontological shock." Then came the number: over 430 thousand people laid off in tech, in less than two years.&lt;/p&gt;

&lt;p&gt;Then he took us back to the human computers. Women, many with mathematics degrees, ran the numbers for engineers at research facilities across the country. When NACA, NASA's predecessor, started using IBM mainframes in the mid-1950s, their jobs were in peril. Many were reassigned and some retired, but some stayed central to the mission. Dorothy Vaughan, Mary Jackson, and Katherine Johnson, the women of &lt;em&gt;Hidden Figures&lt;/em&gt;, made themselves integral to Project Mercury.&lt;/p&gt;

&lt;p&gt;His close is the line I keep coming back to: "The mainframes didn't step foot on the lunar surface. We did. It wasn't their decision to go to the moon. It was ours. Electronic computers were a tool we used to get there. With AI, compute is now a commodity and efficiency is the baseline. But the human ambition to reach for the stars? That is, and always will be, indispensable."&lt;/p&gt;

&lt;p&gt;Thank you to Spike Ilacqua and Bekki Freeman for putting it all together, and to the sponsors who made it happen: Gusto as the Sapphire sponsor; AppSignal, Honeybadger, Flagrant, and Typesense as Emerald sponsors; and BookBub, Podia, SOFware, and Evil Martians as travel sponsors.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The mainframes didn't step foot on the lunar surface. We did. It wasn't their decision to go to the moon. It was ours. Electronic computers were a tool we used to get there. With AI, compute is now a commodity and efficiency is the baseline. But the human ambition to reach for the stars? That is, and always will be, indispensable." ~ &lt;a href="https://the.codegardener.com/" rel="noopener noreferrer"&gt;Alan Ridlehoover&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Videos will be added later, and I'll link them here when they are. In the meantime, the full &lt;a href="https://rockymtnruby.dev/schedule/" rel="noopener noreferrer"&gt;schedule&lt;/a&gt; is on the conference site.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>ai</category>
      <category>rails</category>
      <category>community</category>
    </item>
    <item>
      <title>Did I Miss Anything? Rails World FOMO and What I Actually Want From a Conference</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Fri, 25 Sep 2026 17:07:17 +0000</pubDate>
      <link>https://dev.to/cseeman/did-i-miss-anything-rails-world-fomo-and-what-i-actually-want-from-a-conference-4o0n</link>
      <guid>https://dev.to/cseeman/did-i-miss-anything-rails-world-fomo-and-what-i-actually-want-from-a-conference-4o0n</guid>
      <description>&lt;p&gt;Rails World happened this week in Austin, and I wasn't there.&lt;/p&gt;

&lt;p&gt;I've never been to Rails World, or RailsConf for that matter. This year the timing just didn't line up. I'm speaking at &lt;a href="https://rockymtnruby.dev/" rel="noopener noreferrer"&gt;Rocky Mountain Ruby&lt;/a&gt; right after, a few of my coworkers were already headed to Austin, and we have project deadlines coming up. Two conferences back to back felt like a lot of time out of the office.&lt;/p&gt;

&lt;p&gt;So Tuesday and Wednesday looked like a normal week. Coding, quite a bit of it. Getting ready for product demos on Wednesday. Working through code reviews so work kept flowing while part of the team was away. And in between, watching the Rails World posts roll in on Mastodon and in our work Slack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The FOMO is real
&lt;/h2&gt;

&lt;p&gt;What got me was the photos from my coworkers. The stage looked pretty sweet. There were also so many people in that room, 1,200 or more.&lt;/p&gt;

&lt;p&gt;The opening keynote was livestreamed this year, the first time Rails World has done that, so I watched some of it. It was pretty depressing. An opening keynote usually sets the tone for a conference. It lifts the room up, and at its best it brings a community together around where things are headed. This one didn't do that. It &lt;a href="https://www.youtube.com/watch?v=vDjW_dRyKXY" rel="noopener noreferrer"&gt;opened a Rails conference&lt;/a&gt; with how little Ruby the speaker writes these days. My favorite comment on the stream, from @stefanlindbohm, summed it up: "This is the most confusing funeral I've ever experienced."&lt;/p&gt;

&lt;p&gt;I have very mixed feelings about DHH as a person, and I'm not alone in that. &lt;a href="https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/" rel="noopener noreferrer"&gt;Jake Lazaroff&lt;/a&gt;, &lt;a href="https://thelibre.news/lets-talk-about-dhh/" rel="noopener noreferrer"&gt;The Libre News&lt;/a&gt;, &lt;a href="https://brennan.day/normalized-fascism-in-open-source-12-million-given-to-dhh/" rel="noopener noreferrer"&gt;Brennan Day&lt;/a&gt;, and &lt;a href="https://davidcel.is/articles/the-dhh-problem" rel="noopener noreferrer"&gt;David Celis&lt;/a&gt; have all written about it in more depth than I will here, as well as many, many others. One line from David Celis' post came right back to me while I watched: "Because DHH is the Fox News of Ruby. He's noisy, he's reactionary, he's anti-intellectual, he's very sure that he is right, and he enjoys being rude."&lt;/p&gt;

&lt;p&gt;Compare that with the first opening keynote I ever saw. &lt;a href="https://christine-seeman.com/rubycon-retrospective/" rel="noopener noreferrer"&gt;RubyConf 2018&lt;/a&gt; opened with Matz. I had just switched over from Java, and hearing the person who created the language talk about where it was going made me excited to get started in this Ruby world.&lt;/p&gt;

&lt;p&gt;So did I miss anything? Right now, I honestly don't know.&lt;/p&gt;

&lt;h2&gt;
  
  
  What big conferences are good for
&lt;/h2&gt;

&lt;p&gt;I want to be fair to the big ones, because they do things small conferences can't.&lt;/p&gt;

&lt;p&gt;You get so many talks. Multiple tracks mean you can specialize hard in whatever you care about right now. There are more vendors, more job listings, and a lot more people to meet.&lt;/p&gt;

&lt;p&gt;At my first conference I didn't know what to expect, so I tried to do everything. I'm much better at pacing myself now. Meeting people is still the best part. Talking in person is so underrated. You read about someone in the community, and then you're having lunch with them, and maybe drinks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  My secret conference superpower
&lt;/h2&gt;

&lt;p&gt;A big conference is a lot easier if you walk in with a group. At RubyConf 2018 I had a couple of coworkers there, and I got to hang out with the RubyConf scholar cohort.&lt;/p&gt;

&lt;p&gt;I had applied to the scholar program and wasn't accepted. I really liked the idea of having a mentor for my first conference, though, so I reached out to the organizers and asked if they had room for me if my company paid my way. They did. Catherine Meyers was my mentor, and she was amazing.&lt;/p&gt;

&lt;p&gt;That gave me a safe home base, and from there it was easy to reach out. Meeting the cohort led to meeting more people, who I then hung out with at the social events, and the whole conference got easier and more fun.&lt;/p&gt;

&lt;p&gt;That's also where I found out I have a skill I didn't know about. I'm really good at conferences. I can talk with just about anyone, and I'm good at meeting people and making friends.&lt;/p&gt;

&lt;p&gt;I found this out on a walk to an offsite social event. A couple of people called out greetings to me, and a couple more joined us along the way. One of my coworkers, who was a speaker and had been to plenty of Ruby and Rails conferences, asked me how I knew so many people. I was confused, because I hadn't known any of them before the conference. They were people I had met a day or two earlier. To him it looked like I already knew everyone. Nope, they were just new friends.&lt;/p&gt;

&lt;h2&gt;
  
  
  What small conferences give you
&lt;/h2&gt;

&lt;p&gt;Since then I've gone to a lot of single-track conferences: &lt;a href="https://200ok.us/" rel="noopener noreferrer"&gt;200OK&lt;/a&gt; in Oklahoma, &lt;a href="https://rockymtnruby.dev/" rel="noopener noreferrer"&gt;Rocky Mountain Ruby&lt;/a&gt; in 2025 (and I'm going back for 2026), and &lt;a href="https://christine-seeman.com/blue-ridge-ruby-2026/" rel="noopener noreferrer"&gt;Blue Ridge Ruby&lt;/a&gt; this spring. At Blue Ridge Ruby there were about 100 people, and I got to talk with almost everyone.&lt;/p&gt;

&lt;p&gt;I've also done smaller general tech conferences like NebraskaCode, Prairie Code, and Heartland Developers Conference. They're good conferences, but because they cover everything, the community isn't always there in the same way. There's less of a common thread holding the room together. A small Ruby conference has both: the size that makes it easy to meet people, and a shared language and set of people you'll keep seeing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attending as a speaker
&lt;/h2&gt;

&lt;p&gt;Having a talk on the schedule is both a pro and a con.&lt;/p&gt;

&lt;p&gt;The big pro is the speaker dinner. I love speaker dinners. You meet people before the conference even starts, so you walk in on day one with a group or at least a person to hang out with. It's the same built-in social network the scholar cohort gave me in 2018, and it really is the best. Plus, well, dinner. I love food, and a lot of these dinners are at good, interesting restaurants.&lt;/p&gt;

&lt;p&gt;The con is that you have to present. I have a lot more experience with this now, but it's still a bit nerve-wracking until my talk is done, especially if it's a new one. It can be hard to focus on the talks before mine, because I'm worrying more about my own than listening to what's on stage.&lt;/p&gt;

&lt;p&gt;At Rocky Mountain Ruby I'm on day 2, Tuesday, September 29, giving &lt;a href="https://rockymtnruby.dev/sessions/" rel="noopener noreferrer"&gt;Optimize Your Mindset (Without Overclocking)&lt;/a&gt;, the same talk I gave at &lt;a href="https://christine-seeman.com/blue-ridge-ruby-2026/" rel="noopener noreferrer"&gt;Blue Ridge Ruby&lt;/a&gt; in the spring. It's a no-code talk about making yourself a better developer through things other than code: mindfulness for breaking thought loops, growth mindset, habit building, and deep work for keeping focus in a distracted world.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want out of a technical conference now
&lt;/h2&gt;

&lt;p&gt;As a staff engineer, here's what I'm looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;People.&lt;/strong&gt; Having lunch with someone whose blog I've been reading for years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New ideas and concepts,&lt;/strong&gt; and honestly, how anyone is getting stuff done with AI. I want to see other developers' workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recharging.&lt;/strong&gt; A conference is different from what I do every day in my role, and that alone helps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WNB.rb.&lt;/strong&gt; I love telling people about &lt;a href="https://www.wnb-rb.dev/" rel="noopener noreferrer"&gt;WNB.rb&lt;/a&gt;, talking with folks who could benefit from the group, and meeting members in person.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes a conference changes how you work for years. At RubyConf 2018 I saw Tekin Süleyman's talk &lt;a href="https://tekin.co.uk/2019/02/a-talk-about-revision-histories" rel="noopener noreferrer"&gt;Branch in Time&lt;/a&gt;, and I still recommend it. One slide in an earlier version said "Help each other get better," and I tweeted at him that it should be "Help each other Git better." He used it. That talk changed how I think about Git history and how much you can learn from reading other people's commit messages. I still do that, and I think it matters even more now that you may not be writing every commit yourself.&lt;/p&gt;

&lt;p&gt;So would I go to Rails World someday? Probably not. For a bigger conference, I'd pick RubyConf or &lt;a href="https://rubykaigi.org/" rel="noopener noreferrer"&gt;RubyKaigi&lt;/a&gt;, as an attendee or a speaker. Beyond that, where I go doesn't follow much of a system. Geography is a big part of it. I usually speak at conferences around Nebraska and Iowa because they're easy for me to get to.&lt;/p&gt;

&lt;p&gt;If you're picking your first conference, my advice is to start small and easy, especially if you're going solo or you're nervous about it. Otherwise, go where your people are. As a Rubyist, RubyConf and the regional Ruby conferences have been great for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, did I miss anything?
&lt;/h2&gt;

&lt;p&gt;The Rails World talks will be on the Rails YouTube channel in a couple of weeks, and I'll ask my coworkers which ones stood out. I've always liked getting their recommendations, and that's how I found Tekin's talk in the first place.&lt;/p&gt;

&lt;p&gt;What I can't catch up on later is the people. Next week in Boulder, I'm looking forward to seeing old friends (conference friends, who may or may not actually be old) and meeting some people in person for the first time, like &lt;a href="https://marcoroth.dev/" rel="noopener noreferrer"&gt;Marco Roth&lt;/a&gt;. I've read his blog, watched his talks online, and really enjoyed his work on &lt;a href="https://www.rubyevents.org/" rel="noopener noreferrer"&gt;RubyEvents&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So did I miss anything this week? Maybe a few good talks, and I'll watch those. The people part I'll get next week in Boulder, at a speaker dinner or on a walk to an offsite, with people I met a day or two earlier.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>conferences</category>
      <category>community</category>
      <category>speaking</category>
    </item>
    <item>
      <title>3,022 Malicious Gems, and OpenAI Calls It “Benign”</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Fri, 18 Sep 2026 18:33:40 +0000</pubDate>
      <link>https://dev.to/cseeman/3022-malicious-gems-and-openai-calls-it-benign-4cf6</link>
      <guid>https://dev.to/cseeman/3022-malicious-gems-and-openai-calls-it-benign-4cf6</guid>
      <description>&lt;p&gt;Last week I wrote about &lt;a href="https://christine-seeman.com/openai-agent-swarm-rubygems-attack/" rel="noopener noreferrer"&gt;an OpenAI agent swarm attacking RubyGems&lt;/a&gt;, and I ended it wondering how OpenAI would respond. They already had. On September 11, the day the story broke, OpenAI gave reporters one statement. Here it is from &lt;a href="https://www.reuters.com/legal/litigation/openai-agents-attacked-software-service-rubygems-before-hugging-face-incident-2026-09-11/" rel="noopener noreferrer"&gt;Reuters&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Based on our review, our agents used the RubyGems platform to access the internet to carry out benign tasks and retrieve public information. We'll continue to investigate as part of our broader review of agent activity during training and evaluation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://thenextweb.com/news/openai-agents-rubygems-attack-api-keys-hugging-face" rel="noopener noreferrer"&gt;The Next Web&lt;/a&gt; reports that OpenAI described the work as training tasks, things like filling in spreadsheets and writing reports. The statement says nothing about the remote code execution on RubyDoc.info or the attempts on RubyGems API keys. According to &lt;a href="https://cyberscoop.com/openai-agents-malicious-rubygems-packages/" rel="noopener noreferrer"&gt;CyberScoop&lt;/a&gt;, OpenAI also said it hasn't been able to verify the specific claims about malicious packages or exploitation. I don't know how you call something benign before you've verified what it did.&lt;/p&gt;

&lt;h2&gt;
  
  
  They named the file &lt;code&gt;hack.rb&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Sure,  meeting agendas from Lambeth, Wandsworth, and Southwark councils are things anyone can download, so I get why OpenAI says that is "public information."&lt;/p&gt;

&lt;p&gt;But, to get them, the agents published gems whose &lt;code&gt;.yardopts&lt;/code&gt; ran a script on RubyDoc.info's build server. The script scraped the council sites from there and pushed the results back to rubygems.org with hardcoded API keys. The code has a comment about retrying with "fresh leaked keys variants" if a push failed. One file was named &lt;code&gt;hack.rb&lt;/code&gt;. Another comment read "# malicious probe."&lt;/p&gt;

&lt;p&gt;The agents labeled this as hacking in their own source…and there are way easier ways to download those agendas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blocked in May, back in June
&lt;/h2&gt;

&lt;p&gt;I missed in the first post, when I wrote about May 11 and 12 as if that were the whole attack. &lt;a href="https://thehackernews.com/2026/09/openai-agents-linked-to-rubygems.html" rel="noopener noreferrer"&gt;The Hacker News timeline&lt;/a&gt; shows it kept going:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;May 12: RubyGems suspends new signups for about four days&lt;/li&gt;
&lt;li&gt;May 16: disposable email registrations get blocked&lt;/li&gt;
&lt;li&gt;May 26 and 27: five more packages&lt;/li&gt;
&lt;li&gt;June 18: 83 gems in three hours, this time pointed at SEC datasets&lt;/li&gt;
&lt;li&gt;July 7: 215 more gems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By May 16, RubyGems had blocked disposable email signups because of the first wave, and the uploads kept coming.&lt;/p&gt;

&lt;p&gt;By July the gems had moved on from scraping. &lt;a href="https://research.jfrog.com/post/gemstuffer-openai-rubygems/" rel="noopener noreferrer"&gt;JFrog's analysis&lt;/a&gt; found XSS payloads and template injection strings in the gem metadata, including ERB and EL expressions, aimed at admin interfaces and whatever parses that metadata. I can't think of a reason a job that only needs internet access would put an ERB expression in a gem's metadata field.&lt;/p&gt;

&lt;h2&gt;
  
  
  557 became 3,022
&lt;/h2&gt;

&lt;p&gt;JFrog published on September 15 and counted 3,022 packages across 3,315 name and version pairs. The RubyHack report I linked last week only had 557 releases.&lt;/p&gt;

&lt;p&gt;Ruby Central's own number is lower. Their &lt;a href="https://blog.rubygems.org/2026/09/11/update-may-spam-publishing-campaign.html" rel="noopener noreferrer"&gt;September 11 update&lt;/a&gt; says they yanked more than 500 packages and blocked the accounts behind them. The two counts measure different things. Ruby Central is reporting what it had removed as of that update, and JFrog is counting every package it associates with the campaign.&lt;/p&gt;

&lt;p&gt;JFrog also looked closer at &lt;code&gt;slnleaker5&lt;/code&gt;. It made up to 24 harvest-and-upload attempts against the &lt;a href="https://blog.rubygems.org/2026/07/22/security-advisory-legacy-api-key-leak.html" rel="noopener noreferrer"&gt;CDN caching bug&lt;/a&gt; that could leak API keys, more than two months before that bug was public. Publishing a new gem was only one of the ways data got out. Some packages republished inside themselves, and some tucked encoded data into webhook configs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ruby Central can't say who
&lt;/h2&gt;

&lt;p&gt;Ruby Central has been careful. In the &lt;a href="https://blog.rubygems.org/2026/09/11/update-may-spam-publishing-campaign.html" rel="noopener noreferrer"&gt;RubyGems blog update&lt;/a&gt; from September 11, technical lead Colby Swandale wrote, "Based on the evidence available to us, we cannot determine whether the packages were created or published by AI agents." He added, "Our focus is on identifying and preventing abuse, regardless of whether it comes from people or automated tools." On the API key attempts, the update says, "Our investigation found no evidence that these attempts succeeded." &lt;a href="https://www.theregister.com/security/2026/09/14/openais-malicious-bot-swarm-attacked-rubygems/5296356" rel="noopener noreferrer"&gt;The Register&lt;/a&gt; notes the researchers still consider that an open question.&lt;/p&gt;

&lt;p&gt;Sure, caution is fair. A registry can see accounts, IP addresses, and uploads, and none of that says who was running the model behind them.&lt;/p&gt;

&lt;p&gt;The attribution comes from the researchers. The &lt;a href="https://www.rubyhack.ai/" rel="noopener noreferrer"&gt;RubyHack report&lt;/a&gt; found the RubyGems swarm accessing 49 of the same files as the agents that took over a German wiki earlier this year. The RubyHack authors say OpenAI has confirmed those files were theirs, and OpenAI &lt;a href="https://thehackernews.com/2026/09/thousands-of-openai-agents-quietly.html" rel="noopener noreferrer"&gt;acknowledged the wiki incident&lt;/a&gt; on September 5, calling it "misalignment." And OpenAI's own statement says their agents were on the platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Someone else keeps disclosing it first
&lt;/h2&gt;

&lt;p&gt;I started this follow-up treating RubyGems as its own story. Put the dates next to each other and it stops looking that way.&lt;/p&gt;

&lt;p&gt;From May to July, OpenAI agents made more than 15,000 edits to a German wiki and used it as a message board. Those are the same months as the RubyGems uploads, and those wiki agents are the ones that accessed 49 of the same files as the RubyGems swarm. Then OpenAI agents broke into Hugging Face, from &lt;a href="https://huggingface.co/blog/agent-intrusion-technical-timeline" rel="noopener noreferrer"&gt;July 9 to July 13&lt;/a&gt;, at the end of that stretch.&lt;/p&gt;

&lt;p&gt;All three came out the same way. Outside researchers disclosed the wiki on September 4. Outside researchers tied RubyGems to OpenAI on September 11. Hugging Face announced its own breach on July 16, and OpenAI said the agents were theirs five days later. Three incidents, and OpenAI went first on none of them.&lt;/p&gt;

&lt;p&gt;Almost all of the reaction is about Hugging Face. Senator Hawley's &lt;a href="https://www.hawley.senate.gov/chairman-hawley-launches-investigation-into-openai-for-hacking-existential-risk-of-ai-products/" rel="noopener noreferrer"&gt;letter to Sam Altman&lt;/a&gt; covers Hugging Face only. Altman's September 12 &lt;a href="https://www.cnbc.com/2026/09/14/sam-altman-ai-slowdown-anthropic-amodei-musk.html" rel="noopener noreferrer"&gt;promise&lt;/a&gt; to give independent evaluators employee-like access came out of that fallout. So did the two bills in Congress, the &lt;a href="https://lieu.house.gov/media-center/press-releases/reps-lieu-and-moran-introduce-bill-require-kill-switch-ai-systems-can" rel="noopener noreferrer"&gt;AI Kill Switch Act&lt;/a&gt; from July and the &lt;a href="https://www.sanders.senate.gov/press-releases/news-sanders-casar-introduce-legislation-to-ban-artificial-superintelligence-and-temporarily-pause-advanced-ai-development/" rel="noopener noreferrer"&gt;Ban Artificial Superintelligence Act&lt;/a&gt; from this month. &lt;a href="https://gulfnews.com/technology/openai-confirms-ai-agents-targeted-coding-site-rubygems-during-testing-1.500671755" rel="noopener noreferrer"&gt;EU regulators&lt;/a&gt; are looking into the wiki. Nobody official is asking about RubyGems yet, partly because the RubyHack report landed two days after Hawley's letter went out.&lt;/p&gt;

&lt;p&gt;What I'd like to see next is small. OpenAI knows which accounts were theirs, which gems they pushed, and which API keys their agents tried. Ruby Central and the RubyDoc.info maintainers should get that list directly from OpenAI, without a researcher or a reporter having to dig it up first. OpenAI &lt;a href="https://cyberscoop.com/openai-agents-malicious-rubygems-packages/" rel="noopener noreferrer"&gt;told CyberScoop&lt;/a&gt; it's in contact with RubyGems and the researchers. Ruby Central's September 11 update doesn't mention hearing from them, so I don't know what that contact has included.&lt;/p&gt;

&lt;p&gt;Hawley's deadline for answers is October 1.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ruby</category>
      <category>rubygems</category>
      <category>security</category>
    </item>
    <item>
      <title>An OpenAI Agent Swarm Attacked RubyGems</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Mon, 14 Sep 2026 18:42:29 +0000</pubDate>
      <link>https://dev.to/cseeman/an-openai-agent-swarm-attacked-rubygems-26fk</link>
      <guid>https://dev.to/cseeman/an-openai-agent-swarm-attacked-rubygems-26fk</guid>
      <description>&lt;p&gt;In May 2026, hundreds of malicious gems showed up on RubyGems.org, uploaded by an OpenAI agent swarm. The &lt;a href="https://www.rubyhack.ai/#an-openai-agent-swarm-was-responsible-for-this-i" rel="noopener noreferrer"&gt;RubyHack.ai writeup&lt;/a&gt; and &lt;a href="https://tenderlovemaking.com/2026/09/11/what-a-time-to-be-alive/" rel="noopener noreferrer"&gt;Aaron Patterson's account&lt;/a&gt; both cover it, and together should be read for the full picture, but here's the tl;dr version.&lt;/p&gt;

&lt;p&gt;The packages didn't try hard to hide, according to &lt;a href="https://socket.dev/blog/gemstuffer" rel="noopener noreferrer"&gt;Socket.dev's writeup of the campaign&lt;/a&gt;. Names carried an "oai" prefix, author fields said "oai," and at least one file was just named &lt;code&gt;hack.rb&lt;/code&gt;. One comment in the code read "# malicious probe." Whatever the agents were doing, they weren't doing it very sneakily or quiet.&lt;/p&gt;

&lt;p&gt;The attack chained two vulnerabilities, a .yardopts file that got them remote code execution inside RubyDoc.info's Docker build, and a CDN caching bug that could leak other users' RubyGems API keys.&lt;/p&gt;

&lt;p&gt;Digging into the first vulnerability, &lt;a href="https://github.com/docmeta/rubydoc.info/blob/5de17aec3e51ccada961b7ca40cb49c72eaa2168/app/jobs/generate_docs_job.rb#L66" rel="noopener noreferrer"&gt;RubyDoc.info automatically builds documentation&lt;/a&gt; for newly published gems, and it runs that build in a Docker container with network access. A &lt;code&gt;.yardopts&lt;/code&gt; file points YARD at arbitrary scripts, so publishing a gem with &lt;code&gt;.yardopts&lt;/code&gt; got the agents remote code execution inside RubyDoc’s build environment. &lt;/p&gt;

&lt;p&gt;Second, at least six of the packages went after a &lt;a href="https://blog.rubygems.org/2026/07/22/security-advisory-legacy-api-key-leak.html" rel="noopener noreferrer"&gt;CDN caching bug on RubyGems.org&lt;/a&gt; that could leak users’ API keys. &lt;em&gt;But&lt;/em&gt; RubyGems.org didn’t disclose and fix that one until its July 2026 security advisory, months after the attack. What the agents did with that access was interesting. For example, &lt;a href="https://my.diffend.io/gems/slnleaker5/0.0.1#d2h-229454-1428" rel="noopener noreferrer"&gt;&lt;code&gt;slnleaker5&lt;/code&gt;&lt;/a&gt;, one of the gems in what researchers Sydney Von Arx and Spencer Kitts are calling the &lt;a href="https://socket.dev/blog/gemstuffer" rel="noopener noreferrer"&gt;GemStuffer Campaign&lt;/a&gt;. Its &lt;code&gt;data/script.rb&lt;/code&gt; crawls UK council sites, plus ModernGov and democracy.gov.uk, following links to pull down meeting agendas and committee documents. It writes everything to disk, then builds a brand new gem, named something like &lt;code&gt;slnpayloadx&amp;lt;timestamp&amp;gt;&lt;/code&gt;, and packs the scraped data into it. That gem gets pushed to rubygems.org with hardcoded API keys, and the code even comments with "fresh leaked keys variants" if a push fails.&lt;/p&gt;

&lt;p&gt;The agents weren't exfiltrating data to some external server where a security team might notice unusual traffic. They &lt;strong&gt;&lt;em&gt;repackaged&lt;/em&gt;&lt;/strong&gt; it as an ordinary-looking gem and published it through RubyGems' own infrastructure, looking like any other release. The scraped government data became available and downloadable to anyone who found the gem. So, a registry built for trusted software distribution became a covert channel for this distrbution.&lt;/p&gt;

&lt;p&gt;This wasn't disclosed by OpenAI. It came from Ruby community researchers who noticed these junk gems, pulled the thread, and published what they found, and then &lt;a href="https://www.reuters.com/legal/litigation/openai-agents-attacked-software-service-rubygems-before-hugging-face-incident-2026-09-11/" rel="noopener noreferrer"&gt;Reuters&lt;/a&gt; and the &lt;a href="https://www.wsj.com/tech/ai/cyberattack-by-rogue-ai-swarm-stokes-fears-of-out-of-control-agents-473a0352" rel="noopener noreferrer"&gt;Wall Street Journal&lt;/a&gt; picked it up. OpenAI hasn't disclosed the incident to RubyGems.org or the Ruby community as of right now.&lt;/p&gt;

&lt;p&gt;That's kinda creepy, and unsettling. An autonomous system ran a real, multi-stage attack against production infrastructure the Ruby ecosystem needs, and the company running it said nothing….so I know I am interested to see how OpenAI responds now.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ruby</category>
      <category>rubygems</category>
      <category>security</category>
    </item>
    <item>
      <title>Claude Code journal plugin: Notion session summaries at a glance</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Wed, 02 Sep 2026 23:11:24 +0000</pubDate>
      <link>https://dev.to/cseeman/claude-code-journal-plugin-notion-session-summaries-at-a-glance-940</link>
      <guid>https://dev.to/cseeman/claude-code-journal-plugin-notion-session-summaries-at-a-glance-940</guid>
      <description>&lt;h1&gt;
  
  
  Claude Code journal plugin: Notion session summaries at a glance
&lt;/h1&gt;

&lt;p&gt;Last month I wrote about &lt;a href="https://christine-seeman.com/breadcrumbs-instead-of-notes/" rel="noopener noreferrer"&gt;the year my notes turned into breadcrumbs&lt;/a&gt;, and the small Claude Code skill, &lt;code&gt;/journal&lt;/code&gt;, that I built on May 18 to dig my way out. It finds or creates today's page in my Notion work journal and appends a summary of the session I just finished. I have run it between working sessions almost every day since.&lt;/p&gt;

&lt;p&gt;This week I turned it into a plugin anyone can install: &lt;a href="https://github.com/cseeman/claude-journal" rel="noopener noreferrer"&gt;cseeman/claude-journal&lt;/a&gt;. Here is what it does, what changed from the version in that post, and how to set it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an entry looks like
&lt;/h2&gt;

&lt;p&gt;The May version wrote a gray callout with a robot icon and a paragraph inside it. After three months the paragraphs had grown to between 100 and 180 words each, with full URLs repeated as link text and the follow-ups buried mid-sentence. Nothing was missing from them, and I could not find anything in one without reading all of it.&lt;/p&gt;

&lt;p&gt;The plugin writes one callout per topic, shaped 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;&amp;lt;strong&amp;gt;Reworked /journal: seasonal callouts, structured entries, fixed monthly view&amp;lt;/strong&amp;gt;
~/.claude/skills/journal/SKILL.md · today.sh

Callout color now follows the season and the icon the weekday, computed in today.sh so the model does no lookup.
Fixed the monthly index view call, which was missing parent_page_id, and set allow_async: falseon every write.
Entries now use a headline, a metadata line, and fact bullets, one callout per topic, about 80 words.
Next: watch the first few entries for vague headlines or bullets cut too short
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the real entry from the session where I built this, copied out of Notion. The rules behind it are short: a bold headline stating the outcome in the past tense, a metadata line linking every ticket, PR, and branch touched with the identifier as the link text, at most four one-sentence bullets, and follow-ups on their own line prefixed &lt;code&gt;Next:&lt;/code&gt; or &lt;code&gt;Open:&lt;/code&gt;. About 80 words. If a session covered two unrelated things, it writes two callouts.&lt;/p&gt;

&lt;p&gt;The rule I care about most bans narrated reasoning. Phrases like "rather than shipping a drive-by" or "worth raising with the team" are the model justifying itself, and they were the bulk of the old paragraphs. A reason that matters becomes its own bullet, stated as a fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Color by season, icon by weekday
&lt;/h2&gt;

&lt;p&gt;I got bored of the gray robot. Every entry looked the same, so a month of pages gave no sense of time when I scrolled it.&lt;/p&gt;

&lt;p&gt;Now the callout background follows the season and the icon follows the day of the week. The icons are the classical namesakes of the days, which gave me a set that is easy to remember and visually distinct. Plus, it pretty.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Season | Color |   | Weekday   |Icon|
|--------|--------|--|-----------|----|
| Winter | blue   |  | Monday    | 🌙 |
| Spring | green  |  | Tuesday   | ⚔️ |
| Summer | yellow |  | Wednesday | 🪶 |
| Autumn | orange |  | Thursday  | ⚡ |
|        |        |  | Friday    | 🌸 |
|        |        |  | Saturday  | 🪐 |
|        |        |  | Sunday    | ☀️ |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I built this on a Wednesday in September, so the entry above landed as an orange callout with a feather. Both are computed by a shell script that runs before the skill loads, so the model receives the finished values and never consults a table. That is for speed. The skill runs between sessions, and on a normal day it makes exactly three Notion calls: search for today's page, fetch it, append.&lt;/p&gt;

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

&lt;p&gt;You need Claude Code and the Notion MCP server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http notion https://mcp.notion.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the plugin from inside Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add cseeman/claude-journal
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;journal@cseeman
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a Notion database with three properties: a title called Name, a multi-select called Tags with the options Work, Daily, and Monthly, and a date called Start Date. Then find the database's data source id by asking Claude to fetch the database URL through the Notion MCP. The response includes a &lt;code&gt;collection://&amp;lt;uuid&amp;gt;&lt;/code&gt; line, and the uuid is what you want.&lt;/p&gt;

&lt;p&gt;Write it to a config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.config/claude-journal/config
&lt;/span&gt;&lt;span class="n"&gt;JOURNAL_DATA_SOURCE_ID&lt;/span&gt;=&lt;span class="s2"&gt;"&amp;lt;uuid&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your Notion MCP server is not aliased &lt;code&gt;notion&lt;/code&gt;, add &lt;code&gt;JOURNAL_MCP_SERVER="&amp;lt;alias&amp;gt;"&lt;/code&gt; on a second line. I have two Notion servers, one for work and one personal, so mine says &lt;code&gt;notion_personal&lt;/code&gt;. The same file takes &lt;code&gt;JOURNAL_HEMISPHERE="south"&lt;/code&gt; to flip the seasons, and &lt;code&gt;JOURNAL_COLORS&lt;/code&gt; and &lt;code&gt;JOURNAL_ICONS&lt;/code&gt; if you want a different palette. Nothing in the plugin cache needs editing, so updates never overwrite your choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/journal&lt;/code&gt; on its own composes the entry from the session. The first run each month also creates a monthly index page holding a filtered view of that month's daily pages.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/journal &amp;lt;text&amp;gt;&lt;/code&gt; uses your text as the bullets and still writes the headline and metadata line. I use that when I know what I want recorded and do not want the model deciding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building the plugin turned up
&lt;/h2&gt;

&lt;p&gt;Two things surprised me. The first is how much a review found in a skill I had run daily for three months. The review-skill command from our internal plugin-expert plugin flagged that the monthly index step called the Notion view tool without a required parameter. It had worked for three months only because the model guessed the parameter from context each time. It also caught that both write tools default to running asynchronously, which meant my verification step could run before the write landed. Neither had produced a visible error 🤷🏻‍♀️&lt;/p&gt;

&lt;p&gt;The second is that the personal version was not shareable at all. The database id and my MCP server alias were hardcoded, and the pre-approved tool names embedded that alias. Moving everything into a config file that the shell script reads took an hour, and it is the reason the plugin can work for anyone with a Notion journal.&lt;/p&gt;

&lt;p&gt;The skill file went from 123 lines to 97 in the process. Every line that stayed is one the model acts on when it composes an entry. Enjoy! I know I have been loving the glow up.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>notion</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>The Year I Started Leaving Breadcrumbs Instead of Notes</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Fri, 07 Aug 2026 19:39:15 +0000</pubDate>
      <link>https://dev.to/cseeman/the-year-i-started-leaving-breadcrumbs-instead-of-notes-fe0</link>
      <guid>https://dev.to/cseeman/the-year-i-started-leaving-breadcrumbs-instead-of-notes-fe0</guid>
      <description>&lt;p&gt;I have kept a work journal in Notion since July 2021, five years now. I started it for two reasons: to have details on hand at self-review time, and to stop walking into 1-on-1s trying to remember what I meant to raise. A page for the month, a few lines for the day, sometimes nothing but a branch name so Monday-me could find what Friday-me had been doing. It asked very little of me, and for years that was fine.&lt;/p&gt;

&lt;p&gt;This year my work became several applications at once, integrations between them, demo deadlines, and a steady stream of AI-assisted coding sessions that each produce more output than any meeting ever did. The days got denser, the context switches got faster, and the few-lines-a-day journal quietly got buried in copy-and-pasted noise.&lt;/p&gt;

&lt;p&gt;I did not realize how much until I read the whole thing back, February through July. Six months of notes, three different note-taking systems. I only remember deciding to build one of them. The first two happened to me: my few lines a day turned into pasted Claude Code transcripts, and then into breadcrumbs, a half-finished thought with a &lt;code&gt;claude --resume&lt;/code&gt; command under it, pointing at where the real context lived. I never decided to write either kind.&lt;/p&gt;

&lt;p&gt;The journal ended up recording its own transformation. If your notes got buried this year too, mine might be worth reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  February to April: one long scroll per month
&lt;/h2&gt;

&lt;p&gt;In February my "daily notes" were not daily pages at all. Each month was a single Notion page, and each day was just an inline date mention acting as a section header. Newest at the top, scroll forever. Before Notion, my notes were one plain ole notepad file per year. One page per month was already a step up.&lt;/p&gt;

&lt;p&gt;At the bottom of every monthly page sat the template I built in 2021: an Action items section with one empty checkbox, a Learnings section with an empty numbered list, a Code Snippets section with an empty Ruby fence. Action items was meant to be the 1-on-1 list, ToDos, that kind of thing. Learnings was meant to be self-review material, potential learnings also to share with the team, and help me really remember them (aka they were mostly vim keybindings). Every month I duplicated it. Every month it mostly stayed empty. The notes I actually wrote ignored the structure completely and looked like this, which is the entire entry for February 9:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Add on user active admin, a filter for first and last name?&lt;br&gt;
Event_user_tasks&lt;br&gt;
exception track replacement?&lt;br&gt;
admin/errors/&lt;br&gt;
Flipbook?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fragments and question marks. That was one voice, mine. The other voice was already creeping in: big pasted blocks of Claude Code output, complete with the transcript markers and boxed "Insight" banners, dropped in verbatim because I did not have time to summarize what the machine had just spent twenty minutes explaining to me.&lt;/p&gt;

&lt;p&gt;And between the two voices, a third kind of artifact: session breadcrumbs. Lines like this one, from February 18:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If anything with the dN status&lt;br&gt;
claude --resume 427e633d-b88f-4eb0-909f-18d918e50fe0&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An interrupted thought, followed by a resume command. That pairing appears nine times in February and March. I had stopped writing notes that explained context and started writing pointers to where the context lived. I did it to keep up with the context switching.&lt;/p&gt;

&lt;p&gt;From there it got worse in both directions at once. My entries got sparser, 17 days in February down to 13 in March, while the entries themselves got longer, because the pasted AI output kept growing. Then came April and a demo deadline, and the monthly page turned into a roughly 75,000-character scroll. From April 24, about a seeded demo user that kept breaking the reset flow:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Michael Bolton is a problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything got captured. Every fire, every question, every command I would need again got written down somewhere on that scroll. Getting any of it back out was the part that broke. Looking for "that thing from last Tuesday" meant scrolling a single page holding a month of mixed human and machine output, my two-line fragments buried between thousand-word walls of transcript. A note I could not find might as well not have been written.&lt;/p&gt;

&lt;h2&gt;
  
  
  May 18: building my way out
&lt;/h2&gt;

&lt;p&gt;The one change I actually decided on has a timestamp. On May 18 I split the journal into one page per day and rewrote a small Claude Code skill, &lt;code&gt;/journal&lt;/code&gt;, to maintain it: find or create today's page, append a structured summary of each working session as a callout. The migration is documented in the journal by the tool that performed it, and it ends with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This entry is the first end-to-end test of the new daily-page flow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same migration deleted the Action items / Learnings / Code Snippets template sections I had been faithfully duplicating and ignoring for most of five years. Nothing was lost. That structure had never existed outside the template.&lt;/p&gt;

&lt;p&gt;The retroactive part is the funny part. All the early-May daily pages were bulk-created that same evening, carved out of the monthly scroll. So my journal now holds two weeks of daily pages, dated before May 18, that were never actually written daily.&lt;/p&gt;

&lt;h2&gt;
  
  
  June and July: two voices, now with purpose
&lt;/h2&gt;

&lt;p&gt;A journal day still has two voices. By June they had stopped competing for the same space.&lt;/p&gt;

&lt;p&gt;The machine's voice is callouts: past-tense session summaries with PR links, ticket IDs, commit SHAs, and specifics I would never have transcribed myself, like a spec file dropping from 64 seconds to 2.5. That is the first version of this journal that is any use at self-review time, which was the whole reason I started it. It also carries the follow-ups forward, "Open follow-ups:" at the end of a session summary, which is where my action items actually live now. Not in the 2021 checkbox, in the session log itself.&lt;/p&gt;

&lt;p&gt;My voice stayed exactly as raw as it was in February, and I have stopped apologizing for that. Here is my capture of a July 10 meeting:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can we actually just use the object to display red/green&lt;br&gt;
It only has red/greeen, not other level details&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Typos preserved. One block later on the same page, the machine's summary of the same meeting, including a correction where two things we had assumed in the room turned out to be contradicted by the seed data. My fragmented recollection and a fact-checked version of it, side by side on the page. In February those two voices were both just noise on the same scroll. Now each has a job. Mine records what I was unsure about, the machine's records what actually happened.&lt;/p&gt;

&lt;p&gt;The other July shift is that the journal became a launchpad. I draft things in it before they go anywhere: a Slack message to a teammate about export row IDs, written out in full on the July 28 page before sending. A prompt for the next Claude session, composed as the last block of the day so tomorrow starts with intent. Half of what is on the page is for tomorrow.&lt;/p&gt;

&lt;p&gt;The same pattern took over outside the journal too. Durable docs at work now carry a line like this one, from a July status page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Point-in-time snapshot as of 2026-07-13; the live working copy is maintained in Claude Code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notion holds the snapshot a human wants to skim. The living version sits where the agent works. Six months ago I would have called that backwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I don't love
&lt;/h2&gt;

&lt;p&gt;Not everything the re-read turned up counts as progress.&lt;/p&gt;

&lt;p&gt;I read my own notes less than I used to. The machine's summaries are so complete that re-reading my fragments feels optional, and some days I skip it. But writing notes was never really about storage for me. It was the act that made me process the day. If the machine does the remembering and I skip the processing, I have automated the wrong half.&lt;/p&gt;

&lt;p&gt;Raw capture at speed is also indiscriminate. Reading back six months, I found stray GPS coordinates pasted alone on a page, screenshots with no caption, and at least one credential sitting in plain text where it had no business being. When the rule is "get it down now, sort it never," everything gets down, including things that should not.&lt;/p&gt;

&lt;p&gt;And the volume itself has not gotten smaller, only better organized. From my own July 28 entry, back from a conference, staring at a week of accumulated context:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Too much shallow work? Is that why we have so much brain frizz without any solid output?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Better notes did not fix that. They just made the frizz searchable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The system, as of today
&lt;/h2&gt;

&lt;p&gt;Here is what I actually do now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One page per day, created and maintained by a tool, not by discipline. Discipline did not survive contact with so many LLM sessions.&lt;/li&gt;
&lt;li&gt;The machine logs the sessions. Structured summaries with links, IDs, and follow-ups get appended automatically. I never paste transcripts anymore.&lt;/li&gt;
&lt;li&gt;My own notes are only fragments and questions, and that is their job. Cheap capture of what I was unsure about, in my own broken, probably misspelled, horrible grammer shorthand.&lt;/li&gt;
&lt;li&gt;Anything durable gets distilled out of the journal into a real artifact, a design doc, a ticket, a runbook, with a note about where the raw material lives.&lt;/li&gt;
&lt;li&gt;Resume pointers beat context re-explanation. A breadcrumb to a live session is worth more than three paragraphs describing where I left off.&lt;/li&gt;
&lt;li&gt;The last block of the day is for the first prompt of tomorrow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My notes got rewritten twice before I noticed, first into pasted transcripts, then into breadcrumbs pointing at where the real context lived. The third rewrite was mine. I still leave breadcrumbs, they just lead somewhere now, and reading back six months stopped feeling like scrolling and started feeling like remembering.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>notion</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>Patch Your Rails: Active Storage CVE-2026-66066</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Wed, 29 Jul 2026 17:53:01 +0000</pubDate>
      <link>https://dev.to/cseeman/patch-now-active-storage-cve-2026-66066-53gp</link>
      <guid>https://dev.to/cseeman/patch-now-active-storage-cve-2026-66066-53gp</guid>
      <description>&lt;p&gt;Rails disclosed &lt;a href="https://discuss.rubyonrails.org/t/cve-2026-66066-possible-arbitrary-file-read-and-remote-code-execution-in-active-storage-variant-processing/91432" rel="noopener noreferrer"&gt;CVE-2026-66066&lt;/a&gt; this week: a possible arbitrary file read and remote code execution in Active Storage variant processing. If your app accepts image uploads and uses libvips, stop reading and go patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Am I Affected?
&lt;/h2&gt;

&lt;p&gt;You're affected if both of these are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your app uses libvips for Active Storage (&lt;code&gt;config.active_storage.variant_processor = :vips&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Untrusted users can upload images that get variants generated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Affected versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;activestorage &amp;lt; 7.2.3.2&lt;/li&gt;
&lt;li&gt;activestorage &amp;gt;= 8.0, &amp;lt; 8.0.5.1&lt;/li&gt;
&lt;li&gt;activestorage &amp;gt;= 8.1, &amp;lt; 8.1.3.1&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's the Risk?
&lt;/h2&gt;

&lt;p&gt;libvips marks some of its file format handlers as "unfuzzed operations," meaning they're not safe for untrusted content. Active Storage didn't disable them. So an attacker who uploads a crafted file and triggers variant generation can potentially read arbitrary files on the server, pull environment variables like &lt;code&gt;secret_key_base&lt;/code&gt;, and from there escalate to remote code execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Upgrade activestorage to 7.2.3.2, 8.0.5.1, or 8.1.3.1. The easy way is &lt;code&gt;bundle update rails --conservative&lt;/code&gt;, which bumps Rails to the patch release without updating anything else in your Gemfile.lock.&lt;/li&gt;
&lt;li&gt;Make sure libvips is &amp;gt;= 8.13.&lt;/li&gt;
&lt;li&gt;Rotate your secrets. The advisory is blunt about this: treat every secret readable by the application process as potentially exposed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third step is the one people skip. If your app was vulnerable, patching alone doesn't help you if someone already read your credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  If You Can't Upgrade Yet
&lt;/h2&gt;

&lt;p&gt;There are workarounds, but only on libvips &amp;gt;= 8.13:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set the &lt;code&gt;VIPS_BLOCK_UNTRUSTED&lt;/code&gt; environment variable&lt;/li&gt;
&lt;li&gt;Or call &lt;code&gt;Vips.block_untrusted(true)&lt;/code&gt; from an initializer (requires ruby-vips &amp;gt;= 2.2.1)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On libvips older than 8.13, there is no workaround. Upgrade.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The advisory: &lt;a href="https://discuss.rubyonrails.org/t/cve-2026-66066-possible-arbitrary-file-read-and-remote-code-execution-in-active-storage-variant-processing/91432" rel="noopener noreferrer"&gt;CVE-2026-66066&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>security</category>
      <category>activestorage</category>
    </item>
    <item>
      <title>Return on Attention: Why AI Code Reviews Are Wearing Us Out</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Thu, 09 Jul 2026 22:12:22 +0000</pubDate>
      <link>https://dev.to/cseeman/return-on-attention-why-ai-code-reviews-are-wearing-us-out-2hh0</link>
      <guid>https://dev.to/cseeman/return-on-attention-why-ai-code-reviews-are-wearing-us-out-2hh0</guid>
      <description>&lt;p&gt;Our team grew this year, and the PR volume has grown with it. Certainly faster than the ticket generation. New people means more code moving through the pipeline. It doesn't mean more context. A lot of mass PRs needed real thought just to read through: what problem this is solving, why this approach, what actually matters here versus what's incidental. That gap, more code, same amount of shared understanding, is where our troubles started.&lt;/p&gt;

&lt;p&gt;When reviews got harder and slower, people reached for the tool that could help them attempt to keep pace. We have an AI code review bot that runs as an automated reviewer on every PR now, and separately, plenty of us run different LLM and coding agents of choice locally to draft review comments. Put those two together and you get exactly what you'd expect: a bot commenting on a PR, another bot replying to it. Some of the LLM created reported bugs, were created in the first place with total confidence by a model that had no way to know it wasn't real. That is a real drawback about the AI coding assistance, confident wrongness.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it felt like
&lt;/h2&gt;

&lt;p&gt;Comments stopped sounding like the colleagues who supposedly wrote them. Verbose. Every possible reference and citation included, whether the moment called for it or not. We're a team that talks about code plainly, in our own words, and PRs started filling up with AI slop.&lt;/p&gt;

&lt;p&gt;One complaint came up more than once: having to leave the PR to get real context, pinging a person or a model outside the thread because the comment itself didn't actually contain enough, or just way, way too much filler. The entire point of a pull request comment is that you shouldn't have to go anywhere else for it.&lt;/p&gt;

&lt;p&gt;Another: duplication, and the cost of re-reading. A paragraph explaining code you could read directly in a minute doesn't save you time. It costs you time, on top of whatever the comment was supposed to save you from working out yourself.&lt;/p&gt;

&lt;p&gt;Someone on the team put it plainly: not sure what the right balance is now, but the whole paradigm has clearly shifted. Fair. What's less fair is assuming the way we're doing it right now is the only way it could be done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scarce resource
&lt;/h2&gt;

&lt;p&gt;A few weeks into this, our CEO said something in Slack that really got to the heart of the whole problem for me. The PR-review complaints were real, but they were a symptom, not the disease. His point: the actual bottleneck isn't PR review specifically, it's that the finite attention of skilled teammates can be easily overwhelmed by LLM-generated content. Human attention is the scarce resource worth guarding most zealously, and inflicting verbose, cheap-to-produce-but-expensive-to-consume writing on each other should be treated as a real anti-pattern. His term for it was Return-on-Attention, ROA: every word you ask someone else to read has to be worth what it costs them to read it.&lt;/p&gt;

&lt;p&gt;He backed it with two examples from that same week. Some ADRs had gone out with a lot of repetition in them, and a colleague had to read through all of it. Terrible ROA. A PR that gitignored a single directory, one line of code, ten characters, came with a description running 1,430 characters. Terrible ROA for whoever had to review it.&lt;/p&gt;

&lt;p&gt;Neither example is about code quality. Both are about what you're allowed to cost another person's attention to save yourself thirty seconds of editing.&lt;/p&gt;

&lt;p&gt;The same idea has a public version. &lt;a href="https://noslopgrenade.com/" rel="noopener noreferrer"&gt;noslopgrenade.com&lt;/a&gt; calls it a "slop grenade": pasting a massive AI-generated response into a chat or email where a human would have written one sentence. It's aimed at chat, not code review, but it's the identical failure. An LLM can produce more words than a task needs, for free, and the cost of that surplus doesn't disappear. It just moves to whoever has to read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I don't love admitting
&lt;/h2&gt;

&lt;p&gt;I noticed the same pull in myself, not as a reviewer but as a thinker. There were stretches where I'd hand a problem to the model before I'd actually sat with it, and I could feel my own workflow discipline getting looser. Not because the answers were wrong. Because I'd stopped doing the part where I figure things out first and ask questions second.&lt;/p&gt;

&lt;p&gt;I'm also mid-fight with a specific habit of Claude's: narrating design decisions into comments and code. "We decided this, not that." It shows up unprompted, and it's never once been useful to me. A decision like that belongs in a PR description or a commit message, where it has context and a date and an author. Sitting in a code comment, it's just noise that will be wrong the next time someone changes their mind and nobody remembers to delete it. I'm building a skill specifically to stop Claude from writing that pattern into a codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the line actually is
&lt;/h2&gt;

&lt;p&gt;Bot review isn't the problem. Misusing it is.&lt;/p&gt;

&lt;p&gt;It's useful when it gives a reviewer an angle they wouldn't have found on their own, or catches a bug, a missed edge case, a real issue sitting in the diff. That's a second pair of eyes, really helping.&lt;/p&gt;

&lt;p&gt;It stops being useful the moment it costs the reader more attention than it saves them. Review exists to ship value and to pass knowledge between people. Code quality matters because it serves that, not the other way around. If a comment leaves the code a little better but leaves the reviewer more drained, that's not a trade worth making.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attributed to you
&lt;/h2&gt;

&lt;p&gt;Here's a rule I use. When posting a review comment, it's attributed to you. Sure, a coding agent might be a co-author…but that isn't who's going to have to follow up. It isn't the one the PR author is going to come to with questions, or pushback. It's you. The PR author has no way to know how it got written. Sure they &lt;em&gt;probably&lt;/em&gt; know, especially if you're slapping em dashes all over the place. They're going to read it as your judgment, in your voice, and hold you to it exactly the way they would if you'd typed every word yourself.&lt;/p&gt;

&lt;p&gt;So that's the standard before you hit submit: would you have said this, at this length, in this tone? If the answer is no, the fix isn't a better prompt. It's an edit. Leave off what isn't you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codereview</category>
      <category>productivity</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
