<?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>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>
    <item>
      <title>What Prime Day Taught Me About Prompt Engineering</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Mon, 22 Jun 2026 18:26:49 +0000</pubDate>
      <link>https://dev.to/cseeman/what-prime-day-taught-me-about-prompt-engineering-3gek</link>
      <guid>https://dev.to/cseeman/what-prime-day-taught-me-about-prompt-engineering-3gek</guid>
      <description>&lt;p&gt;I wanted to get better at prompt engineering. Not the trick-the-robot kind, the boring-but-useful kind: how to ask a model a question so you get an answer you can actually trust.&lt;/p&gt;

&lt;p&gt;The trouble with practicing is that most tutorials use made-up examples, and it's hard to tell a good answer from a bad one when you don't care about the topic. So I practiced on something I did care about: the deals sitting in my Amazon cart. I had a vacuum I'd been eyeing and a hair styler that was "43% off," and I genuinely wanted to know if those were good prices or just good marketing. The stakes were real, actual money on an actual decision, and that's what made it a good drill. A vague prompt gives you a confident answer, and when you actually care, you can feel that the answer is hollow.&lt;/p&gt;

&lt;p&gt;What I learned, with the real deals and the actual before-and-after prompts:&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap hiding in every deal
&lt;/h2&gt;

&lt;p&gt;Start with the hair styler. The listing said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Shark FlexStyle. Limited time deal. $199.00, 43% savings. List Price: $349.99.&lt;/p&gt;
&lt;/blockquote&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%2F4xc8sbkdmavde1kqucj1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4xc8sbkdmavde1kqucj1.png" alt="Amazon listing for the Shark FlexStyle showing $199.00 marked 43% off a $349.99 list price" width="544" height="1110"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My first instinct was the prompt most people write:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Shark FlexStyle $199, 43% off list $349.99, is that a good deal?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This feels reasonable. It is also nearly useless: it lets the model answer the &lt;em&gt;easy&lt;/em&gt; question (is 43% off a big discount? sure!) instead of the &lt;em&gt;real&lt;/em&gt; one (is $199 actually a good price?). That $349.99 list price is a marketing anchor. A lazy prompt accepts it, and so you get a lazy "yes, great deal!" back.&lt;/p&gt;

&lt;p&gt;The fix was re-framing this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Act as a pricing analyst. I don't care whether $199 looks like a discount off list. I care whether $199 is a genuinely good price for the Shark FlexStyle right now. Before concluding, work through: (1) the actual street price over the last 6-12 months, (2) how often it drops to or below $199, (3) the real discount vs. its typical selling price, not vs. list. Cite a source and date for each price, or mark it unverified.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same question, completely different answer. What the assistant came back with, in its own telling:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;$199 is a genuinely good price, but not for the reason Shark markets it. It's the 6-month floor and has only appeared a couple of times in that window. The more honest benchmark is its recent median price, and $199 is a real savings off &lt;em&gt;that&lt;/em&gt;, not the headline savings off the inflated $349.99 list.&lt;/p&gt;
&lt;/blockquote&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%2Fbpncjr9g07grvway1mnf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbpncjr9g07grvway1mnf.png" alt="The honest discount vs. street price is ~20% below median, and ~13% below its most common " width="800" height="1316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm taking those specifics on faith, the assistant reported them and I didn't independently verify its price history, but that's almost beside the point. What changed was the &lt;em&gt;shape&lt;/em&gt; of the answer: instead of "43% off, great deal!" I got "$199 is the all-time low on Amazon - only hit twice in 180 days, and the previous touch was one day only" The only thing I did differently was tell the model to ignore the anchor and compare to street price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson one: reframe the question to attack the anchor.&lt;/strong&gt; "Compare to street price, not list" is the line I am going to find more helpful going forward. It applies to nearly every "is this a good deal" question you'll ever ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  When "better" has no meaning yet
&lt;/h2&gt;

&lt;p&gt;Next I tried a comparison, which is where vague prompts fall apart:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dyson V8 Cordless: $269.99, 31% off list $389.99.&lt;/p&gt;

&lt;p&gt;Shark IZ363HT with HEPA filter: $199.99, 43% off list $349.99.&lt;/p&gt;
&lt;/blockquote&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%2F1101l5wds6urcygqy4z2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1101l5wds6urcygqy4z2.png" alt="Compare with similar" width="800" height="1200"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Just the plan old compare with similar on Alexa for Shopping on Amazon&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The trap is right there in the numbers. The Shark is 43% off and cheaper, so a lazy prompt crowns it the winner in one line. But "better deal" is meaningless until you say &lt;em&gt;better for what&lt;/em&gt;. A comparison has no answer until you define the criteria.&lt;/p&gt;

&lt;p&gt;The prompt I landed on added three new moves on top of the anchor break:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Act as a product and pricing analyst. Don't let a bigger discount percentage decide the winner. Compare the Dyson V8 ($269.99) and Shark IZ363HT ($199.99) as a purchase. Ignore list-price discounts; compare each to its real street price. Flag if they're different product classes. Score on weighted criteria: price-vs-street 30%, cleaning 25%, runtime 20%, HEPA 15%, upkeep 10%. Then answer two things separately: better DEAL, and better PRODUCT. Cite source and date per price or mark unverified.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three things made this work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Weighted criteria.&lt;/strong&gt; "Better" means nothing until you declare what matters and how much. Writing the weights forced me to admit what I actually cared about.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Split the deal from the product.&lt;/strong&gt; A thing can be a great deal &lt;em&gt;and&lt;/em&gt; the wrong buy. Keeping those two questions separate clears up most of the confusion in a comparison.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flag the class mismatch.&lt;/strong&gt; These two vacuums aren't the same tier, and I wanted the model to say so instead of comparing raw prices.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result split exactly the way good analysis should (again, these are the assistant's figures, which I didn't independently confirm):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Better DEAL: the Shark at $199.99, a genuine discount off its typical street price. The Dyson's deal is real but based on thin historical data. Better PRODUCT: the Dyson V8, for cleaner engineering and easier maintenance, though the Shark's HEPA seal is a meaningful win for a cat household.&lt;/p&gt;
&lt;/blockquote&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%2F7zy6yfgi4o06c5yp19kc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7zy6yfgi4o06c5yp19kc.png" width="800" height="1436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One prompt, two different winners. A single "which is better?" would have collapsed that into one arbitrary answer and I'd have learned nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson two: in any comparison, the quality of the answer is capped by whether you defined the criteria.&lt;/strong&gt; The model can't tell you "better" until you tell it "better how."&lt;/p&gt;

