← All posts
·10 min read

Claude Code with Ruby on Rails: A Practical Setup

Claude CodeRuby on RailsBackendWorkflow
Claude Code with Ruby on Rails: A Practical Setup

Why Rails codebases need deliberate Claude Code configuration

Claude Code knows Ruby on Rails. It knows ActiveRecord, strong_params, belongs_to, has_many :through, before_action, RSpec, and the Rails testing conventions. The issue is that this knowledge is framework-level, not codebase-level.

Rails applications accumulate conventions over years: how models are scoped, which callbacks are used and where, how controllers handle authorization, which gems handle authentication versus which are custom. Claude Code without configuration guesses at these conventions. It generates code that runs but does not fit, requiring corrections that add up across a sprint.

The fix is a CLAUDE.md that converts your implicit conventions into explicit rules Claude reads before every session. This guide covers that configuration, the ActiveRecord patterns that prevent migration accidents, the RSpec setup that keeps Claude's test output consistent, and the permission hooks that stop risky commands running without your knowledge.

If you are starting from scratch, the Claude Code setup guide covers installation and initial configuration before any of this applies.

The Rails CLAUDE.md

The CLAUDE.md at your project root is the highest-leverage configuration for any Rails codebase. It needs to answer: how is the app structured, how is the database managed, how are tests run, what gems are in use, and what are the hard rules?

# Rails project rules

## Ruby and Rails versions
- Ruby: 3.3.x (see .ruby-version)
- Rails: 7.2
- Bundler: use `bundle exec` before all Rails commands
- Version manager: rbenv (not rvm or asdf)

## Project structure
- Models: app/models/, single-table inheritance in sti/ subdirectory
- Controllers: app/controllers/ with concerns in concerns/
- Services: app/services/, one class per file, POROs only
- Mailers: app/mailers/
- Jobs: app/jobs/ (ActiveJob with Sidekiq backend)
- Serializers: app/serializers/ (blueprinter gem)

## Database
- PostgreSQL 15 (local: port 5432, credentials in .env.development)
- ActiveRecord ORM only, no raw SQL except in model class methods with a comment explaining why
- Use `includes()` for associations accessed in views or serializers
- Use `joins()` only when filtering on association attributes, not for loading
- Scopes: define on the model, not in controllers or service objects

## Migrations
- Never run `rails db:migrate` unless explicitly instructed
- Never create a migration without saying so first
- Descriptive migration names: `rails generate migration AddStripeIdToUsers stripe_id:string:index`
- Always review the generated migration file before applying
- `rails db:migrate:status` to check state before writing new model code

## Tests
- Framework: RSpec (not Minitest)
- Run: `bundle exec rspec` from project root
- Fast pass: `bundle exec rspec spec/models/` or `spec/controllers/`
- Factories: FactoryBot in spec/factories/, one factory per model
- No fixtures. FactoryBot only.
- System specs: Capybara + Chrome headless, run separately with `rspec spec/system`

## Gems to be aware of
- Authentication: Devise (users) + Pundit (authorization)
- API: Grape (in api/ directory), not standard Rails controllers for API endpoints
- Background jobs: Sidekiq
- Pagination: Pagy (not Kaminari or will_paginate)
- File uploads: Active Storage (S3 in production, disk in development)

## Hard rules
- No ActiveRecord callbacks that trigger network requests (emails, webhooks). Use jobs.
- No business logic in controllers. Service objects in app/services/.
- No `User.all` or collection queries without `.limit()` in non-background contexts.
- `strong_params` required on every controller action that accepts user input.
- All Pundit policies must have a corresponding spec in spec/policies/.

Two sections prevent the most common Claude Code mistakes in Rails codebases.

The migration rules stop Claude from applying migrations as a side effect of model changes. Rails developers who have been burned by a bad migration in staging will recognize why this matters. The rule to state migration intent before running anything gives you a review window.

The gems section matters because Claude Code will default to common gems if it does not know what is installed. Without the Grape reference, Claude will write API endpoints as standard Rails controllers in a project that routes API calls through Grape. Without the Pagy reference, it will generate Kaminari pagination. These are easy mistakes to make and tedious to undo across multiple files.

ActiveRecord workflows that avoid common traps

Claude Code handles ActiveRecord well when the task is scoped clearly. The problems come from two directions: N+1 queries in associations, and model changes that create migration debt without flagging it.

The N+1 problem. Claude generates association calls the way a developer who has not profiled the query log would: individually. An index page that calls order.user.name for each row generates one query per row. With the includes() rule in CLAUDE.md, Claude adds eager loading proactively.

For associations you know will be loaded in views, tell Claude explicitly during model design:

"Write a scope with_details on Order that eager-loads user and line_items, for use in the orders index view. The scope should chain on any existing scope cleanly."

The result is a reusable, documented scope rather than scattered includes calls in controllers.

Association validation. Rails 5+ makes belongs_to associations required by default. Claude sometimes generates optional associations using optional: true when you want the database-level constraint to hold. Add to CLAUDE.md:

- `belongs_to` associations: required by default. Only add `optional: true` when
  the record is genuinely valid without the parent. Comment explaining why when used.

Scopes versus class methods. Claude Code mixes these up in legacy codebases. Scopes defined as lambdas chain correctly; class methods that return relations do too, but class methods that conditionally return nil break chains. CLAUDE.md rule: "All query methods on models use scope syntax (scope :name, -> { ... }). Class methods only for non-query logic."

For patterns that apply across database-heavy development, the Claude Code database guide covers MCP database server setup, query inspection, and schema design workflows.

RSpec integration and FactoryBot setup

The most productive Rails workflow with Claude Code runs RSpec automatically after every change. Claude treats a failing test as an incomplete task.

Add to CLAUDE.md:

