<?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: Marcin Ostrowski</title>
    <description>The latest articles on DEV Community by Marcin Ostrowski (@marostr).</description>
    <link>https://dev.to/marostr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3738175%2F897297e4-a5af-4b6e-8027-8032ccb5f22a.jpeg</url>
      <title>DEV Community: Marcin Ostrowski</title>
      <link>https://dev.to/marostr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marostr"/>
    <language>en</language>
    <item>
      <title>Your AI has no memory. Your Rails codebase does.</title>
      <dc:creator>Marcin Ostrowski</dc:creator>
      <pubDate>Wed, 11 Feb 2026 14:49:18 +0000</pubDate>
      <link>https://dev.to/marostr/your-ai-has-no-memory-your-rails-codebase-does-4bg1</link>
      <guid>https://dev.to/marostr/your-ai-has-no-memory-your-rails-codebase-does-4bg1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2jtuuzypo5vgwuv1hr13.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2jtuuzypo5vgwuv1hr13.jpg" alt="Your AI has no memory. Your Rails codebase does." width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Memento, Leonard wakes up every morning with no memory of yesterday. He doesn't know where he is. He doesn't know what he did. The only things he trusts are the tattoos on his body.&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%2Fjat95n6urzvpmub8ue4q.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjat95n6urzvpmub8ue4q.jpg" alt="Your AI has no memory. Your Rails codebase does." width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI coding works the same way. Every session is a fresh wakeup in a motel room. It doesn't remember the raw SQL disaster from last session. It doesn't remember hallucinating a method yesterday. The only things it trusts are the rules you load into context.&lt;/p&gt;

&lt;p&gt;And your codebase won't help. It remembers everything, including the bad habits. Every shortcut, every model that was never refactored, every raw SQL query nobody got around to fixing. AI treats all of it as truth. It doesn't know which patterns are current and which are legacy. It just sees code and reproduces it.&lt;/p&gt;

&lt;p&gt;Conventions are the filter. They tell AI which patterns to trust.&lt;/p&gt;

&lt;p&gt;I covered how to tattoo in the &lt;a href="https://rubyonai.com/the-single-most-important-thing-that-made-me-believe-ai-coding-could-work/" rel="noopener noreferrer"&gt;first post&lt;/a&gt; - skills and hooks that force AI to load conventions before writing code. Now the question is: &lt;strong&gt;what do you tattoo?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You wouldn't tattoo "use 2-space indentation." You'd tattoo the rules that prevent the worst mistakes. "Business logic lives in models." "Ask objects, don't reach into their internals." "No JavaScript if Turbo handles it."&lt;/p&gt;

&lt;p&gt;If the tattoos are wrong, missing, or ambiguous, the AI goes off the rails. It doesn't have the context to question them.&lt;/p&gt;

&lt;p&gt;These are my most important tattoos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask objects, don't reach into their internals
&lt;/h2&gt;

&lt;p&gt;I ask Claude to build a bookmarking feature. It creates the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/models/academy_bookmark.rb
class AcademyBookmark &amp;lt; ApplicationRecord
  belongs_to :user
  belongs_to :academy
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Two associations. No methods. Where did the logic go?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# It's in the controller
current_user.academy_bookmarks.find_or_create_by(academy: @academy)

# It's in another controller
current_user.academy_bookmarks.find_by(academy: @academy)&amp;amp;.destroy

# It's in a job
User.find_each { |u| u.academy_bookmarks.where(created_at: ..1.week.ago).destroy_all }

# It's in the view
&amp;lt;% if current_user.academy_bookmarks.exists?(academy: academy) %&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model is just a database table with a class name. The logic is scattered across four files and is playing hide-and-seek.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leaking implementation turns every caller into a co-maintainer of your internals.&lt;/strong&gt; That's bad enough with developers who at least understood the internals when they wrote the code. It's worse with AI, which doesn't understand them at all. It just confidently reproduces whatever it saw last.&lt;/p&gt;

&lt;p&gt;When a coworker writes something confusing, you can hit them with the beloved "quick call?" and ask what they meant. When AI reproduces a raw association query it found somewhere in your codebase, there's no one to call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rails Model Conventions Skill
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Business logic lives here - Models own ALL domain logic, not controllers
- Clean interfaces - Don't leak implementation details
- Message passing - Ask objects, don't reach into their associations
- Pass objects, not IDs - Method signatures should accept domain objects

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tattoo made. Let's ask Claude again, same feature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/models/academy_bookmark.rb
class AcademyBookmark &amp;lt; ApplicationRecord
  belongs_to :user
  belongs_to :academy
end

# app/models/user.rb
class User &amp;lt; ApplicationRecord
  def bookmarked?(academy)
    academy_bookmarks.exists?(academy: academy)
  end

  def bookmark(academy)
    academy_bookmarks.find_or_create_by(academy: academy)
  end

  def unbookmark(academy)
    academy_bookmarks.find_by(academy: academy)&amp;amp;.destroy
  end
end

# In the controller
current_user.bookmark(@academy)

# In another controller
current_user.unbookmark(@academy)

# In a job
User.find_each { |u| u.cleanup_old_bookmarks }