&lt;h2&gt;
  
  
  The model is smarter, not infallible
&lt;/h2&gt;

&lt;p&gt;The part that surprised me most came in that same comparison answer, which also contained this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The $130 gap vs. the Dyson is real money…&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and later:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;…are comfortable paying $70 more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both can't be true. The actual gap is $269.99 minus $199.99, which is &lt;strong&gt;$70&lt;/strong&gt;. The $130 was wrong, and the model contradicted itself within the same answer. A well-engineered prompt made the reasoning much better. It did not make the arithmetic correct.&lt;/p&gt;

&lt;p&gt;The fix is one more constraint: &lt;em&gt;"show the price-difference math explicitly."&lt;/em&gt; Errors hide in summary claims but not in shown work. If I'd forced it to write &lt;code&gt;$269.99 - $199.99 = $70&lt;/code&gt;, the contradiction would have been impossible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson three: a better prompt improves reasoning, not truth. Always verify the model's math.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint I should've expected: word count
&lt;/h2&gt;

&lt;p&gt;When I copied that comparison prompt into the Amazon shopping assistant, only the first half pasted. There was a character limit, and my nicely-formatted prompt with its ROLE and TASK and OUTPUT headers got cut off mid-sentence.&lt;/p&gt;

&lt;p&gt;The annoying part is that nothing told me the limit existed until I hit it. These assistants rarely document it, so you find the ceiling by bumping your head on it. That changes the strategy: since you can't plan around a number you don't know, put the instructions that change the answer &lt;em&gt;first&lt;/em&gt;, so if you do get truncated, you lose the formatting and not the substance.&lt;/p&gt;

&lt;p&gt;That accident taught me something useful. Under a tight budget, you rank techniques by how much each word buys you and cut the rest. The headers and scaffolding? First to go, the model infers structure on its own. What you protect, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The anchor break (street price, not list)&lt;/li&gt;
&lt;li&gt;The deal-vs-product split&lt;/li&gt;
&lt;li&gt;The weighted criteria (compressed to inline shorthand)&lt;/li&gt;
&lt;li&gt;The cite-or-unverified rule&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The compressed version was a quarter the length and carried every essential instruction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Compare as a purchase: Dyson V8 $269.99 vs Shark IZ363HT (HEPA) $199.99. Ignore list-price discounts; compare each to its real street price. Note if they're different product classes. Score on: price-vs-street 30%, cleaning 25%, runtime 20%, HEPA 15%, upkeep 10%. Answer two things separately: better DEAL, and better PRODUCT. Cite source+date per price or mark unverified.&lt;/p&gt;
&lt;/blockquote&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%2F89ccxrjjt8ggc2s1g5c6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89ccxrjjt8ggc2s1g5c6.png" alt="Shopping assistant's detailed reply with a price-vs-street table, 12-month price history chart, and a buy/wait verdict" width="800" height="1429"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Now with more details that you care about in the prompt. The engineered prompt, with the price-history table and chart it produced.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson four: a prompt's power lives in its constraints and reframes, not its formatting.&lt;/strong&gt; Headers are a readability luxury. The instructions that change the &lt;em&gt;answer&lt;/em&gt; are the ones you never cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same question, side by side
&lt;/h2&gt;

&lt;p&gt;This is the comparison I'd point a skeptic to. I ran a third product through both kinds of prompt so you can see the difference in one frame: the &lt;strong&gt;Garmin epix Pro (Gen 2) Sapphire Edition, 51mm&lt;/strong&gt;. A premium watch with a real list price north of $1,000, which makes the anchor problem even more tempting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without prompt engineering&lt;/strong&gt;, you write what feels natural:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is the Garmin epix Pro (Gen 2) Sapphire 51mm a good deal right now?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And you get back something that sounds authoritative and tells some but maybe not all you want:&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%2Fxjx4lwxlcyh59lqww6ue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxjx4lwxlcyh59lqww6ue.png" width="800" height="905"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The vague prompt, the vague(ish) answer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Notice what that answer does: it accepts the discount at face value, and uses list price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With prompt engineering&lt;/strong&gt;, you spend the reframes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Act as a pricing analyst. I care whether the current price of the Garmin epix Pro (Gen 2) Sapphire 51mm is genuinely good, not whether it looks discounted off list. Before concluding: (1) find its street/historical price over the last 6-12 months, (2) note how often it hits the current price or lower, (3) state the real discount vs. typical selling price, not list. Cite source and date per price or mark unverified. Then give a verdict: buy / wait / skip, and a "wait-for" target price if I should hold out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;em&gt;shape&lt;/em&gt; of what comes back changes entirely. Instead of a sales pitch, you get a verdict with the reasoning shown:&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%2Fwynfsk6qutgnem7dt51h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwynfsk6qutgnem7dt51h.png" width="800" height="1488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With some real, actionable information and price history in context:&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%2F4pjsdyedxgekwvlgk55d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4pjsdyedxgekwvlgk55d.png" alt="12-month median: ~$624.90. The current $549.99 is a real ~$75" width="800" height="1410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is a decision, with the math shown and the uncertainty labeled. One answer is a brochure, the other tells you exactly how good the price is, how often it appears, and what to hold out for. The difference isn't the model. It's the four or five sentences of reframing you put in front of it.&lt;/p&gt;

&lt;p&gt;And one last thing, the part I didn't expect. The &lt;em&gt;first&lt;/em&gt; time I ran this, the assistant answered from only about 40 days of data and called it a ~$50 saving. The phrase "over the last 6-12 months" in my prompt is what made it go back, pull a full year of price history, and correct itself to the ~$75 figure with a proper wait-for target. The reframe didn't just change the format of the answer. It changed how hard the model looked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheat-sheet
&lt;/h2&gt;

