<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thoughtbot="https://thoughtbot.com/feeds/">
  <title>Giant Robots Smashing Into Other Giant Robots</title>
  <subtitle>Written by thoughtbot, your expert partner for design and development.
</subtitle>
  <id>https://robots.thoughtbot.com/</id>
  <link href="https://thoughtbot.com/blog"/>
  <link href="https://feed.thoughtbot.com" rel="self"/>
  <updated>2026-06-08T00:00:00+00:00</updated>
  <author>
    <name>thoughtbot</name>
  </author>
<entry>
  <title>Enforcing Your Ruby Style Guide on AI-Generated Code</title>
  <link rel="alternate" href="https://thoughtbot.com/blog/enforcing-your-ruby-style-guide-on-ai-generated-code"/>
  <author>
    <name>Daniel Garcia</name>
  </author>
  <id>https://thoughtbot.com/blog/enforcing-your-ruby-style-guide-on-ai-generated-code</id>
  <published>2026-06-08T00:00:00+00:00</published>
  <updated>2026-06-05T14:53:08Z</updated>
  <content type="html">&lt;p&gt;As AI-assisted software development becomes more widely adopted, more of the Ruby code in our Rails apps is being
written by agents. Each team has its own conventions for how that code should look and behave, and we want those
conventions enforced automatically rather than relying on the agent to remember them on its own. This is part of a
broader practice called harness engineering, using tools, guardrails, validators, and persistence to increase the
probability that our agents produce the outcomes we want. A capable model is only part of the equation. The rest is
everything we put around it, including the context it operates within, the rules it follows, and the checks that
catch its mistakes.&lt;/p&gt;

&lt;p&gt;The concept of harness engineering in software development is still in its early stages and there aren’t many
resources on how to implement an agent harness within the context of Rails applications. At thoughtbot, we’re
experimenting with how to encode how we work into various tools and contexts in order to increase the quality of the
AI output. This post walks through one specific piece of the harness we’ve been building. It’s a Claude Code hook
that runs RuboCop against any Ruby files the agent touches, gives the agent a chance to fix what it can, and
surfaces what it can’t.&lt;/p&gt;
&lt;h2 id="rules-as-the-first-layer"&gt;
  
    Rules as the First Layer
  
&lt;/h2&gt;

&lt;p&gt;We recently released a &lt;a href="https://github.com/thoughtbot/guides/tree/main/rails/ai-rules"&gt;set of Claude Code rules&lt;/a&gt;
designed to be dropped into a project’s &lt;code&gt;.claude/&lt;/code&gt; directory so that coding agents can follow thoughtbot’s Rails
conventions when writing code. It aims to ensure that when coding agents generate or modify code in a Rails
project, that they adhere to conventions like TDD, RESTful routes, and strong params. You can use this as a
starting point to add information specific to your project and the coding agent will use and update it when doing
work. Think of it as a living memory for your coding agent, keeping track of architectural decisions, edge cases,
and team conventions.&lt;/p&gt;

&lt;p&gt;The rules and context in these files are the
&lt;a href="https://martinfowler.com/articles/harness-engineering.html#FeedforwardAndFeedback"&gt;feedforward&lt;/a&gt;/&lt;a href="https://martinfowler.com/articles/harness-engineering.html#ComputationalVsInferential"&gt;inferential&lt;/a&gt;
aspect of our user harness. They guide the agent before and during work so that it increases the odds of getting
the job right the first time. A linter can flag a 250-line controller action that’s doing too much but it can’t
tell you which of those lines belong in the model. That’s where the agent can really add value, and where a good
set of rules makes the difference.&lt;/p&gt;

&lt;p&gt;But rules alone aren’t enough. A good set of rules and a detailed yet concise &lt;code&gt;CLAUDE.md&lt;/code&gt; file can greatly increase
the quality of the agent’s code, but because results are non-deterministic, it isn’t guaranteed that the agent
won’t make mistakes. This is where adding a
&lt;a href="https://martinfowler.com/articles/harness-engineering.html#FeedforwardAndFeedback"&gt;feedback&lt;/a&gt;/&lt;a href="https://martinfowler.com/articles/harness-engineering.html#ComputationalVsInferential"&gt;computational&lt;/a&gt;
aspect to our user harness can empower agents to fix their own mistakes and produce the results we want with less
and less hand-holding. The rest of this post focuses on one specific feedback loop, using a Claude Code hook to run
RuboCop on the Ruby files the agent has touched, and giving it a chance to fix any violations.&lt;/p&gt;
&lt;h2 id="claude-code-hooks-for-deterministic-behavior"&gt;
  
    Claude Code Hooks for Deterministic Behavior
  
