Claude Code for Ruby & Rails — Complete Setup Guide
Claude Code works well with Ruby on Rails when configured with your app's conventions, test setup, and gem stack. This guide covers the CLAUDE.md template, automated RSpec hooks, ActiveRecord migration patterns, Sidekiq worker generation, and Hotwire workflows.
Rails CLAUDE.md Template
# Project: [Your App Name]
## Stack
- Ruby 3.3, Rails 7.2
- Database: PostgreSQL 16 (via pg gem)
- Background jobs: Sidekiq 7 (Redis-backed), queues: default, mailers, critical
- Frontend: Hotwire (Turbo + Stimulus), importmap, no webpack
- Auth: Devise 4 with JWT for API routes
- Tests: RSpec 3 + FactoryBot + Shoulda-Matchers + VCR
- Linting: RuboCop with rubocop-rails, rubocop-rspec extensions
## Commands
```bash
bin/rails server # start dev server
bin/rails console # rails console
bundle exec rspec # run all specs
bundle exec rspec spec/models/user_spec.rb # single spec file
bundle exec rubocop # lint
bundle exec rubocop -a # auto-correct safe offenses
bin/rails db:migrate # run migrations
bin/rails db:migrate:status # check migration status
```
## Conventions
- Service objects in app/services/[domain]/[action]_service.rb — accept keyword args, return Result object
- No fat models — business logic in services, not callbacks
- Avoid after_create callbacks for side effects; use explicit service calls
- Factories in spec/factories/, one file per model
- FactoryBot: use build(:model) for unit tests, create(:model) for integration
- Error handling: use rescue_from in ApplicationController, custom error classes in app/errors/
## Project structure highlights
- app/services/ — domain service objects
- app/policies/ — Pundit authorization policies
- app/serializers/ — ActiveModel::Serializers for API JSON
- app/sidekiq/ — Sidekiq worker classes
- spec/support/ — shared examples, helpers, VCR cassettes
Automated RSpec Hooks
Run specs after every file edit so Claude sees failures immediately:
// .claude/settings.json
{
"allowedTools": ["Edit", "Write", "Bash", "Read", "Glob", "Grep"],
"hooks": [
{
"event": "PostToolUse",
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "cd $PROJECT_ROOT && bundle exec rspec --format progress 2>&1 | tail -30"
}]
}
]
}
For TDD, target a single spec file first:
"command": "bundle exec rspec spec/models/user_spec.rb 2>&1". Switch to the full suite when the unit is complete.RuboCop lint hook
{
"event": "PostToolUse",
"matcher": "Write",
"hooks": [{
"type": "command",
"command": "cd $PROJECT_ROOT && bundle exec rubocop --format progress 2>&1 | tail -20"
}]
}
Teaching Claude Idiomatic Ruby/Rails
| Pattern | CLAUDE.md instruction |
|---|---|
| Service objects | "All business logic in app/services/. Services accept keyword args and return a Result struct with .success?, .value, .error." |
| No fat models | "Do not add business logic to models. Models: validations, associations, scopes only. No after_create callbacks with side effects." |
| Error handling | "Define custom error classes in app/errors/. Rescue in ApplicationController with rescue_from and return structured JSON." |
| N+1 prevention | "Always use includes/preload for associations rendered in views or serializers. Run bullet gem in test env — it will raise on N+1." |
| Safe migrations | "Use strong_migrations conventions: add_column nullable first, backfill separately, add NOT NULL constraint last." |
| Testing | "Use build() for unit tests, create() only for integration. No let! — use let with explicit calls in before blocks." |
ActiveRecord Migration Workflows
Zero-downtime column addition
claude "add a 'last_login_at' datetime column to the users table.
Table has 8M rows — needs to be zero-downtime.
Add nullable with no default, add index concurrently on PostgreSQL.
Follow strong_migrations conventions."
Polymorphic association
claude "add a polymorphic 'commentable' association to the comments table.
Generate the migration (commentable_type string, commentable_id bigint),
add a composite index on [commentable_type, commentable_id],
update the Comment model with belongs_to :commentable, polymorphic: true,
and update Post and Article models with has_many :comments, as: :commentable."
Data migration pattern
claude "write a Rails migration that backfills the full_name column on users
from first_name + last_name. Use find_each in batches of 1000.
Don't lock the table — use update_column. Add a reversible block that sets
full_name to nil on rollback."
Sidekiq Worker Generation
Basic idempotent worker
claude "create a Sidekiq worker SendWelcomeEmailWorker in app/sidekiq/.
Queue: mailers. Retries: 3. The perform method takes user_id.
Fetch user with User.find — rescue ActiveRecord::RecordNotFound and return
without raising (idempotent for deleted users).
Write RSpec spec with Sidekiq::Testing.fake! and test the guard clause."
Batch processing worker
claude "create ExportReportWorker that processes a report export job.
accept: report_id, export_format (:csv, :xlsx).
Use ReportExportService to generate the file.
Upload result to S3 using ActiveStorage.
Update report.status to :completed or :failed.
Include retry logic with exponential backoff via sidekiq_retry_in."
Hotwire (Turbo + Stimulus) Workflows
Turbo Frame partial update
claude "add inline comment editing to the posts#show page.
Use a Turbo Frame around each comment.
Edit button renders the form inside the same frame.
PATCH /comments/:id returns turbo_stream.replace for the comment partial.
No full page reload. Follow our existing Hotwire conventions."
Stimulus controller
claude "create a Stimulus controller 'character-counter' that:
- connects to a textarea with data-controller='character-counter'
- shows remaining character count in a linked span (data-character-counter-target='count')
- max character limit as a value: data-character-counter-max-value='280'
- turns red when under 20 characters remaining
Write the controller in app/javascript/controllers/character_counter_controller.js"
API Development (JSON:API / REST)
claude "add a REST API endpoint GET /api/v1/users/:id.
Serializer using ActiveModel::Serializers returning id, email, full_name, created_at.
Authenticate with Devise JWT — authenticate_user! before action.
Policy check via Pundit: UserPolicy#show?.
Render 403 if unauthorized, 404 if not found.
Write request spec covering auth, authorization, happy path."
5 Tips for Ruby + Claude Code
- Add your
Gemfileto the conversation context early — Claude uses gem versions to pick correct API patterns (e.g., Devise 4 vs 5, Pundit 2 vs 3, Rails 7.0 vs 7.2). - For migrations, tell Claude "this table has X million rows and runs on PostgreSQL — use add_index concurrently and follow strong_migrations." Claude will add the right disable_ddl_transaction! call.
- When debugging N+1 queries, paste the Bullet gem output or SQL log snippet. Claude identifies the exact association missing from the includes chain.
- Specify your RSpec shared examples location: "shared examples are in spec/support/shared_examples/". Claude will use
it_behaves_likerather than duplicating expect blocks. - For Sidekiq workers, always say "make the perform method idempotent — it may be called more than once for the same job ID." Claude adds guard clauses and uses find_or_create_by instead of create.