&lt;p&gt;The whole thing distilled, for when you want to try this yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The five core techniques&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Role for clarity, not theater.&lt;/strong&gt; Set a role only when it changes what good output looks like, and say why. "Act as a pricing analyst; I care about real price history over marketing claims."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Explicit output structure.&lt;/strong&gt; Tell it the shape: a table, then a verdict, then a confidence rating. Structure is the highest-leverage habit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Few-shot examples.&lt;/strong&gt; Show one example of the answer you want. It pins down tone and granularity better than any description.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reason before verdict.&lt;/strong&gt; Ask for the work first, conclusion last. Evidence-first ordering kills confident-but-wrong answers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Constraints, especially for uncertainty.&lt;/strong&gt; "If you can't verify it, say unverified rather than guessing." The uncertainty clause is the one people skip, and it's what prevents fabrication.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;For comparisons, add&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define and weight your criteria. No criteria, no real answer.&lt;/li&gt;
&lt;li&gt;Split "best deal" from "best product." Don't let a steeper discount pick the winner.&lt;/li&gt;
&lt;li&gt;Force the math to be shown so errors can't hide in summaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For deals specifically&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Break the anchor: compare to street price, never to list price.&lt;/li&gt;
&lt;li&gt;Ask how &lt;em&gt;often&lt;/em&gt; the price appears, not just how low it is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Meta-habits&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Specificity beats politeness. The model can't read intent you didn't state.&lt;/li&gt;
&lt;li&gt;Prompt engineering is debugging, not authorship. Run it, find the specific failure, fix that one thing.&lt;/li&gt;
&lt;li&gt;Watch which constraints the model quietly drops, then promote those to non-negotiable on the next pass.&lt;/li&gt;
&lt;li&gt;Under a word limit, cut formatting first and reframes last.&lt;/li&gt;
&lt;li&gt;Name the scope. A phrase like "over the last 6-12 months" makes the model look harder, not just format differently; it pulled a full year of data and corrected its own answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reading the patterns vs. using them
&lt;/h2&gt;

&lt;p&gt;None of this is new, and I knew that. I'd read &lt;a href="https://arxiv.org/abs/2302.11382" rel="noopener noreferrer"&gt;White et al. (2023), &lt;em&gt;A Prompt Pattern Catalog to Enhance Prompt Engineering with ChatGPT&lt;/em&gt;&lt;/a&gt; a while back. It catalogs prompt techniques as reusable "patterns," the way software design patterns work, and the moves I leaned on map almost one to one onto it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Act as a pricing analyst" is the &lt;strong&gt;Persona Pattern&lt;/strong&gt;: give the model a point of view so it knows what to focus on.&lt;/li&gt;
&lt;li&gt;"Compare to street price, not list" is the &lt;strong&gt;Question Refinement Pattern&lt;/strong&gt;: get the model to answer a better version of the question than the one you asked.&lt;/li&gt;
&lt;li&gt;"Work through (1), (2), (3) before concluding" is the &lt;strong&gt;Cognitive Verifier Pattern&lt;/strong&gt;: break a question into sub-questions and build the answer from them.&lt;/li&gt;
&lt;li&gt;"Cite each price or mark it unverified" and "verify the math" are the &lt;strong&gt;Fact Check List&lt;/strong&gt; and &lt;strong&gt;Reflection&lt;/strong&gt; patterns: make the model surface what its answer depends on, then check itself.&lt;/li&gt;
&lt;li&gt;The assistant offering to "pull price history on any specific item before you commit" is the &lt;strong&gt;Flipped Interaction Pattern&lt;/strong&gt;, where the model drives by asking you questions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But knowing the catalog and using it are different skills. Reading the paper, the patterns felt like a tidy list to nod along to. It wasn't until I was staring at a $549 watch I actually wanted, with real money on the line, that "Question Refinement" stopped being a term and became a reflex: &lt;em&gt;no, don't accept the list price, make it compare to street.&lt;/em&gt; The patterns only stuck once I had a reason to care whether the answer was right.&lt;/p&gt;

&lt;p&gt;If you want an easier intro than the paper, Descript's &lt;a href="https://www.descript.com/blog/article/5-advanced-prompts-to-get-better-answers-from-chatgpt" rel="noopener noreferrer"&gt;5 Advanced Prompts to Get Better Answers from ChatGPT&lt;/a&gt; walks through five of these same patterns with plain examples. Read either one. Then go find a question you actually care about and use them, because that's the part the reading can't do for you.&lt;/p&gt;

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

&lt;p&gt;I set out to learn prompt engineering and ended up also learning that the Shark IZ363HT was a genuinely good deal and the Dyson was the better vacuum, which is its own kind of useful. Still don't know which to buy, but at least I do know more and have developed some better prompting habits. Every one of these techniques is just a way of refusing to accept the easy answer: don't trust the list price, don't accept an undefined "better," don't believe the math without seeing it.&lt;/p&gt;

&lt;p&gt;Pick something you actually want to know the answer to. That's the trick. When you care, you can feel the difference between a hollow answer and a real one, and chasing that difference is the entire skill.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>promptengineering</category>
      <category>llm</category>
      <category>shopping</category>
    </item>
    <item>
      <title>It Worked on My Machine (Literally)</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Tue, 09 Jun 2026 22:13:00 +0000</pubDate>
      <link>https://dev.to/cseeman/it-worked-on-my-machine-literally-4ekn</link>
      <guid>https://dev.to/cseeman/it-worked-on-my-machine-literally-4ekn</guid>
      <description>&lt;p&gt;I have a &lt;a href="https://trmnl.com/" rel="noopener noreferrer"&gt;TRMNL&lt;/a&gt; on my desk. If you haven't seen one, it's a little e-ink display from &lt;a href="https://trmnl.com/" rel="noopener noreferrer"&gt;trmnl.com&lt;/a&gt; that shows you whatever you tell it to: your calendar, the weather (but in Haiku form), a far side comic, a random Studio Ghibli picture. The whole device runs on plugins, and the nice thing is you can write your own. I'd been meaning to build a TRMNL plugin for a while, and I finally landed on an idea that was small enough to actually finish: show what I'm currently reading on &lt;a href="https://app.thestorygraph.com/" rel="noopener noreferrer"&gt;StoryGraph&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Just three things, really. My profile name, what I'm currently reading, and the next couple of books in my to-read pile. That's it. A small project. I even said the words "basic, simple plugin" out loud, which in hindsight was me daring the universe.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plan
&lt;/h2&gt;

&lt;p&gt;TRMNL plugins can fetch their data a few different ways. The one that fit was &lt;strong&gt;polling&lt;/strong&gt;: TRMNL pings a URL on a schedule, gets back some JSON, and renders it with a &lt;a href="https://shopify.github.io/liquid/" rel="noopener noreferrer"&gt;Liquid&lt;/a&gt; template. So I needed a small server that returns my reading data as JSON, plus the templates to lay it out on the screen.&lt;/p&gt;

&lt;p&gt;The catch: StoryGraph doesn't have a public API. No tidy endpoint to call. If I wanted the data, I'd have to scrape it off my public profile page. I found a reference project, &lt;a href="https://github.com/xdesro/storygraph-api" rel="noopener noreferrer"&gt;storygraph-api&lt;/a&gt;, that does exactly this, and it gave me the lay of the land: the URLs to hit (&lt;code&gt;/currently-reading/username&lt;/code&gt;, &lt;code&gt;/to-read/username&lt;/code&gt;) and the HTML structure of a book on the page.&lt;/p&gt;

&lt;p&gt;I wanted to keep this lightweight. Plain Ruby where I could, a real framework only if I needed one. For a service with two or three JSON routes, plain Ruby plus &lt;a href="https://github.com/rack/rack" rel="noopener noreferrer"&gt;Rack&lt;/a&gt; is plenty. No Rails, no Hanami, just a Rack app and &lt;a href="https://nokogiri.org/" rel="noopener noreferrer"&gt;Nokogiri&lt;/a&gt; to parse the HTML. Easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first wall
&lt;/h2&gt;

&lt;p&gt;Before writing a line of application code, I did the one thing I always tell other people to do: I tested the riskiest assumption first. Could I even fetch a StoryGraph page?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl https://app.thestorygraph.com/profile/christine_s
HTTP 403
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hm. I added a browser User-Agent. Still 403. I added the full set of Chrome headers, the &lt;code&gt;sec-ch-ua&lt;/code&gt; bits, a cookie jar, all of it. Still 403. Then I looked at the response headers and saw the actual story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cf-mitigated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;challenge&lt;/span&gt;
&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudflare&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;StoryGraph sits behind a Cloudflare managed challenge. My polite little &lt;code&gt;curl&lt;/code&gt; request was getting waved off at the door before it ever reached their servers. And here's the part that surprised me: it wasn't about the headers at all. Cloudflare was fingerprinting the &lt;strong&gt;TLS handshake itself&lt;/strong&gt;. Real browsers negotiate TLS in a particular, recognizable way (the cipher order, the extensions, the whole shape of the "hello"), and &lt;code&gt;curl&lt;/code&gt; does it differently. You can spoof every header in the world and you'll still look like a robot, because the give-away happens one layer down, before any headers are sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that actually worked
&lt;/h2&gt;

&lt;p&gt;The fix turned out to be a tool I'd never had a reason to use before: &lt;a href="https://github.com/lexiforest/curl-impersonate" rel="noopener noreferrer"&gt;curl-impersonate&lt;/a&gt;. It's &lt;code&gt;curl&lt;/code&gt; rebuilt to mimic a real browser's TLS fingerprint exactly. Same ciphers, same curves, same handshake shape as Chrome.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl_chrome136 &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    https://app.thestorygraph.com/currently-reading/elliek
200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two hundred. The door opened. Watching that &lt;code&gt;403&lt;/code&gt; flip to &lt;code&gt;200&lt;/code&gt; was easily the most satisfying moment of the whole project. The challenge wasn't checking who I claimed to be, it was checking how I &lt;em&gt;spoke&lt;/em&gt;, and now I had the right accent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the actual thing
&lt;/h2&gt;

&lt;p&gt;With the hard part de-risked, the rest came together quickly, which is how these things usually go once the scary unknown is gone.&lt;/p&gt;

&lt;p&gt;The service is a small Rack app. One real endpoint, &lt;code&gt;/reads.json&lt;/code&gt;, that takes a username and a limit. It fetches two pages through curl-impersonate, hands the HTML to a Nokogiri scraper that pulls out each book's title, author, and cover, and returns a clean little JSON payload. There's a &lt;code&gt;/health&lt;/code&gt; route and a tiny index page, and that's the whole surface area.&lt;/p&gt;

&lt;p&gt;A few decisions I'm happy with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Caching.&lt;/strong&gt; Scraping is slow and I didn't want to hammer StoryGraph every time TRMNL polls. An in-memory cache with a thirty-minute TTL means repeated polls cost nothing and I stay a good citizen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failing soft.&lt;/strong&gt; If a scrape fails, the endpoint still returns &lt;code&gt;200&lt;/code&gt; with an &lt;code&gt;error&lt;/code&gt; field instead of a &lt;code&gt;500&lt;/code&gt;. A blank e-ink screen tells you nothing. A screen that says "couldn't load, is the profile public?" at least tells you where to look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retries.&lt;/strong&gt; StoryGraph occasionally drops a rapid second request, so the fetcher retries with a short backoff.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the templates. TRMNL supports four layout sizes (full, two halves, and a quadrant), and I wrote Liquid for each, with the empty and error states baked in so the display always has something sensible to show. I wrapped it all in a Docker image that installs the right curl-impersonate build for the architecture, and I had a passing test suite running against saved HTML fixtures so I wasn't hitting the network on every run.&lt;/p&gt;

&lt;p&gt;It worked. Locally, it really worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second wall (this one was my fault)
&lt;/h2&gt;

&lt;p&gt;I pointed the scraper at my own profile and got a redirect to a sign-in page. My books were nowhere.&lt;/p&gt;