## After every code change

1. Run `bundle exec rspec spec/models/` for model changes (fast)
2. Run `bundle exec rspec spec/controllers/` for controller changes
3. Run `bundle exec rspec` for the full suite before marking any task complete
4. Do not ask for review until `bundle exec rspec` exits 0

FactoryBot patterns Claude handles well

FactoryBot with well-structured factories is the testing setup where Claude Code produces the most consistent output. Three conventions to add to your spec/factories setup:

Traits for state variations. Instead of separate factories for active and inactive users, use traits:

FactoryBot.define do
  factory :user do
    email { Faker::Internet.email }
    name { Faker::Name.name }
    
    trait :admin do
      role { "admin" }
    end
    
    trait :suspended do
      suspended_at { 1.day.ago }
    end
  end
end

Tell Claude: "Use FactoryBot traits for state variations. Never create separate named factories for states that a trait can express."

create versus build versus build_stubbed. Claude defaults to create which hits the database. In unit tests for model methods that do not need persistence, build_stubbed is 10x faster. Add to CLAUDE.md:

- `create` only when the test needs a persisted record (controller tests, queries)
- `build` for model validations and methods that do not query the DB
- `build_stubbed` for service object unit tests

Shared contexts for authentication. If you use Devise, add a shared context in spec/support/:

shared_context "authenticated user" do
  let(:user) { create(:user) }
  before { sign_in user }
end

Tell Claude where shared contexts live and that it should include them rather than writing inline sign_in calls in every spec. This keeps Claude's controller spec output consistent.

The Claude Code testing guide covers the test-generation workflow in depth, including TDD loops and coverage analysis that apply to any Ruby project.

Controller and strong params patterns

Rails controllers are where business logic accumulates against convention. Claude Code will put logic in controllers if you do not tell it not to. The service object rule in CLAUDE.md is the main guardrail, but controllers still need explicit patterns.

Strong params structure. Claude generates correct strong_params but will sometimes nest them incorrectly for nested attributes. Add an example to CLAUDE.md:

## Strong params convention

def user_params
  params.require(:user).permit(
    :email, :name, :role,
    addresses_attributes: [:id, :street, :city, :country, :_destroy]
  )
end

# - Permitted params listed alphabetically within each group
# - Nested attributes always include :id and :_destroy for form handling
# - No params permitted that are not explicitly needed for this action

Before actions for authorization. With Pundit, the convention is authorize @resource in each action. Claude will generate it correctly if the pattern is clear. What Claude gets wrong without guidance: calling policy_scope(Resource) in actions that do not render a collection, or forgetting authorize entirely on create/update actions. Add:

## Pundit usage

- `authorize @resource` in every action before returning a response
- `policy_scope(Resource)` only in index actions that return collections
- Every Pundit policy class has a spec: `spec/policies/{model}_policy_spec.rb`

Service objects for business logic. When a controller action needs more than two steps, Claude should extract a service object. The trigger in CLAUDE.md:

## Service objects

If a controller action has more than 2 lines of non-HTTP logic, extract a service.
Services in app/services/ named as verbs: CreateSubscription, CancelOrder, SendInvoice.
Services initialize with required data, expose a single `call` method, return a result object.

This rule alone prevents controllers from accumulating conditional logic that is hard to test and hard for Claude to reason about in future sessions.

Using Claude Code worktrees for risky Rails changes

Rails schema migrations, gem upgrades, and large refactors are high-risk changes. Claude Code works natively with Git worktrees, which lets you run Claude in an isolated copy of the repository while your working directory stays clean.

The workflow for a risky Rails change:

# Create a worktree for the migration work
git worktree add ../myapp-migration-work feature/add-subscription-model

# Open Claude Code in the worktree directory
cd ../myapp-migration-work && claude

Claude Code runs in the isolated worktree. Migrations it creates, files it modifies, and commands it runs stay in that directory. You review the diff before merging. The main working tree is unaffected throughout.

This pattern is particularly useful for Rails upgrades (6.x to 7.x), schema changes with multi-step migrations, and gem replacements where Claude needs to touch many files at once. The Claude Code git workflow guide covers worktrees and branching patterns in more detail.

What Rails developers get wrong first

Three mistakes appear consistently when Rails developers start using Claude Code in production codebases:

Not specifying the Ruby version manager. Claude generates rbenv commands in an rvm project and the PATH resolution fails silently. One line in CLAUDE.md prevents this: specify the version manager and how to activate the correct Ruby version before any command.

Letting Claude add gems without a Bundler run. Claude will add a gem to Gemfile and reference it in code before running bundle install. The code looks complete but the server crashes on load. Add to your hard rules: "Never require a gem in code before running bundle install and confirming the gem is available."

Under-specifying authorization. Claude Code will omit Pundit authorize calls in controller actions when the action name does not obviously suggest authorization is needed. Custom actions (POST /users/:id/suspend) are the most common miss. Make the rule explicit: "Every controller action that modifies data or returns sensitive data calls authorize before returning."

Getting more from your Rails workflow

The configuration in this guide produces a Rails setup where migration discipline is enforced, the ORM stays performant, and RSpec runs automatically as part of Claude's task loop. The underlying principle is the same as any Claude Code best practices guide: the more precisely you define your project's conventions, the less time you spend correcting Claude's output.

A configured Rails codebase with Claude Code compresses the debugging cycle. Developers working with long-lived Rails applications have reported cutting debugging time from 45-minute call-chain analysis sessions to under 10 minutes. The gains come not from Claude being smarter, but from Claude knowing your specific codebase well enough to read error context correctly.

Start with the CLAUDE.md template above. Add the RSpec enforcement section. Add the Pundit rules when authorization is in scope. Each rule added reduces the correction rate in the next session.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir