DEV Community

Olivia Craft
Olivia Craft

Posted on

CLAUDE.md for Ruby: 13 Rules That Make AI Write Idiomatic, Production-Ready Ruby

Ruby has a distinct culture around idiomatic code. The language is designed so that well-written Ruby reads almost like English — concise, expressive, and elegant. AI-generated Ruby often misses this: the code works, but it reads like Ruby written by someone who learned Python first.

A CLAUDE.md file at your repo root tells your AI assistant what "good Ruby" looks like for your project. Here are 13 rules that matter most.


Rule 1: Ruby version and runtime environment

Ruby version: 3.3+. Use YJIT in production (`RUBY_YJIT_ENABLE=1` or `--yjit`).
Bundler manages all gems — no manual `require` for anything in the Gemfile.
`.ruby-version` file is authoritative for local development.
Enter fullscreen mode Exit fullscreen mode

Ruby 3.x has significant performance improvements and new syntax. AI often generates code compatible with Ruby 2.x. Specifying the version prevents outdated patterns like proc { } where -> {} (lambda) is cleaner, or missing pattern matching syntax available since 3.0.


Rule 2: Idiomatic conditionals — trailing and ternary

Prefer trailing conditionals for single-line guards:
  `return if invalid?` not `if invalid? then return end`
  `notify! unless silent?` not `if !silent? then notify! end`

Use ternary only for simple value selection:
  `status = active? ? :online : :offline`

Avoid nested ternaries — extract to a method instead.
Enter fullscreen mode Exit fullscreen mode

This is the most visible Ruby idiom gap in AI output. Ruby developers read return if condition as a natural guard clause. Multi-line if/end for single conditions is considered verbose and non-idiomatic.


Rule 3: Symbols over strings for keys

Hash keys: use symbols for internal data structures.
  `{ name: "Alice", role: :admin }` not `{ "name" => "Alice", "role" => "admin" }`

String keys only when the key comes from external input (JSON parsing, HTTP params) or when
the key must be dynamic.

Use `Hash#fetch` with a default or block when the key might be absent:
  `config.fetch(:timeout, 30)` not `config[:timeout] || 30`
Enter fullscreen mode Exit fullscreen mode

Symbols are immutable and memory-efficient. The || fallback for missing keys is a common bug when the value can legitimately be false or nil. fetch is the correct idiom.


Rule 4: Blocks, procs, and lambdas — use the right tool

Blocks: for one-off iteration and DSL construction (`each`, `map`, `tap`, `yield`).
Lambdas (`-> {}`): when you need to store callable behavior, check arity, or return from within.
Procs (`proc {}`): rarely — only when you explicitly need loose arity behavior.

Prefer `&method(:name)` over a block that just calls a method:
  `users.map(&method(:transform))` not `users.map { |u| transform(u) }`

Use `Symbol#to_proc` for simple field extraction:
  `names.map(&:upcase)` not `names.map { |n| n.upcase }`
Enter fullscreen mode Exit fullscreen mode

AI frequently generates verbose blocks where &:method_name or &method(:name) is cleaner. These patterns are central to Ruby's functional style.


Rule 5: Enumerable over manual loops

Use Enumerable methods instead of index-based loops:
- `map` / `flat_map` for transformations
- `select` / `reject` for filtering
- `reduce` / `each_with_object` for aggregation
- `find` for first match
- `group_by`, `tally`, `chunk_while` for grouping
- `any?`, `all?`, `none?`, `count` for predicates

Never use `for` loops — use `each`.
Avoid `inject(:+)` when `sum` is available.
Enter fullscreen mode Exit fullscreen mode

Ruby's Enumerable module is one of its greatest strengths. AI sometimes generates C-style index loops or nested conditionals where a chain of Enumerable methods is cleaner and more idiomatic.


Rule 6: Method visibility and attr_* macros

Use `attr_reader`, `attr_writer`, `attr_accessor` instead of manual getter/setter methods.
Mark methods private when they're implementation details — default to private, not public.
Use `protected` only for methods called by instances of the same class (rare).

Order in class body: class methods → initialize → public instance methods → private methods.
Separate sections with `private` keyword, not comments.
Enter fullscreen mode Exit fullscreen mode

AI often generates explicit def name; @name; end getters. attr_reader :name is the Ruby convention. Correct visibility also matters — AI tends to leave everything public.


Rule 7: Error handling — rescue, not rescue Exception

Error handling:
- Rescue `StandardError` or specific subclasses — never `Exception`
  (`Exception` catches `SignalException`, `Interrupt`, `NoMemoryError` — almost always wrong)
- Prefer inline rescue for simple defaults: `value = risky_call rescue default_value`
  (only when the rescue is genuinely a simple fallback, not for flow control)
- Use `ensure` for cleanup, not rescue blocks
- Custom exceptions: inherit from `StandardError`, name with `Error` suffix
  `class PaymentFailedError < StandardError; end`
- Raise with a message: `raise PaymentFailedError, "Card declined: #{reason}"`
Enter fullscreen mode Exit fullscreen mode

rescue Exception is a common AI mistake that catches signals and system errors, preventing clean process shutdown. Always rescue specific errors.


Rule 8: Frozen string literals

Add `# frozen_string_literal: true` at the top of every Ruby file.
This makes all string literals immutable and reduces object allocation.

When a mutable string is genuinely needed: `String.new("mutable")` or `+"mutable"`.
Use string interpolation (`"#{var}"`) over concatenation (`str + other`).
Prefer `<<` (shovel) for in-place string building only when mutation is intentional and documented.
Enter fullscreen mode Exit fullscreen mode

Frozen string literals are a free performance win and a convention in modern Ruby gems. AI omits this unless specified.


Rule 9: Rails conventions (if using Rails)

If this is a Rails project:

- Fat models, thin controllers — business logic belongs in models or service objects
- Service objects in `app/services/` for operations that cross model boundaries
- Scopes over class methods for chainable queries: `scope :active, -> { where(active: true) }`
- `find_each` / `in_batches` for large dataset iteration — never `.all.each`
- Avoid N+1: use `includes`, `eager_load`, or `preload` — add Bullet gem to detect
- Strong parameters in controllers, not models
- `before_action` for authentication/authorization, not inline checks
- No raw SQL — use Arel or query interface; raw SQL only with `sanitize_sql`
Enter fullscreen mode Exit fullscreen mode

Rails conventions are dense. Without explicit guidance, AI generates controllers with business logic, models with SQL injection risk, and queries that cause N+1 problems at scale.


Rule 10: Testing — RSpec conventions

Testing: RSpec with FactoryBot for fixtures.

- Describe behavior, not implementation: `describe '#charge'` not `describe 'PaymentsController line 42'`
- Use `let` and `let!` for setup — not instance variables in `before` blocks
- `subject` for the object under test
- One assertion per example (generally) — exceptions: related state changes
- `context` blocks for branching: `context 'when payment fails'`
- Shared examples for common behavior across multiple classes
- `described_class` over hardcoded class names in specs
- VCR or WebMock for external HTTP — never hit real APIs in tests
Enter fullscreen mode Exit fullscreen mode

AI generates verbose, procedural RSpec that doesn't use let, nests before blocks unnecessarily, and creates brittle specs tied to implementation details.


Rule 11: Dependency injection and module composition

Prefer module mixins over inheritance for shared behavior:
  `include Auditable` not `class User < AuditableBase`

Use dependency injection for external services:
  `def initialize(mailer: UserMailer)` not `UserMailer.deliver` inside methods

Avoid monkey-patching core classes — use refinements (`refine String do`) when extension is necessary.
No `method_missing` without `respond_to_missing?`.
Enter fullscreen mode Exit fullscreen mode

Ruby's module system is powerful. AI tends toward inheritance hierarchies where mixins are more flexible. Dependency injection makes testing trivial — pass a mock mailer in tests, real mailer in production.


Rule 12: Pattern matching (Ruby 3.x)

Use pattern matching for complex conditional dispatch:

  case response
  in { status: 200, body: { user: { id: Integer => id } } }
    process_user(id)
  in { status: 422, errors: [*, String => first_error, *] }
    handle_validation_error(first_error)
  in { status: (400..499) }
    handle_client_error(response)
  end

Use `in` (one-armed pattern match) for destructuring:
  `response => { user: { name:, email: } }`
Enter fullscreen mode Exit fullscreen mode

Ruby 3.x pattern matching is underutilized in AI output because it's relatively new. It eliminates chains of if response[:status] == 200 && response[:body][:user] conditions.


Rule 13: Gemfile and dependency hygiene

Gemfile conventions:
- Pin major versions for stability: `gem 'rails', '~> 7.1'`
- Group gems correctly: `:development`, `:test`, `:development, :test`
- `bundle audit` before any update — check for security advisories
- No gems with `require: false` unless you manually require them
- Prefer gems with active maintenance — check last commit date on GitHub before adding
- Lock Ruby version in Gemfile: `ruby '~> 3.3'`
Enter fullscreen mode Exit fullscreen mode

What goes in your CLAUDE.md

# Ruby Project — AI Coding Rules

## Runtime
Ruby 3.3+. frozen_string_literal: true on every file.

## Style
- Trailing conditionals for guards: `return if invalid?`
- Symbols for internal hash keys
- Enumerable over loops: map/select/reduce/find
- &:method_name and &method(:name) over verbose blocks

## Error Handling
rescue StandardError (specific subclasses preferred). Never rescue Exception.
Custom errors inherit StandardError, named with Error suffix.

## Architecture
attr_reader/writer/accessor over manual getters/setters.
Private by default — public only what callers need.
Service objects in app/services/ for cross-model operations.

## Rails (if applicable)
Fat models, thin controllers. Scopes for chainable queries.
find_each for large sets. includes/eager_load to prevent N+1.

## Testing
RSpec + FactoryBot. let/let! for setup. One assertion per example.
context blocks for branching. VCR/WebMock for external HTTP.

## Dependencies
bundle audit on every update. Pin major versions.
Enter fullscreen mode Exit fullscreen mode

Why Ruby needs explicit rules

Ruby's design philosophy is that there are many ways to do the same thing, but idiomatic Ruby has strong preferences. AI assistants default to the most common patterns in their training data — often older Ruby or patterns from other languages.

CLAUDE.md makes the AI's behavior predictable session to session: the same idioms, the same conventions, the same patterns — whether you're working alone or with a team.

The full rules pack covering 12+ languages is at gumroad — $27.

Top comments (0)