&lt;p&gt;It took me an embarrassing minute to realize: my StoryGraph profile was &lt;strong&gt;private&lt;/strong&gt;. Of course it was. Public profiles scrape fine; private ones bounce you to the login wall, exactly as they should. The fix was a single toggle in my StoryGraph settings, and suddenly there I was in JSON form: &lt;em&gt;Eloquent Ruby&lt;/em&gt;, &lt;em&gt;Effective Testing with RSpec 3&lt;/em&gt;, &lt;em&gt;The Staff Engineer's Path&lt;/em&gt;. Reader, my to-read pile is exactly as on-brand as you'd expect.&lt;/p&gt;

&lt;p&gt;To see it on the actual device, I ran the container locally and pointed a &lt;a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/" rel="noopener noreferrer"&gt;cloudflared&lt;/a&gt; tunnel at it, which gave me a temporary public URL to paste into TRMNL. A minute later my little e-ink screen lit up with my current reads. I may have done a small chair dance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twist
&lt;/h2&gt;

&lt;p&gt;The tunnel was never meant to be permanent (it runs off my laptop, and the URL changes every time it restarts), so the next step was deploying somewhere real. I built the Docker image for &lt;a href="https://fly.io/" rel="noopener noreferrer"&gt;Fly.io&lt;/a&gt;, set my username, and shipped it.&lt;/p&gt;

&lt;p&gt;The health check was green. The scrape failed. Every single time.&lt;/p&gt;

&lt;p&gt;Same code. The exact same image that had just pulled my books down on my Mac, now returning "couldn't load the profile" from the cloud, over and over. I retried. I checked the profile was still public. I stared at it for a while.&lt;/p&gt;

&lt;p&gt;Then it clicked, and it's the lesson I keep coming back to. curl-impersonate beats Cloudflare's &lt;strong&gt;fingerprint&lt;/strong&gt; check. It does nothing about Cloudflare's &lt;strong&gt;IP reputation&lt;/strong&gt; check. My Mac sits behind a residential IP that looks like a person. Fly's machines sit on datacenter IP ranges that Cloudflare knows perfectly well belong to a hosting provider, and it blocks them on sight, accent or no accent. The request from my laptop and the request from Fly were byte-for-byte identical in every way I controlled. The only difference was where they came from, and that difference was the whole game.&lt;/p&gt;

&lt;p&gt;It worked on my machine. The single most clichéd sentence in software, and here it was, completely literal and completely true.&lt;/p&gt;

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

&lt;p&gt;The code was never the hard part. I spent maybe an afternoon on the Rack app, the scraper, the templates, all of it. I spent far longer learning that a request has properties I'd never had to think about: the shape of its handshake, the reputation of the address it leaves from. Those live underneath the application entirely, and no amount of clean Ruby touches them.&lt;/p&gt;

&lt;p&gt;There are real ways forward from here. I could run it from a residential connection (an always-on box at home behind a stable tunnel). I could route the outbound requests through a service that provides residential IPs and handles the Cloudflare dance for me. Each is a tradeoff between cost, complexity, and how much of my own hardware I want babysitting a reading list. For now, the laptop tunnel does the job, and I've left the deploy config in the repo for when I commit to a permanent home.&lt;/p&gt;

&lt;p&gt;I'm planning to share the code once it's had the cleanup it badly needs. It works, but "works" and "ready to show people" are two different states, and right now there are a few rough edges I'd rather not hand to anyone. When it's tidied up I'll post the repo, so if you want to build something similar for your own TRMNL, keep an eye out.&lt;/p&gt;

&lt;p&gt;But the plugin itself is done, and it's genuinely lovely to glance over at my desk and see what I'm reading rendered in crisp e-ink. A weekend project that turned into a short tour of everything that happens to a web request before your code ever sees it. I'll take it. I just won't call the next one "basic" out loud.&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.amazonaws.com%2Fuploads%2Farticles%2F615w9v8237gs62vobxlg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F615w9v8237gs62vobxlg.png" alt="The " width="630" height="504"&gt;&lt;/a&gt;&lt;br&gt;
Isn't it glorious??&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>development</category>
      <category>learning</category>
    </item>
    <item>
      <title>Blue Ridge Ruby 2026: A Conference About the Long Game</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Tue, 05 May 2026 14:33:56 +0000</pubDate>
      <link>https://dev.to/cseeman/blue-ridge-ruby-2026-a-conference-about-the-long-game-357a</link>
      <guid>https://dev.to/cseeman/blue-ridge-ruby-2026-a-conference-about-the-long-game-357a</guid>
      <description>&lt;p&gt;The first morning of &lt;a href="https://blueridgeruby.com/" rel="noopener noreferrer"&gt;Blue Ridge Ruby&lt;/a&gt;, John Athayde stood at the front of the YMI Cultural Center and showed us pictures of trees. Forests. His own land. The projector at the venue was so good that the photos felt like windows. His talk, &lt;a href="https://speakerdeck.com/johnathayde/learning-from-permaculture-sustainable-software-development" rel="noopener noreferrer"&gt;Learning from Permaculture: Sustainable Software Development&lt;/a&gt;, used permaculture principles to talk about how we tend our codebases. I came in expecting a Ruby conference. I didn't expect to spend the morning thinking about how a forest grows itself, and how the same patterns might apply to the systems I work on every day. He also pointed us toward Chad Fowler's &lt;a href="https://aicoding.leaflet.pub" rel="noopener noreferrer"&gt;The Phoenix Architecture,&lt;/a&gt;, which is now next on my reading list.&lt;/p&gt;

&lt;p&gt;That was the first talk. Once the first talk happens, the conference has actually started.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small conference, in the best way
&lt;/h2&gt;

&lt;p&gt;Blue Ridge Ruby is a regional conference in Asheville, North Carolina. About 100 attendees this year, held at the YMI Cultural Center on South Market Street. The size matters. I had time to talk with almost everyone. The schedule helped too: 30-minute talks with 30-minute breaks in between. By the end of each day I wasn't drained the way I usually am at bigger conferences. Fitting, since the conference itself turned out to be largely about sustainability.&lt;/p&gt;