&lt;/h2&gt;

&lt;p&gt;This aspect of the user harness gives us deterministic control over the output of the code by using
&lt;a href="https://code.claude.com/docs/en/hooks-guide"&gt;hooks&lt;/a&gt;. Hooks are custom shell commands, LLM prompts, or HTTP
endpoints we define that can run when certain events happen in Claude Code’s lifecycle. This way, we can enforce
certain actions always run rather than hoping the agent decides to do them.&lt;/p&gt;

&lt;p&gt;Your custom hooks and Claude Code communicate with each other via &lt;code&gt;stdin&lt;/code&gt;, &lt;code&gt;stdout&lt;/code&gt;, &lt;code&gt;stderr&lt;/code&gt;, and exit codes. When
your custom hook is executed, Claude Code passes event-specific data as JSON to your script’s &lt;code&gt;stdin&lt;/code&gt;. Then your
script tells Claude Code what to do next by either writing to &lt;code&gt;stdout&lt;/code&gt; or &lt;code&gt;stderr&lt;/code&gt; with a specific exit code. These
scripts can run linters or prevent the agent from taking destructive actions, for example. An exit code of &lt;code&gt;0&lt;/code&gt;
tells Claude Code to proceed with whatever action it was performing. For many events your script hooks into, an
exit code of &lt;code&gt;2&lt;/code&gt; (with a &lt;code&gt;stderr&lt;/code&gt; message) is used by Claude Code as feedback. Claude Code will use this
information to block whatever event triggered it and take corrective action.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://images.thoughtbot.com/kotmqxqa4xm279jl31m7t3iwlu8g_hook_flow.png" alt="Diagram showing how Claude Code hooks work: a triggered event runs custom logic that either lets Claude continue or blocks and redirects it."&gt;&lt;/p&gt;
&lt;h2 id="enforcing-ruby-style-guide-adherence"&gt;
  
    Enforcing Ruby Style Guide Adherence
  
&lt;/h2&gt;

&lt;p&gt;Lets look at an example with Rubocop. You may already have a pre-commit hook that runs rubocop with the
&lt;code&gt;--autocorect&lt;/code&gt; flag to fix things that are considered safe to auto-fix like style linting rules. Having this in a
pre-commit hook that’s shared across your team, ensures you have a last line of defense when shipping code.
Depending on the plugins you use though, there may be errors that surface which require judgement and reasoning in
order to fix. These are fixes you make manually and that sometimes require knowledge of the architecture and other
parts of the codebase. Injecting Rubocop into an agent’s lifecycle in the form of a hook (in addition to a
pre-commit hook) can increase the trustworthiness of the agent’s output. Violations come back to the agent
immediately while the change is in working memory and the agent can fix them in the same turn. These include fixes
of the more complicated errors that require knowledge of other parts of the codebase. Here’s a simplified setup to
get this up and running on your project.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;.claude/hooks/rubocop-gate.sh&lt;/code&gt;, we’ll add a script that runs Rubocop and instructs the agent on how to fix
errors that may require some reasoning.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE_PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Find Ruby files Claude added, modified, or newly created (not yet tracked).&lt;/span&gt;
ruby_files&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AM HEAD &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'*.rb'&lt;/span&gt; &lt;span class="s1"&gt;'*.rake'&lt;/span&gt; &lt;span class="s1"&gt;'Gemfile'&lt;/span&gt; &lt;span class="s1"&gt;'Rakefile'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    git ls-files &lt;span class="nt"&gt;--others&lt;/span&gt; &lt;span class="nt"&gt;--exclude-standard&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'*.rb'&lt;/span&gt; &lt;span class="s1"&gt;'*.rake'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;RUBY_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ruby_files&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="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUBY_FILES&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;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Second stop attempt: Claude already got one chance to fix violations.&lt;/span&gt;
&lt;span class="c"&gt;# Surface anything still broken, then let it stop.&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="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;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.stop_hook_active'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&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;REMAINING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop &lt;span class="nt"&gt;--force-exclusion&lt;/span&gt; &lt;span class="nv"&gt;$RUBY_FILES&lt;/span&gt; 2&amp;gt;&amp;amp;1&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="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"RuboCop violations remain after one retry. Surfacing for review:"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REMAINING&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="k"&gt;fi
  &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="nv"&gt;OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop &lt;span class="nt"&gt;--force-exclusion&lt;/span&gt; &lt;span class="nt"&gt;--autocorrect&lt;/span&gt; &lt;span class="nv"&gt;$RUBY_FILES&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$STATUS&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2 &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