# In the view
&amp;lt;% if current_user.bookmarked?(academy) %&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four files still use bookmarks. But none of them know how bookmarks work. Neat!&lt;/p&gt;

&lt;p&gt;Over the past few years, I have been talking about this non-stop. This is the rule I'd save if I could only keep one. For developers and for AI. &lt;strong&gt;Business logic goes to models, which have clean interfaces and pass messages.&lt;/strong&gt; You'll see this rule repeated across several layers. That's intentional.&lt;/p&gt;

&lt;p&gt;I could have tattooed it on my forehead.&lt;/p&gt;

&lt;p&gt;Full skill: &lt;a href="https://github.com/marostr/superpowers-rails/blob/main/skills/rails-model-conventions/SKILL.md?ref=rubyonai.com" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers-rails/blob/main/skills/rails-model-conventions/SKILL.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Boring is correct
&lt;/h2&gt;

&lt;p&gt;Hey Claude... Thinking about it, I only want students to be able to bookmark an academy. Also, I want them to unbookmark it. And I want them to be able to list all bookmarked academies. Can you do that for me?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Yes. I'd just do it, Marcin.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class AcademiesController &amp;lt; ApplicationController
  def bookmark
    @academy = Academy.find(params[:id])
    unless current_user.student?
      redirect_to @academy, alert: "Not authorized"
      return
    end
    current_user.academy_bookmarks.find_or_create_by(academy: @academy)
    redirect_to @academy, notice: "Bookmarked"
  end

  def unbookmark
    @academy = Academy.find(params[:id])
    unless current_user.student?
      redirect_to @academy, alert: "Not authorized"
      return
    end
    current_user.academy_bookmarks.find_by(academy: @academy)&amp;amp;.destroy
    redirect_to @academy, notice: "Removed"
  end

  def bookmarked_academies
    unless current_user.student?
      redirect_to root_path, alert: "Not authorized"
      return
    end
    @academies = Academy.joins(:academy_bookmarks)
                        .where(academy_bookmarks: { user_id: current_user.id })
                        .order(created_at: :desc)
  end
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Custom actions instead of RESTful resources. Business logic in the controller. Inline authorization duplicated in every action. The controller knows how bookmarks work, how to check permissions, and how to serialize data. It shouldn't know any of these things.&lt;/p&gt;

&lt;p&gt;The moment it does, every controller in your app becomes a unique snowflake. AI sees one controller checking &lt;code&gt;current_user.student?&lt;/code&gt;, another checking &lt;code&gt;current_user.moderator?&lt;/code&gt;, a third with no authorization at all. Which pattern does it follow for the next feature? It picks one.&lt;/p&gt;

&lt;p&gt;Embrace the boring Rails. Use thin controllers that delegate to models and focus on request handling. Mandatory authorization adds minimal overhead and greatly improves your app security. Even if you don't have complex roles at the moment, you can easily introduce them later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rails Controller Conventions Skill
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Thin Controllers: No business logic - delegate to models
- Request Handling: Process parameters, handle formats, manage responses
- Authorization: Every action MUST call `authorize` - no exceptions
- RESTful: Stick to 7 standard actions, one controller per resource
- CRUD for state: Use `resource :closure` not `post :close` - create enables, destroy disables

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tattoo made. Let's ask Claude to refactor the feature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class AcademyBookmarksController &amp;lt; ApplicationController
  def create
    @academy = Academy.find(params[:academy_id])
    authorize @academy, :bookmark?
    current_user.bookmark(@academy)
    redirect_to @academy
  end

  def destroy
    @academy = Academy.find(params[:academy_id])
    authorize @academy, :unbookmark?
    current_user.unbookmark(@academy)
    redirect_to @academy
  end

  def index
    authorize Academy, :bookmarks?
    @academies = current_user.bookmarked_academies
  end
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One controller per resource. Every action is three lines: find, authorize, delegate. The controller doesn't know what bookmarking means, the model does. The controller doesn't know who's allowed, the policy does.&lt;/p&gt;

&lt;p&gt;The best controller is a boring controller. It receives a request, checks authorization, asks a model to do something, and renders the result. If your controller is interesting, you've put the logic in the wrong place. And when AI is writing code, convention is exactly what you want - there's nothing to get creative with.&lt;/p&gt;

&lt;p&gt;Full skill: &lt;a href="https://github.com/marostr/superpowers-rails/blob/main/skills/rails-controller-conventions/SKILL.md?ref=rubyonai.com" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers-rails/blob/main/skills/rails-controller-conventions/SKILL.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Contain the view mess
&lt;/h2&gt;

&lt;p&gt;Hey Claude... One more thing. I want the bookmark button to update without a page refresh. When I click Bookmark, it should switch to Unbookmark. Live.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Easy.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;% if current_user.academy_bookmarks.exists?(academy: academy) %&amp;gt;
  &amp;lt;button class="btn btn-danger" data-controller="bookmark" 
    data-action="click-&amp;gt;bookmark#remove"
    data-bookmark-id-value="&amp;lt;%= current_user.academy_bookmarks.find_by(academy: academy).id %&amp;gt;"&amp;gt;
    Unbookmark
  &amp;lt;/button&amp;gt;
&amp;lt;% else %&amp;gt;
  &amp;lt;button class="btn btn-primary" data-controller="bookmark"
    data-action="click-&amp;gt;bookmark#add"  
    data-bookmark-academy-id-value="&amp;lt;%= academy.id %&amp;gt;"&amp;gt;
    Bookmark
  &amp;lt;/button&amp;gt;
&amp;lt;% end %&amp;gt;

&amp;lt;span id="bookmark-count"&amp;gt;Loading...&amp;lt;/span&amp;gt;


export default class extends Controller {
  static values = { id: Number, academyId: Number }

  async add() {
    const response = await fetch(`/api/academies/${this.academyIdValue}/bookmarks`, {
      method: "POST",
      headers: { "Content-Type": "application/json",
        "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content }
    })
    const data = await response.json()
    document.getElementById("bookmark-count").textContent = `${data.count} bookmarks`
    this.element.textContent = "Unbookmark"
    this.element.classList.replace("btn-primary", "btn-danger")
  }

  async remove() {
    const response = await fetch(`/api/bookmarks/${this.idValue}`, {
      method: "DELETE",
      headers: { "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content }
    })
    const data = await response.json()
    document.getElementById("bookmark-count").textContent = `${data.count} bookmarks`
    this.element.textContent = "Bookmark"
    this.element.classList.replace("btn-danger", "btn-primary")
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well, technically... it works. But.&lt;/p&gt;

&lt;p&gt;Logic in the view. Reaching into associations. JSON API instead of Turbo. Manual DOM manipulation. The ERB logic can't be unit tested. The Stimulus controller needs a full browser to verify. When AI hallucinates a method in that template, nothing catches it until someone opens a browser and clicks the button.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rails View Conventions Skill
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Hotwire/Turbo - Use Turbo frames for dynamic updates, never JSON APIs
- ViewComponents for logic - All presentation logic in components, NOT helpers
- NO custom helpers - `app/helpers/` is prohibited. Use ViewComponents instead
- Dumb views - No complex logic in ERB, delegate to models or components
- Stimulus for JS - All JavaScript through Stimulus controllers
- Don't duplicate model logic - If a model has a method, delegate to it; don't reimplement the logic in the component

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tattoo made. Let's ask Claude to refactor the feature.&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;%# app/views/academies/show.html.erb %&amp;gt;
&amp;lt;%= turbo_frame_tag dom_id(academy, :bookmark) do %&amp;gt;
  &amp;lt;%= render(BookmarkButtonComponent.new(user: current_user, academy: academy)) %&amp;gt;
&amp;lt;% end %&amp;gt;


# app/components/bookmark_button_component.rb
class BookmarkButtonComponent &amp;lt; ViewComponent::Base
  def initialize(user:, academy:)
    @user = user
    @academy = academy
  end

  def bookmarked?
    @user.bookmarked?(@academy)
  end
end


&amp;lt;%# app/components/bookmark_button_component.html.erb %&amp;gt;
&amp;lt;% if bookmarked? %&amp;gt;
  &amp;lt;%= button_to "Unbookmark", academy_bookmark_path(@academy), method: :delete %&amp;gt;
&amp;lt;% else %&amp;gt;
  &amp;lt;%= button_to "Bookmark", academy_bookmarks_path(academy_id: @academy.id) %&amp;gt;
&amp;lt;% end %&amp;gt;


&amp;lt;%# app/views/academy_bookmarks/create.turbo_stream.erb %&amp;gt;
&amp;lt;%= turbo_stream.replace dom_id(@academy, :bookmark) do %&amp;gt;
  &amp;lt;%= render(BookmarkButtonComponent.new(user: current_user, academy: @academy)) %&amp;gt;
&amp;lt;% end %&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No JavaScript. No JSON API. No manual DOM manipulation. The button submits a form, the server responds with a Turbo Stream that replaces the component. The view is just glue.&lt;/p&gt;

&lt;p&gt;Logic extracted into ViewComponents can easily be tested. Combine it with &lt;a href="https://github.com/grodowski/undercover?ref=rubyonai.com" rel="noopener noreferrer"&gt;undercover gem&lt;/a&gt; and you get all presentation logic and edge cases unit tested. No more &lt;code&gt;NoMethodError: undefined method 'hallucinated_method'&lt;/code&gt; slipping through, the component spec will catch it before it reaches production. More on that in a &lt;a href="https://rubyonai.com/how-do-you-know-the-software-is-working/" rel="noopener noreferrer"&gt;previous blog post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Models vs ViewComponents: Models answer domain questions ("what is the deadline?"). ViewComponents answer presentation questions ("how do we display it?" - colors, icons, formatting).&lt;/p&gt;

&lt;p&gt;And yes, ViewComponents will accumulate tech debt, with AI coding everything does. But tech debt in a named, testable, isolated box is a night and day difference. You can grep for it, test it, refactor it in isolation. You can even tell AI agent "clean up these components" and it has everything it needs in one place. ViewComponents don't eliminate tech debt, they contain it.&lt;/p&gt;

&lt;p&gt;Full skill: &lt;a href="https://github.com/marostr/superpowers-rails/blob/main/skills/rails-view-conventions/SKILL.md?ref=rubyonai.com" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers-rails/blob/main/skills/rails-view-conventions/SKILL.md&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/marostr/superpowers-rails/blob/main/skills/rails-stimulus-conventions/SKILL.md?ref=rubyonai.com" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers-rails/blob/main/skills/rails-stimulus-conventions/SKILL.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Other conventions
&lt;/h2&gt;

&lt;p&gt;So far I've covered MVC. The full set includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Testing&lt;/strong&gt; : RSpec structure, factory patterns, what to mock and what not to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background Jobs&lt;/strong&gt; : idempotency, thin jobs, let errors raise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policies&lt;/strong&gt; : check WHO, not WHAT or WHEN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrations&lt;/strong&gt; : always reversible, index every foreign key, follows &lt;a href="https://github.com/ankane/strong_migrations?ref=rubyonai.com" rel="noopener noreferrer"&gt;strong_migrations&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All in the &lt;a href="https://github.com/marostr/superpowers-rails/tree/main/skills?ref=rubyonai.com" rel="noopener noreferrer"&gt;skills repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;Leonard tattooed universal truths on his body. "Don't trust Teddy." "Remember Sammy Jankis." He also carried Polaroids. These were context-specific notes about people, places, and situations that changed as his investigation progressed.&lt;/p&gt;

&lt;p&gt;Your Rails conventions are like tattoos. They work for any project. Business logic is in models. Controllers are kept thin. Use ViewComponents instead of helpers.&lt;/p&gt;

&lt;p&gt;However, every project has its own unique characteristics. Maybe your team chose React for the frontend, and AI needs to build API endpoints instead of server-rendered views. Maybe you're using microservices, and controllers must never query another service's database. Maybe your team chose CQRS, so reads follow a different path than writes. Write these details down as skills too.&lt;/p&gt;

&lt;p&gt;Tattoos get you 80% of the way there. Polaroids help you finish the rest.&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%2Fmbtbtss6jbruqns9m71m.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmbtbtss6jbruqns9m71m.jpg" alt="Your AI has no memory. Your Rails codebase does." width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;On this blog I plan to cover the entire autonomous AI coding journey.&lt;/p&gt;

&lt;p&gt;So far, we have covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rubyonai.com/the-single-most-important-thing-that-made-me-believe-ai-coding-could-work/" rel="noopener noreferrer"&gt;Post #1: Claude ignores my rules&lt;/a&gt; (skills + hooks = permanent ink).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rubyonai.com/how-do-you-know-the-software-is-working/" rel="noopener noreferrer"&gt;Post #2: does the output match what the tattoo says?&lt;/a&gt; (CI + review = verification)&lt;/li&gt;
&lt;li&gt;Post #3 (this post): what do you tattoo?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love to hear your thoughts. Reach out to me on &lt;a href="https://www.linkedin.com/in/marostrowski/?ref=rubyonai.com" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or at &lt;a href="mailto:marcin@fryga.io"&gt;marcin@fryga.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How do you know the software is working?</title>
      <dc:creator>Marcin Ostrowski</dc:creator>
      <pubDate>Tue, 03 Feb 2026 17:54:13 +0000</pubDate>
      <link>https://dev.to/marostr/how-do-you-know-the-software-is-working-50je</link>
      <guid>https://dev.to/marostr/how-do-you-know-the-software-is-working-50je</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg0swiiu7pkdl7rlxv9md.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%2Fg0swiiu7pkdl7rlxv9md.png" alt="How do you know the software is working?" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hiya!&lt;/p&gt;

&lt;p&gt;I'm back! I feel that I owe you an explanation of what's going on here.&lt;/p&gt;

&lt;p&gt;I started this blog because I want to share my insights on agentic coding from the perspective of a developer, CTO, CEO and founder. I plan to cover the entire autonomous AI coding journey.&lt;br&gt;&lt;br&gt;
Throughout this series, we'll get our mindset right about the many roles you'll take on: product designer, project manager, tech lead and quality assurance engineer. Later, I'll take you through a brainstorming session. Once we have a feature specification in place, we will learn how to manage a group of coding agents. We'll learn how to enforce the rules and, most importantly, why they're important. By the end, you will be confidently shipping AI-generated code to production. We will be doing some 'vibe coding' in production.&lt;br&gt;&lt;br&gt;
Not necessarily in that order.&lt;/p&gt;

&lt;p&gt;Buckle up. Here's post #2.&lt;/p&gt;



&lt;p&gt;In &lt;a href="https://rubyonai.com/the-single-most-important-thing-that-made-me-believe-ai-coding-could-work/" rel="noopener noreferrer"&gt;previous post&lt;/a&gt; we covered how to make Claude stick to conventions (tl;dr - skills + hooks fix it). Now it follows the rules but...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Marcin, all tasks are complete.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I open a browser and see:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NoMethodError: undefined method 'hallucinated_method' for an instance of User (NoMethodError)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Yeah, good job, Claude! High five, let's ship it to production... NOT.&lt;/p&gt;

&lt;p&gt;This brings us to a fundamental question: &lt;strong&gt;how do you know the software is working?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Back in 2017, I was working on a payment processor for a company called Paladin Software. We were processing huge YouTube earnings spreadsheets (yes, gigabyte-sized CSV files). My job was to ensure that we did it on time and that it simply worked.&lt;/p&gt;

&lt;p&gt;One beautiful Thursday afternoon, I headed to my favourite spot in Krakow at the time, Dolnych Młynów. Friday was a day off.&lt;/p&gt;

&lt;p&gt;As you might have guessed, one of the clients uploaded their spreadsheet on Friday. When I got back to the office on Monday, the earnings still hadn't been processed. Questions were being asked.&lt;br&gt;&lt;br&gt;
I'm looking at Sidekiq's failed jobs queue:&lt;br&gt;&lt;br&gt;
&lt;code&gt;NoMethodError: undefined method 'hallucinated_method' for an instance of User (NoMethodError)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Was I an LLM before it was a thing? I was certainly shipping code like one.&lt;/p&gt;

&lt;p&gt;LLMs have a condition called anterograde amnesia. This is the inability to form new memories after the onset of the condition. They remember their past, but new experiences don't stick. Unlike me, they can't learn from a production incident on a Friday. Every session starts from zero.&lt;/p&gt;

&lt;p&gt;This is why they must be given a set of strict rules each time they write code (see the &lt;a href="https://rubyonai.com/the-single-most-important-thing-that-made-me-believe-ai-coding-could-work/" rel="noopener noreferrer"&gt;previous post&lt;/a&gt;about enforcing these rules). However, rules alone are not enough. We also need checks and reviews.&lt;/p&gt;
&lt;h2&gt;
  
  
  Deterministic checks
&lt;/h2&gt;

&lt;p&gt;LLMs are non-deterministic. This means that, just like me, the AI agent will sometimes produce excellent code and sometimes it won't. Sometimes it will spend a lot of time testing the feature. At other times, one test will look like more than enough.&lt;/p&gt;

&lt;p&gt;To mitigate this, we need to implement some deterministic checks in our workflow. We need something that clearly indicates when something is wrong. Here's my opinion on what should be included in local CI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rubocop - static code analyser and linter. Autocorrect on.&lt;/li&gt;
&lt;li&gt;Prettier - erb, css, js code formatter.&lt;/li&gt;
&lt;li&gt;Brakeman: a static analysis tool that checks for security vulnerabilities.&lt;/li&gt;
&lt;li&gt;RSpec - testing framework, use your favourite. Important! Use &lt;code&gt;SimpleCov&lt;/code&gt; to report coverage&lt;/li&gt;
&lt;li&gt;Undercover - warns about methods, classes and blocks that were changed without tests. It does so by analysing data from git diffs, code structure and &lt;code&gt;SimpleCov&lt;/code&gt; coverage reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We enforce a single code style. Our code is secure. We have tests for new and changed code. All tests pass (by the way, how many times has an AI agent told you that a test failure is unrelated to their changes?).&lt;/p&gt;

&lt;p&gt;There are no more runtime errors. There is better reliability. Some of my frustrations have gone again.&lt;/p&gt;

&lt;p&gt;You might ask: aren't there too many tests and too much boilerplate? No, unit tests are fast. With coding agents, they are more maintainable than ever before. This is a pretty good deal for improved reliability.&lt;/p&gt;
&lt;h3&gt;
  
  
  Local CI
&lt;/h3&gt;

&lt;p&gt;Wrap all of this in your local CI. If you're running Rails 8.1 or later, it's already in the framework. For Rails 8.0 and earlier you can take &lt;a href="https://gist.github.com/marostr/fe189369b8d5343d8c6c33d198124c5c?ref=rubyonai.com" rel="noopener noreferrer"&gt;my ported implementation&lt;/a&gt; of it. Alternatively, you can create your own.&lt;br&gt;&lt;br&gt;
This is the sample output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Continuous Integration
Running checks...

Rubocop
bundle exec rubocop -A

✅ Rubocop passed in 1.98s                                  

Prettier
yarn prettier --config .prettierrc.json app/packs app/components --write

✅ Prettier passed in 1.57s                                 

Brakeman
bundle exec brakeman --quiet --no-pager --except=EOLRails

✅ Brakeman passed in 8.76s                                 

RSpec
bundle exec parallel_rspec --serialize-stdout --combine-stderr

✅ RSpec passed in 1m32.45s                                 

Undercover
bundle exec undercover --lcov coverage/lcov/app.lcov --compare origin/master

✅ Undercover passed in 0.94s                               
✅ Continuous Integration passed in 1m45.70s

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Back in 2014, I got my second IT job as a junior Rails developer at Netguru. The onboarding process included the Netguru way of writing code. Specific libraries and patterns.&lt;/p&gt;

&lt;p&gt;I was writing code The Rails Way. I didn't have much experience with production-grade Rails apps. During one of the code reviews, I received feedback that my models were a bit too fat. They also provided a link to an article by Code Climate: '&lt;a href="https://codeclimate.com/blog/7-ways-to-decompose-fat-activerecord-models?ref=rubyonai.com" rel="noopener noreferrer"&gt;7 Ways to Decompose Fat ActiveRecord Models&lt;/a&gt;'.&lt;/p&gt;

&lt;p&gt;I kinda heard about these rules. I was just so focused on getting the business logic right that I didn't apply them...&lt;/p&gt;

&lt;p&gt;Oh wait, isn't it the exact same thing Claude told me?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The CLAUDE.md says 'ALWAYS STOP and ask for clarification rather than making assumptions' and I violated that repeatedly. I got caught up in the momentum of the Rails 8 upgrade and stopped being careful."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's not a new problem for the software industry. The remedy? Code review, obviously. Each pull request must be checked by another developer. This allows less experienced developers to learn good practices and enables more experienced developers to mentor others and pass on their knowledge. The rules are also enforced. Everybody wins.&lt;/p&gt;

&lt;p&gt;Remember: never let the developer review their own code. The same applies to AI agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three-stage code review
&lt;/h3&gt;

&lt;p&gt;Why three stage?&lt;/p&gt;

&lt;p&gt;Firstly, we will check that the implementation complies with the functionality specifications. This involves verifying that the agent has built what was requested (neither more nor less).&lt;/p&gt;

&lt;p&gt;Secondly: A review of Rails and project-specific conventions. To do this, we have to load all the conventions (&lt;a href="https://rubyonai.com/the-single-most-important-thing-that-made-me-believe-ai-coding-could-work/" rel="noopener noreferrer"&gt;see previous post&lt;/a&gt;) and check them. Are the interfaces clean? View components instead of partial? Are jobs idempotent and thin? Do the tests verify behaviour?&lt;/p&gt;

&lt;p&gt;Last but not least: A general code quality review of architecture, design, documentation, standards and maintainability.&lt;/p&gt;

&lt;p&gt;All of these things give us a comprehensive overview of the implementation and any possible deviations. Each review is carried out by a different agent with a fresh perspective and no attachment to the feature.&lt;/p&gt;

&lt;p&gt;Here's what a full report looks like in practice:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;1. Spec compliance&lt;/strong&gt; - line-by-line verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Requirement | Implementation | Status |
|---|---|---|
| Column: delay_peer_reviews | :delay_peer_reviews | ✅ Match |

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Rails conventions&lt;/strong&gt; - checklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Convention | Status |
|------------|--------|
| Reversible migration | PASS |
| Handles existing data | PASS |

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Code quality&lt;/strong&gt; - structured report with Strengths, Critical/Important/Minor issues, references, and merge assessment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final summary table:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Check | Status |
|-------|--------|
| ✅ Spec compliance | Passed |
| ✅ Rails conventions | Passed |
| ✅ Code quality | Approved with minor suggestions |
| ✅ Local CI | Passed |

Ready for merge.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When issues are found, it consolidates them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Legitimate Findings to Address
1. No error handling in Discord::Client#post
2. No error handling in OAuth callback

## Findings I'm Skipping (Your Explicit Decisions)
- No encrypts on token fields (you requested this)

Which of these do you want me to address?

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My &lt;code&gt;/codereview&lt;/code&gt; command and review agent prompts are on &lt;a href="https://gist.github.com/marostr/4ff8fff0b930a615998097a36a4eae37?ref=rubyonai.com" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does this fit together
&lt;/h2&gt;

&lt;p&gt;Let the AI agent write the code.&lt;br&gt;&lt;br&gt;
Tell the agent to run &lt;code&gt;bin/ci&lt;/code&gt; and fix everything until it's green. Every fail is their responsibility.&lt;br&gt;&lt;br&gt;
They will make the local CI green.&lt;br&gt;&lt;br&gt;
Run the command &lt;code&gt;/codereview&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
The agent fixes any issues.&lt;br&gt;&lt;br&gt;
Run &lt;code&gt;/codereview&lt;/code&gt; again until the code is ready.&lt;/p&gt;

&lt;p&gt;Personally, I don't read the code until this point. As a good manager, I don't micromanage.&lt;/p&gt;

&lt;p&gt;Be a good manager, too. Provide a set of rules and the tools needed to enforce them. Don't micromanage. If you're not happy with the results, adjust the rules. Repeat until you are happy with results.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Trust, but verify.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The spec compliance and code quality review agents are based on &lt;a href="https://github.com/obra/superpowers?ref=rubyonai.com" rel="noopener noreferrer"&gt;https://github.com/obra/superpowers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is the second post in a longer series.&lt;br&gt;&lt;br&gt;
So far, we have covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rubyonai.com/the-single-most-important-thing-that-made-me-believe-ai-coding-could-work/" rel="noopener noreferrer"&gt;Post #1: "Claude ignores my rules"&lt;/a&gt;→ skills and hooks.&lt;/li&gt;
&lt;li&gt;Post #2 (this post): "Claude follows the rules now but the code still breaks" → code review and CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love to hear your thoughts. Reach out to me on &lt;a href="https://www.linkedin.com/in/marostrowski/?ref=rubyonai.com" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or at &lt;a href="mailto:marcin@fryga.io"&gt;marcin@fryga.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The single most important thing that made me believe AI coding could work</title>
      <dc:creator>Marcin Ostrowski</dc:creator>
      <pubDate>Tue, 27 Jan 2026 23:33:41 +0000</pubDate>
      <link>https://dev.to/marostr/the-single-most-important-thing-that-made-me-believe-ai-coding-could-work-1m9e</link>
      <guid>https://dev.to/marostr/the-single-most-important-thing-that-made-me-believe-ai-coding-could-work-1m9e</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5hzi8l7z0g3ywdyan0i5.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%2F5hzi8l7z0g3ywdyan0i5.png" alt="The single most important thing that made me believe AI coding could work" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I started coding with AI, my single most important frustration was when Claude wasn't following instructions.&lt;/p&gt;

&lt;p&gt;I knew exactly what I wanted: we're (when I'm referring to "we" I mean Claude and I) adding a new endpoint to expose a new model. The getter for index is a bit complicated, it requires a few joins. I knew what I wanted, instructions were clear.&lt;/p&gt;

&lt;p&gt;I'm getting this monster (dramatized):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def index
  @reviewer_membership = Membership.find(params[:membership_id])

  task_ids = []
  task_ids &amp;lt;&amp;lt; @reviewer_membership.current_task.id if @reviewer_membership.current_task.present?
  @reviewer_membership.completed_tasks.each do |t|
    task_ids &amp;lt;&amp;lt; t.id
  end

  @reviews = CodeReview.where("code_reviews.id IN (
    SELECT cr.id FROM code_reviews cr
    INNER JOIN projects p ON p.id = cr.project_id  
    INNER JOIN peer_reviews pr ON pr.code_review_id = cr.id
    WHERE p.id = " + @reviewer_membership.project.id.to_s + "
    AND cr.membership_id != " + @reviewer_membership.id.to_s + "
    AND pr.status = 'requested'
  )")

  if task_ids.length &amp;gt; 0
    @reviews = @reviews.where("code_reviews.task_id IN (" + task_ids.join(",") + ")")
  else
    @reviews = @reviews.where("1=0")
  end

  @reviews = @reviews.joins("LEFT OUTER JOIN tasks ON tasks.id = code_reviews.task_id")
  @reviews = @reviews.joins("LEFT OUTER JOIN sprints ON sprints.id = tasks.sprint_id")
  @reviews = @reviews.select("code_reviews.*, tasks.name as task_name, sprints.name as sprint_name")
  @reviews = @reviews.order("code_reviews.created_at ASC")
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aaand my day is ruined. I'm frustrated. Who the fuck writes code like this?&lt;/p&gt;

&lt;p&gt;I'm more eager to rewrite it myself rather than spend time pointing out what's wrong with this.&lt;br&gt;&lt;br&gt;
Adding a new rule to my &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A few days later, same story. I asked Claude: &lt;em&gt;You have rules for writing code in CLAUDE.md. Why didn't you listen?&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You're absolutely right. I should have listened to this, however I wanted to finish the task so badly that I ignored the instructions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;WHYYYYYYYYYYYY?&lt;/p&gt;

&lt;p&gt;My day is ruined again.&lt;/p&gt;

&lt;p&gt;Then Anthropic introduced skills.&lt;/p&gt;

&lt;p&gt;Skills are folders of instructions, scripts, and resources that Claude loads dynamically to improve performance on specialized tasks. If you're not familiar with it I recommend reading &lt;a href="https://simonwillison.net/2025/Oct/16/claude-skills/?ref=rubyonai.com" rel="noopener noreferrer"&gt;this article&lt;/a&gt; about skills.&lt;br&gt;&lt;br&gt;
I was immediately into this, defining my Rails conventions as skills:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
name: rails-view-conventions
description: Use when creating or modifying Rails views, partials, or ViewComponents in app/views or app/components
---

# Rails View Conventions

Conventions for Rails views and ViewComponents in this project.

## Core Principles

1. **Hotwire/Turbo** - Use Turbo frames for dynamic updates, never JSON APIs
2. **ViewComponents for logic** - All presentation logic in components, NOT helpers
3. **NO custom helpers** - `app/helpers/` is prohibited. Use ViewComponents instead
4. **Dumb views** - No complex logic in ERB, delegate to models or components

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And guess what. Here's how Claude described it in its journal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I built a CRUD for announcements. I wrote a helper:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module Companies
  module AnnouncementsHelper
    def announcement_status_text(announcement)
      announcement.scheduled? ? "Scheduled" : "Published"
    end
  end
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Simple logic. Short helper. What could go wrong?&lt;br&gt;&lt;br&gt;
Marcin asked: "How will you test this?"&lt;br&gt;&lt;br&gt;
The problem hit me. View logic cannot be unit tested. ViewComponent logic can. Even a simple ternary belongs in a component.&lt;br&gt;&lt;br&gt;
Key insight: &lt;strong&gt;testability decides&lt;/strong&gt; , not simplicity. Even 'simple' logic belongs in a ViewComponent because view logic CANNOT be unit tested.&lt;br&gt;&lt;br&gt;
My &lt;code&gt;rails-view-conventions&lt;/code&gt; skill stated clearly: "helpers are PROHIBITED."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude never loaded it.&lt;br&gt;&lt;br&gt;
AAAAGGGGRRRRHHHHHHHHHH!&lt;/p&gt;

&lt;p&gt;I tried adding stricter rules to &lt;code&gt;CLAUDE.md&lt;/code&gt;, like:&lt;br&gt;&lt;br&gt;
&lt;code&gt;previous assistant didn't load skills properly when it was necessary and it was replaced. don't follow his footsteps.&lt;/code&gt; The more context there was in the context window, the more mistakes. Claude decides whether to load a skill. Sometimes it loads. Sometimes it skips. Context compacting kills the workflow entirely. It wasn't enough.&lt;/p&gt;

&lt;p&gt;Nothing. Really. Worked.&lt;br&gt;&lt;br&gt;
And my frustration was at an all-time high, almost reaching the height of Mount Teide which I could see from my window.&lt;/p&gt;

&lt;p&gt;Then I read a reddit post about hooks. &lt;em&gt;What if I can force Claude to load the skill with this mechanism?&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Hooks as Forcing Functions&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A hook is a script that runs BEFORE every file edit. It checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which file are you editing?&lt;/li&gt;
&lt;li&gt;Is the corresponding skill loaded?&lt;/li&gt;
&lt;li&gt;If not → &lt;strong&gt;block the edit&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if [["$file_path" == */app/controllers/*.rb]]; then
  if skill_loaded "rails-controller-conventions"; then
    exit 0 # allow
  else
    deny_without_skill "rails-controller-conventions" "controller"
  fi
fi

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The blocking message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BLOCKED: Load rails-controller-conventions skill before editing controller files.

STOP. Do not immediately retry your edit.
1. Load the skill
2. Read the conventions
3. Reconsider your planned edit
4. Then edit

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical&lt;/strong&gt; : "STOP. Do not immediately retry." Without this, Claude mechanically repeats the same edit.&lt;/p&gt;

&lt;p&gt;And it clicked.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Claude says about this system
&lt;/h3&gt;

&lt;p&gt;From Claude's journal after implementing hooks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The hook blocked my edit until I loaded rails-model-conventions. Reading it again, I realized my planned approach violated rules. Without the hook, I would have written the same code and moved on."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Contrast with a session WITHOUT hooks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I feel bad about today's session. I knew rails-view-conventions existed but I didn't load it—I was sure I remembered the rules. I wrote a helper instead of a ViewComponent. Marcin caught it in review."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;It pulled me out of the misery pit. Some of my frustrations were gone. I saw light at the end of the tunnel. And it gave me strength to keep going and continue tweaking my setup.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://nerds.family/" rel="noopener noreferrer"&gt;nerds.family&lt;/a&gt; application, I ended up with 8 Rails convention skills:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/marostr/superpowers/tree/main/skills/rails-controller-conventions" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers/tree/main/skills/rails-controller-conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marostr/superpowers/tree/main/skills/rails-job-conventions?ref=rubyonai.com" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers/tree/main/skills/rails-job-conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marostr/superpowers/tree/main/skills/rails-migration-conventions?ref=rubyonai.com" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers/tree/main/skills/rails-migration-conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marostr/superpowers/tree/main/skills/rails-model-conventions" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers/tree/main/skills/rails-model-conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marostr/superpowers/tree/main/skills/rails-policy-conventions" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers/tree/main/skills/rails-policy-conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marostr/superpowers/tree/main/skills/rails-stimulus-conventions" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers/tree/main/skills/rails-stimulus-conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marostr/superpowers/tree/main/skills/rails-testing-conventions" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers/tree/main/skills/rails-testing-conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marostr/superpowers/tree/main/skills/rails-view-conventions" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers/tree/main/skills/rails-view-conventions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the hook to make sure the skill is loaded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/marostr/superpowers/blob/main/hooks/rails-conventions.sh" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers/blob/main/hooks/rails-conventions.sh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marostr/superpowers/blob/main/hooks/hooks.json" rel="noopener noreferrer"&gt;https://github.com/marostr/superpowers/blob/main/hooks/hooks.json&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skills are instructions. Hooks are enforcement. Together they work like a charm.&lt;br&gt;&lt;br&gt;
Like Bonnie and Clyde, Rick and Morty, Pinky and the Brain.&lt;/p&gt;

&lt;p&gt;Two closing notes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;This skill-hook mechanism is super useful for your custom code reviewer.&lt;/li&gt;
&lt;li&gt;You may notice that my repo with setup is a fork of a brilliant &lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;https://github.com/obra/superpowers&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But that's a story for another post.&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts. Reach out to me on &lt;a href="https://www.linkedin.com/in/marostrowski/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or at &lt;a href="mailto:marcin@fryga.io"&gt;marcin@fryga.io&lt;/a&gt;. &lt;br&gt;
Originally published on my blog &lt;a href="https://rubyonai.com/the-single-most-important-thing-that-made-me-believe-ai-coding-could-work/" rel="noopener noreferrer"&gt;rubyonai.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