&lt;p&gt;The night before, the speakers gathered for dinner. I got plenty of time to chat with &lt;a href="https://kevinjmurphy.com/" rel="noopener noreferrer"&gt;Kevin Murphy&lt;/a&gt;, Ernesto Tagwerker of &lt;a href="https://fastruby.io/" rel="noopener noreferrer"&gt;FastRuby&lt;/a&gt; (a sponsor), &lt;a href="https://www.chael.codes/" rel="noopener noreferrer"&gt;Rachael Wright-Munn&lt;/a&gt;, and Adam McCrea of &lt;a href="https://judoscale.com/job-queues-slides" rel="noopener noreferrer"&gt;Judoscale&lt;/a&gt; (another sponsor). Lovely people, lovely food, but the conference itself didn't feel real to me until that first talk the next morning. That's just how it goes.&lt;/p&gt;

&lt;p&gt;Jeremy Smith, one of the organizers along with Mark Locklear and Joe Peck, kicked things off with an opening that leaned into nostalgia: small golden ages, DuckTales, and the kinds of moments you only recognize while you're inside them. A good frame for what came next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thread: long-term care
&lt;/h2&gt;

&lt;p&gt;Looking back, almost every talk I saw was, at some level, about long-term care. Care of code, care of communities, care of ourselves.&lt;/p&gt;

&lt;p&gt;John Athayde's permaculture talk was the most explicit version of this. Ifat Ribon's &lt;em&gt;Yes, &amp;amp;…: Ruby's Secret Talent for Improvisation&lt;/em&gt; connected improv to code. I'd seen a preview at a &lt;a href="https://www.wnb-rb.dev/" rel="noopener noreferrer"&gt;WNB.rb&lt;/a&gt; online meetup, and it landed even better in person. Improv is about staying loose enough to keep playing with what's in front of you, which is also a pretty good description of working in a long-lived codebase.&lt;/p&gt;

&lt;p&gt;Kevin Murphy's talk, &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;, started out funny. A hilarious bit about being a new employee at "Hours Unlimited" had the room going. Then it quietly flipped a switch. It moved from talking about agents to talking about humans on your team. Code review isn't just a quality gate, it's an invitation to a discussion. Form deep working relationships. Treat the other humans on your team like, well, humans. Lessons that work whether your collaborator is an LLM or a coworker.&lt;/p&gt;

&lt;p&gt;Rachael Wright-Munn's &lt;em&gt;Your First Open-Source Contribution&lt;/em&gt; was about giving back to the ecosystem you depend on, which is its own form of long-term care. And &lt;a href="https://microblog.thomascannon.me/" rel="noopener noreferrer"&gt;Thomas Cannon&lt;/a&gt; closed the loop with &lt;em&gt;5 Ways to Invest in Yourself for the Long Haul&lt;/em&gt;, which made the implicit theme of the whole conference explicit.&lt;/p&gt;

&lt;p&gt;By the end of the first day I was pretty sure the program had been curated around this idea, even if no one said it out loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  My turn at the podium
&lt;/h2&gt;

&lt;p&gt;My talk, &lt;a href="https://blueridgeruby.com/speakers/christine-seeman/" rel="noopener noreferrer"&gt;Optimize Your Mindset (Without Overclocking)&lt;/a&gt;, went on after the lightning talks. Quick word for lightning talks, by the way. I first encountered them at LA RubyConf back in 2018, and they've become one of my favorite parts of any Ruby conference. Five minutes each, anything you want. We got a git commit deep dive, a very funny (and informative) Judoscale bit, a perspective on learning Ruby as a newcomer in 2026, and several others. They're a window into what the community is actually thinking about right now.&lt;/p&gt;

&lt;p&gt;After some fun projector adventures with macOS Tahoe earlier in the day, my setup went off without a hitch. The talk was one I'd given before, but I'd completely overhauled it for Blue Ridge Ruby. The piece that resonated most, I think, was the new section on deep work and focus.&lt;/p&gt;

&lt;p&gt;The short version: the focus problem isn't your fault. There is so much stacked against us actually being able to think. Social media, notifications, infinite feeds, open offices, constant chat, back-to-back meetings. It's a lot. We need help designing our days if we want to keep any room for real thinking. I dug into research from Cal Newport, Nir Eyal's &lt;em&gt;Indistractable&lt;/em&gt;, Johann Hari's &lt;em&gt;Stolen Focus&lt;/em&gt;, and Dr. Gloria Mark at UC Irvine. I'll have a longer write-up on just that section soon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hallway track
&lt;/h2&gt;

&lt;p&gt;The 30-minutes-on, 30-minutes-off rhythm meant the hallway track was as substantive as the talks. I got to meet Burdette Lamar, who helps with documentation on the Ruby Core team. I geeked out with Kielan from Boston about fantasy books, and she reads more than I do, which is saying something. We pulled together an impromptu WNB.rb lunch on day one with about ten members. I got to help &lt;a href="https://alchemists.io/talks/terminus" rel="noopener noreferrer"&gt;Brooke Kuhlmann&lt;/a&gt; pass around eink devices during his talk. And I lost track of time talking with Thomas Cannon about web comics, weight lifting, and books. So many books.&lt;/p&gt;

&lt;p&gt;That's the kind of thing that only really happens at a conference this size. With 100 people and a schedule that gives you room to breathe, you don't have to choose between attending a talk and having a real conversation. You get both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm taking home
&lt;/h2&gt;

&lt;p&gt;I came to Blue Ridge Ruby to give a talk about caring for the human side of being a developer. I left with a much fuller version of that idea, thanks to everyone else on the schedule. Care for the codebase, care for the team, care for the ecosystem, care for yourself. It all rhymes.&lt;/p&gt;

&lt;p&gt;If you missed it this year, keep an eye on &lt;a href="https://blueridgeruby.com/" rel="noopener noreferrer"&gt;blueridgeruby.com&lt;/a&gt; for next year. And to Jeremy, Mark, and Joe: thank you for putting on the kind of conference where you actually have time to think. That's rarer than it should be.&lt;/p&gt;