RuboCop found violations that could not be auto-corrected. Fix them before completing the task.

See .claude/rules/rubocop.md for guidance on how to handle different violation types
(especially Rails, ThreadSafety, and judgment-call cops).

Violations:
&lt;/span&gt;&lt;span class="nv"&gt;$OUTPUT&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The hook runs RuboCop against just the Ruby files in the diff, blocks the agent’s stop event if violations can’t
be auto-corrected, and gives the agent exactly one chance to fix them before stopping work. The &lt;code&gt;stop_hook_active&lt;/code&gt;
field in Claude Code’s JSON payload tells us whether this is Claude’s first attempt to stop work or a retry.
It’s false on Claude’s first stop attempt and true when Claude is retrying after we blocked once. The first time
we run the script, rubocop runs with &lt;code&gt;--autocorrect&lt;/code&gt; and exits 2 if any violations remain. Then, the agent feeds that
output to Claude as the next instruction along with a pointer to &lt;code&gt;.claude/rules/rubocop.md&lt;/code&gt; for guidance on cops
that require a judgement call. If it can’t fix all the violations, the second rubocop execution skips autocorrect
(we’re only reporting at this point, not changing files), prints any leftover violations to stderr for you to
address, and exits 0 so the agent can stop. Remember to &lt;code&gt;chmod +x&lt;/code&gt; this file.&lt;/p&gt;

&lt;p&gt;Here’s an example &lt;code&gt;.claude/rules/rubocop.md&lt;/code&gt; file. It provides guidance to the agent on how to fix errors that
require some reasoning. It’s based on the cops we use at thoughtbot. These instructions will vary depending on
which Rubocop plugins you use and your team’s preferences but it provides a good starting point.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## RuboCop conventions&lt;/span&gt;

Some cops require judgment that autocorrect can't apply. When RuboCop
surfaces one of them, the rules below help decide how to respond.

Don't reach for inline &lt;span class="sb"&gt;`# rubocop:disable`&lt;/span&gt; or &lt;span class="sb"&gt;`# rubocop:todo`&lt;/span&gt; to make
violations go away. If a cop genuinely doesn't fit this codebase, surface it in your final response.

&lt;span class="gu"&gt;### Rails/OutputSafety&lt;/span&gt;
Never silence &lt;span class="sb"&gt;`Rails/OutputSafety`&lt;/span&gt; — &lt;span class="sb"&gt;`html_safe`&lt;/span&gt; and &lt;span class="sb"&gt;`raw`&lt;/span&gt; are XSS vectors.
If you think a specific use is safe, surface it and let the user decide.

&lt;span class="gu"&gt;### ThreadSafety&lt;/span&gt;

Never silence ThreadSafety violations. These cops catch real concurrency
bugs and the right fix usually depends on architectural context.
&lt;span class="p"&gt;
1.&lt;/span&gt; Describe what the cop caught.
&lt;span class="p"&gt;2.&lt;/span&gt; List the possible fixes — typically &lt;span class="sb"&gt;`RequestStore`&lt;/span&gt;/&lt;span class="sb"&gt;`Current`&lt;/span&gt;, instance
   state, a frozen constant, a mutex, or accepting the violation if the app
   runs single-threaded.
&lt;span class="p"&gt;3.&lt;/span&gt; Wait for direction.

&lt;span class="gu"&gt;### Surface, don't refactor&lt;/span&gt;

When the obvious fix would change behavior or hurt readability:
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="sb"&gt;`Rails/SkipsModelValidations`&lt;/span&gt; — &lt;span class="sb"&gt;`update_columns`&lt;/span&gt; / &lt;span class="sb"&gt;`update_all`&lt;/span&gt; /
  &lt;span class="sb"&gt;`update_counters`&lt;/span&gt; skip callbacks intentionally for counter caches, audit
  fields, or bulk operations. Don't quietly refactor to &lt;span class="sb"&gt;`update`&lt;/span&gt; — that
  changes behavior. Surface with reasoning.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Rails/HasManyOrHasOneDependent`&lt;/span&gt; — usually a real bug, but occasionally
  the association is intentionally orphan-tolerant. Surface rather than
  picking a &lt;span class="sb"&gt;`dependent:`&lt;/span&gt; value.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`RSpec/MultipleExpectations`&lt;/span&gt;, &lt;span class="sb"&gt;`RSpec/NestedGroups`&lt;/span&gt; — restructuring often
  hurts readability. If the test reads better as-is, surface and say so.
  Readability beats the cop.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`RSpec/AnyInstance`&lt;/span&gt; — usually a real smell but sometimes legitimately
  needed in legacy code.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Lastly, we need to add config to the &lt;code&gt;.claude/settings.json&lt;/code&gt; file in order to register the &lt;code&gt;Stop&lt;/code&gt; hook.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;....&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${CLAUDE_PROJECT_DIR}/.claude/hooks/rubocop-gate.sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, when your agent completes some work that involves adding or modifying Ruby files, it’ll automatically run
Rubocop and attempt to fix any violations that weren’t caught by &lt;code&gt;--autocorrect&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="one-step-further"&gt;
  
    One step further
  
&lt;/h2&gt;

&lt;p&gt;In addition to giving the agent guidance on how to fix certain violations, you may have noticed that the
&lt;code&gt;.claude/rules/rubocop.md&lt;/code&gt; file also provides instructions on which cops should never be silenced. Cops such as
&lt;code&gt;ThreadSafety&lt;/code&gt; or &lt;code&gt;Lint/Debugger&lt;/code&gt; cops. These are cops that if silenced could cause bugs to be shipped to
production. While keeping this as an enforcement rule helps the agent do the right thing the first time around,
we can take this one step further by taking a more deterministic approach. We can explicitly prevent the agent
from silencing certain cops by configuring a &lt;code&gt;.rubocop_strict.yml&lt;/code&gt; file. This will disable the silencing of cops
that may be silenced on a per file bases in the &lt;code&gt;.rubocop_todo.yml&lt;/code&gt; config.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .rubocop_strict.yml&lt;/span&gt;

&lt;span class="na"&gt;Lint/Debugger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# i.e. binding.irb or debugger statements&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

&lt;span class="na"&gt;ThreadSafety/ClassAndModuleAttributes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

&lt;span class="na"&gt;ThreadSafety/ClassInstanceVariable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

&lt;span class="c1"&gt;# ...other cops you don't want disabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .rubocop.yml&lt;/span&gt;

&lt;span class="na"&gt;require&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rubocop-thread_safety&lt;/span&gt;

&lt;span class="na"&gt;inherit_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# .rubocop_strict.yml must go last to override potential excludes in other files&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_todo.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_strict.yml&lt;/span&gt;

&lt;span class="na"&gt;AllCops&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;NewCops&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enable&lt;/span&gt;
  &lt;span class="na"&gt;TargetRubyVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.2&lt;/span&gt;  &lt;span class="c1"&gt;# adjust to your project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For extra confidence that our agent won’t silence certain cops by slapping on a &lt;code&gt;rubocop:disable&lt;/code&gt; or
&lt;code&gt;rubocop:todo&lt;/code&gt; directive, we can also create our own custom cop that deterministically prevents this from
happening. Consider our &lt;code&gt;ThreadSafety&lt;/code&gt; cop example from before.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/rubocop/cops/thread_safety/no_inline_disable.rb&lt;/span&gt;

&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;RuboCop&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Cop&lt;/span&gt;
    &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ThreadSafety&lt;/span&gt;
      &lt;span class="c1"&gt;# Forbids inline directives that disable ThreadSafety cops.&lt;/span&gt;
      &lt;span class="c1"&gt;#&lt;/span&gt;
      &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NoInlineDisable&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;RuboCop&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cop&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
        &lt;span class="no"&gt;MSG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ThreadSafety cops cannot be disabled inline. "&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
              &lt;span class="s2"&gt;"See .claude/rules/rubocop.md for guidance."&lt;/span&gt;

        &lt;span class="no"&gt;DIRECTIVE_REGEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/#\s*rubocop:(?:disable|todo)\s+([^\n]+)/&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_new_investigation&lt;/span&gt;
          &lt;span class="n"&gt;processed_source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
            &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DIRECTIVE_REGEX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;

            &lt;span class="n"&gt;cops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\s*,\s*/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:strip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;cops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_with?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ThreadSafety/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;add_offense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;source_range&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .rubocop_strict.yml&lt;/span&gt;

&lt;span class="c1"&gt;# ... previous config&lt;/span&gt;

&lt;span class="na"&gt;ThreadSafety/NoInlineDisable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
  &lt;span class="na"&gt;Include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.rb'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.rake'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/Rakefile'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/Gemfile'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .rubocop.yml&lt;/span&gt;

&lt;span class="na"&gt;require&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rubocop-thread_safety&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./lib/rubocop/cops/thread_safety_extensions&lt;/span&gt;

&lt;span class="na"&gt;inherit_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# .rubocop_strict.yml must go last to override potential excludes in other files&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_todo.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_strict.yml&lt;/span&gt;

&lt;span class="na"&gt;AllCops&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;NewCops&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enable&lt;/span&gt;
  &lt;span class="na"&gt;TargetRubyVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.2&lt;/span&gt;  &lt;span class="c1"&gt;# adjust to your project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The more enforcement we can push into the toolchain itself,
the more confident we can be the agent won’t accidently
introduce bugs. Not every cop needs this treatment.
Reserve it for the ones where silencing would ship a bug to
production: thread safety, debuggers left in code, output safety, anything
that touches concurrency or security for example. &lt;/p&gt;
&lt;h2 id="one-piece-of-the-harness"&gt;
  
    One piece of the harness
  
&lt;/h2&gt;

&lt;p&gt;The RuboCop example here is one specific feedback loop, but the same pattern works for any tool that gives you a
clear pass/fail signal on the agent’s output. Wire it into a Stop hook, give the agent a chance to fix what comes
back, and surface what it can’t. Hooks themselves are just one tool in the broader practice of harness
engineering. We’re still in the early days of figuring out what a good Rails agent harness looks like, and a lot
of what we’ve shared here will probably look different in six months as we keep iterating. The harness that works
best for your team will come from paying attention to where your agent actually struggles on your codebase, and
encoding those fixes back into rules, context, subagents, and hooks of your own.&lt;/p&gt;
&lt;h2 id="references"&gt;
  
    References
  
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://code.claude.com/docs/en/hooks"&gt;Claude Hooks Reference&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://evilmartians.com/chronicles/rubocoping-with-legacy-bring-your-ruby-code-up-to-standard#you-shall-not-pass-introducing-"&gt;.rubocop_strict.yml&lt;/a&gt;&lt;/p&gt;

&lt;aside class="related-articles"&gt;&lt;h2&gt;If you enjoyed this post, you might also like:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://thoughtbot.com/blog/debugging-why-your-specs-have-slowed-down"&gt;Debugging Why Your Specs Have Slowed Down&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thoughtbot.com/blog/theme-based-iterations"&gt;Theme-Based Iterations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thoughtbot.com/blog/this-week-in-open-source-6-30"&gt;This Week in Open Source (June 30, 2023)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/aside&gt;
</content>
  <summary>A Rails-flavored guide to wrapping Claude Code in the checks, conventions, and feedback loops that make agent output more trustworthy.</summary>
  <thoughtbot:auto_social_share>true</thoughtbot:auto_social_share>
</entry>
</feed>