&lt;p&gt;Videos will be coming soon if there was any talk you wanted to watch, thanks to &lt;a href="https://www.confreaks.com/" rel="noopener noreferrer"&gt;Confreaks&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>techtalks</category>
      <category>workplace</category>
    </item>
    <item>
      <title>Watching RubyGems.org in Real Time</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Thu, 16 Apr 2026 21:46:47 +0000</pubDate>
      <link>https://dev.to/cseeman/watching-rubygemsorg-in-real-time-11o</link>
      <guid>https://dev.to/cseeman/watching-rubygemsorg-in-real-time-11o</guid>
      <description>&lt;p&gt;RubyGems.org published its first public roadmap this week. That's new, and it's worth noticing.&lt;/p&gt;

&lt;p&gt;I've written about &lt;a href="https://christine-seeman.com/what-just-happened-to-rubygems/" rel="noopener noreferrer"&gt;Ruby Central governance before&lt;/a&gt;, and about &lt;a href="https://christine-seeman.com/contributing-to-rubygems-org/" rel="noopener noreferrer"&gt;choosing to contribute anyway&lt;/a&gt;. One of the things that frustrated me most wasn't any specific decision, it was that plans were hard to follow unless you were already plugged in. The closest thing to a public roadmap were the "State of RubyGems" talks at RubyConf: the &lt;a href="https://www.youtube.com/watch?v=Hea-x7LHO9Y" rel="noopener noreferrer"&gt;2023 San Diego talk&lt;/a&gt; by Samuel Giddins, then Security Engineer in Residence at Ruby Central, and the &lt;a href="https://www.youtube.com/watch?v=tTrMtMe56kE" rel="noopener noreferrer"&gt;2024 Chicago talk&lt;/a&gt; by Giddins, Marty Haught, and Martin Emde. Both were excellent, but once a year and after decisions were already in motion.&lt;/p&gt;

&lt;p&gt;A public roadmap doesn't fix governance. But it does something important: it makes the work legible year-round.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually on the Roadmap
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://blog.rubygems.org/2026/04/15/rubygems-org-has-a-public-roadmap.html" rel="noopener noreferrer"&gt;announcement&lt;/a&gt; points to a GitHub project board with initiatives at various stages. A few things stood out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Organizations moving toward general availability.&lt;/strong&gt; This was in private beta as of the January newsletter and is clearly on the path to shipping. If you've dealt with gem ownership transfers, multi-maintainer coordination, or what happens when the one person with push access leaves a company, this is the feature that addresses it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native gem improvements.&lt;/strong&gt; This involves both the RubyGems client team and contributors from Shopify. Native gems have been a persistent rough edge, particularly for anyone managing platform-specific builds in CI. Seeing it on the roadmap explicitly is good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gem archival and security tooling.&lt;/strong&gt; Both are listed as longer-term work. I have a lot of thoughts about gem archival specifically, but I'll save those for another post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Transparency Matters for Infrastructure
&lt;/h2&gt;

&lt;p&gt;Most software projects you depend on have some kind of public communication: a changelog, a GitHub issues list, a blog. But registries are different. RubyGems.org isn't a gem you pin in your Gemfile and upgrade when you're ready. It's the substrate. Changes to how it works, what it accepts, what policies it enforces. Those decisions ripple out to every Ruby developer and every tool in the ecosystem, whether or not they're paying attention.&lt;/p&gt;

&lt;p&gt;The GitHub project board isn't just a communication tool, either. The announcement specifically invites people to comment on existing issues or file new ones if they see something missing. That's participation, not just broadcast.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm Optimistic About
&lt;/h2&gt;

&lt;p&gt;After the RubyGems fracture last fall (the departure of Samuel Giddins and André Arko, the revoked access, the walkout), I wasn't sure what to expect from Ruby Central heading into this year. A public roadmap with real items, tied to a public issue tracker where people can actually engage, is not nothing.&lt;/p&gt;

&lt;p&gt;It's easy to take for granted if you're used to it from other projects, but for RubyGems.org, this is new. Moving in the right direction counts for something.&lt;/p&gt;

&lt;p&gt;I'm looking forward to watching the Organizations feature ship, and to seeing how the community ends up shaping what comes next.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The announcement: &lt;a href="https://blog.rubygems.org/2026/04/15/rubygems-org-has-a-public-roadmap.html" rel="noopener noreferrer"&gt;RubyGems.org Has a Public Roadmap&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>A Simple Tmux Script for Your Daily Dev Session</title>
      <dc:creator>christine</dc:creator>
      <pubDate>Mon, 06 Apr 2026 21:31:09 +0000</pubDate>
      <link>https://dev.to/cseeman/a-simple-tmux-script-for-your-daily-dev-session-2bjb</link>
      <guid>https://dev.to/cseeman/a-simple-tmux-script-for-your-daily-dev-session-2bjb</guid>
      <description>&lt;p&gt;A couple of years ago I wrote about &lt;a href="https://dev.to/cseeman/how-to-copy-text-from-one-pane-macostmuxalacritty-2ll0"&gt;copying text between tmux panes&lt;/a&gt;. At the time I was still figuring out how all the pieces of my terminal setup fit together (a recurring theme, honestly). Since then, tmux has become the first thing I open every morning. I wrote a small bash script that sets up my dev session, and I've tweaked it enough times that I thought it was worth sharing where it ended up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I Started
&lt;/h2&gt;

&lt;p&gt;My first version created a four-pane layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+------------------+------------------+
| nvim             | rails server     |
+------------------+------------------+
| lazygit          | claude           |
+------------------+------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neovim for editing, Rails server running &lt;code&gt;bin/dev&lt;/code&gt; (which kicks off Overmind with the full Procfile.dev), lazygit for staging and committing, and Claude Code for when I need a second brain. It felt very productive to look at. Four panes! Everything visible! I am a real developer!&lt;/p&gt;

&lt;p&gt;And then I actually used it for a while. The Rails server pane just... sat there, taking up screen real estate while I was writing code that didn't need a running server. Lazygit is great, but I reach for &lt;code&gt;git status&lt;/code&gt; and &lt;code&gt;git diff&lt;/code&gt; in a regular shell just as often. And four panes on a laptop screen? Everything is tiny.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I Landed
&lt;/h2&gt;

&lt;p&gt;Two panes. A shell on the left, Claude Code on the right.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+------------------+------------------+
| shell            | claude           |
+------------------+------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shell handles whatever I need in the moment: editing, git, spinning up the server, running tests. Turns out I don't need a dedicated pane for each tool. I just need a place to work and a place to think out loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Script
&lt;/h2&gt;

&lt;p&gt;The full script is in my &lt;a href="https://github.com/cseeman/dotfiles/tree/main/dev-scripts" rel="noopener noreferrer"&gt;dotfiles&lt;/a&gt;, but here's what it does:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c"&gt;# Universal Development Session Script&lt;/span&gt;
&lt;span class="c"&gt;# Creates a 2-pane tmux layout for any Rails project or worktree&lt;/span&gt;
&lt;span class="c"&gt;# Usage: ./dev-session [session-name] [directory]&lt;/span&gt;

&lt;span class="c"&gt;# Configuration&lt;/span&gt;
&lt;span class="nv"&gt;DEFAULT_EDITOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"nvim"&lt;/span&gt;
&lt;span class="nv"&gt;DEFAULT_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"3000"&lt;/span&gt;

get_current_dir&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;pwd&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

get_project_name&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;get_current_dir&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

get_branch_name&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--git-dir&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;git branch &lt;span class="nt"&gt;--show-current&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt;
    &lt;span class="k"&gt;else
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no-git"&lt;/span&gt;
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

generate_session_name&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;project_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;get_project_name&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;branch_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;get_branch_name&lt;span class="si"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"develop"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"no-git"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nv"&gt;safe_branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/[\/:]/-/g'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;safe_branch&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$project_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;SESSION_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;generate_session_name&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;WORK_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;get_current_dir&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORK_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error: Directory &lt;/span&gt;&lt;span class="nv"&gt;$WORK_DIR&lt;/span&gt;&lt;span class="s2"&gt; does not exist"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Reattach if session already exists&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;tmux has-session &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Session '&lt;/span&gt;&lt;span class="nv"&gt;$SESSION_NAME&lt;/span&gt;&lt;span class="s2"&gt;' already exists. Attaching..."&lt;/span&gt;
    tmux attach-session &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Create session and split into two panes&lt;/span&gt;
tmux new-session &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORK_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
tmux split-window &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORK_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Left pane: shell, right pane: Claude Code&lt;/span&gt;
tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; 1 &lt;span class="s2"&gt;"# Ready for commands (server, etc.)"&lt;/span&gt; C-m
tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; 2 &lt;span class="s2"&gt;"claude"&lt;/span&gt; C-m

tmux &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-pane&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; 1
tmux rename-window &lt;span class="s2"&gt;"dev"&lt;/span&gt;
tmux attach-session &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I alias it in my shell config so I can run &lt;code&gt;dev&lt;/code&gt; from any project directory:&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;alias &lt;/span&gt;&lt;span class="nv"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"~/Documents/Repos/dev-scripts/dev-session"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Parts I Actually Like
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It names sessions from your git branch.&lt;/strong&gt; If I'm on &lt;code&gt;main&lt;/code&gt;, the session is just the project name (&lt;code&gt;qualify&lt;/code&gt;). On a feature branch, it tacks on the branch name (&lt;code&gt;qualify-fix-event-registry&lt;/code&gt;). Slashes in branch names get swapped to hyphens so tmux doesn't choke on them.&lt;/p&gt;

&lt;p&gt;This is my favorite part, because I usually have multiple sessions going at once, one per feature branch or worktree. &lt;code&gt;tmux list-sessions&lt;/code&gt; gives me a quick snapshot of what I've got cooking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qualify: 1 windows (created Thu Apr  3 09:15:00 2026)
qualify-add-snapshot-tests: 1 windows (created Thu Apr  3 10:30:00 2026)
rubygems: 1 windows (created Wed Apr  2 14:00:00 2026)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;It won't create duplicates.&lt;/strong&gt; If a session with that name already exists, the script just attaches to it. Detach with &lt;code&gt;Ctrl-b d&lt;/code&gt;, go make coffee, come back, run &lt;code&gt;dev&lt;/code&gt; again, and you're right where you left off. Sessions survive sleep/wake cycles too, which still feels like magic to me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works anywhere.&lt;/strong&gt; The script doesn't assume Rails or even git. I use it for this blog, for Ruby gems, for random one-off scripts. If there's a git repo, it uses the branch name. If not, you just get the directory name. No fuss.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Claude Code Pane
&lt;/h2&gt;

&lt;p&gt;The right pane launches &lt;a href="https://docs.anthropic.com/en/docs/claude-code/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; automatically. I like having it right there instead of in a separate terminal window. I can glance over at its output while I'm working, and &lt;code&gt;Ctrl-b z&lt;/code&gt; (tmux zoom) is my best friend here, blowing the Claude pane up to full screen when I'm reading a longer response and then popping back to the split.&lt;/p&gt;

&lt;p&gt;The other nice thing is that Claude Code's context stays warm. I can ask it something, go write code in the left pane for twenty minutes, come back and ask a follow-up without re-explaining everything. That alone is worth the dedicated pane.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I'd Change
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;.tmux.conf&lt;/code&gt; is still embarrassingly minimal. It's literally just a scrollback buffer increase. The README in my dotfiles repo documents vim-style pane navigation bindings that I haven't actually configured yet. I will get to it. Eventually. (I tell myself this every week.)&lt;/p&gt;

&lt;p&gt;The four-pane version (&lt;code&gt;dev-session-fixed&lt;/code&gt;) is still sitting in the repo too. I keep it around for when I'm debugging something where I really do need server logs visible the whole time. But I haven't reached for it in weeks, which tells me something.&lt;/p&gt;

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

&lt;p&gt;Grab the script from &lt;a href="https://github.com/cseeman/dotfiles/tree/main/dev-scripts" rel="noopener noreferrer"&gt;my dotfiles&lt;/a&gt;, drop it somewhere in your path, and alias it. The only real dependency is tmux itself. If you don't have Claude Code installed, no worries, that pane just becomes a regular shell.&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;cd &lt;/span&gt;your-project
dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Two panes, named after your branch, ready to go.&lt;/p&gt;

</description>
      <category>tmux</category>
      <category>bash</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
