RuboCop in Practice — Rules I Enable, Disable, and Why
by Eric Hanson, Backend Developer at Clean Systems Consulting
The configuration problem
A default RuboCop run on a greenfield Rails app produces hundreds of offenses before you've written any meaningful logic. Most teams respond in one of two ways: they disable everything that's inconvenient, or they enforce everything and spend code review discussing line length. Neither produces better code.
The productive approach is treating RuboCop as three distinct tools bundled together: a formatter, a complexity detector, and a style enforcer. Each category deserves different treatment in your .rubocop.yml.
Start with a shared config baseline
Don't write your .rubocop.yml from scratch. Two maintained community baselines handle the defaults-versus-opinions split cleanly:
rubocop-rails-omakase— the opinionated config DHH ships with Rails 8. Minimal rules, format-focused, disables most style opinions. Good starting point if your team wants to avoid the style debates entirely.rubocop-shopify— stricter, more comprehensive, represents a large production Rails codebase's settled conventions. Good starting point if you want more enforcement out of the box.
Inherit from one:
inherit_gem:
rubocop-rails-omakase: rubocop.yml
Then layer your project-specific overrides on top. The goal is a .rubocop.yml that's short — under 50 lines of actual configuration — with a clear rationale for every departure from the baseline.
The formatter layer — enable autocorrect, stop discussing
Formatting decisions that have a canonical right answer should be autocorrected on save or in CI pre-commit, never reviewed by humans. These include:
Layout/TrailingWhitespaceLayout/IndentationWidthLayout/EndAlignmentLayout/EmptyLinesAroundClassBodyStyle/StringLiterals(single vs. double quotes — pick one, autocorrect it, never discuss again)Style/TrailingCommaInArguments
Run rubocop --autocorrect-all in CI before the test suite. Any file touched in a PR gets autocorrected. This eliminates the entire category of formatting comments from code review without anyone having to agree on a style guide.
The one layout cop worth disabling: Layout/LineLength. The default is 120 characters (previously 80). Long lines are a symptom of complex expressions, not a cause of bugs. Enforcing line length on a codebase with deeply nested hash arguments or long method chains produces line-break gymnastics that's harder to read than the original. Disable it and address genuine complexity with structural changes, not newlines.
Layout/LineLength:
Enabled: false
The complexity layer — the cops that find real problems
These cops correlate with actual maintenance problems and are worth enforcing strictly:
Metrics/MethodLength with a low limit (10–12 lines) catches methods that are doing too much. The default is 10. Don't raise it — the right response to a violation is refactoring, not config relaxation. The one exception: methods that are primarily data declarations (a call method building a large hash, a let block in specs) can be excluded:
Metrics/MethodLength:
Max: 12
CountAsOne:
- array
- hash
- heredoc
CountAsOne prevents multi-line array and hash literals from inflating the line count — a reasonable concession for declarative data.
Metrics/AbcSize measures Assignments, Branches, and Conditions — a proxy for cyclomatic complexity that's more accurate than line count alone. A method with 8 lines and 4 nested conditionals will pass MethodLength and fail AbcSize. The default threshold is 17; I've found 15 to be the productive signal threshold. Above that, the method reliably has a refactoring opportunity.
Metrics/ClassLength at 150–200 lines catches models and service objects that have grown past manageability. The violation isn't always fixable immediately, but it's useful as a canary — new violations in CI mean something structural is happening.
Style/GuardClause enforces replacing nested conditionals with early returns:
# Flagged
def process(user)
if user.active?
if user.verified?
do_work(user)
end
end
end
# Preferred
def process(user)
return unless user.active?
return unless user.verified?
do_work(user)
end
This one generates the most team disagreement. The cop is right. Guard clauses reduce nesting, make the preconditions explicit, and leave the happy path unindented. Enable it.
The style layer — where to be selective
Most style cops enforce conventions that teams can reasonably disagree on. Enable the ones your team has already converged on; disable the ones that generate debate without improving correctness.
Enable:
Style/FrozenStringLiteralComment — enforces # frozen_string_literal: true at the top of every file. As covered in the symbols-vs-strings article, this eliminates unnecessary string allocations for literals. Autocorrectable.
Style/ReturnNil — flags explicit return nil in favor of bare return. Minor, but consistent.
Style/RedundantReturn — removes return from the last expression in a method. Ruby implicitly returns the last value; explicit return in that position is noise.
Rails/BulkChangeTable — catches individual add_column calls inside a migration that should be combined into change_table. A genuine performance issue at scale: multiple add_column calls each acquire an ACCESS EXCLUSIVE lock in PostgreSQL. Combining them into one change_table block acquires it once.
Disable or tune:
Style/Documentation — requires a comment above every class and module definition. Disable it. Classes should be self-documenting through their names and method interfaces; a mandatory comment block above every class produces copy-pasted boilerplate and no signal.
Naming/MethodParameterName — flags short parameter names like n, e, i. Disable it. |n| in a map block, |e| in a rescue, |i| in an each_with_index are idiomatic and their scope is obvious from context. Enforcing verbose names in tight iteration blocks makes them harder to read.
Style/ClassAndModuleChildren — enforces nested vs. compact module notation (module Foo::Bar vs module Foo; module Bar). Either is fine; the cop's opinion doesn't improve anything. Disable.
Style/SymbolProc — flags { |x| x.method } in favor of &:method. Enable it where the method reference is clean; be aware it flags cases where the block form is more readable due to context. Teams that agree on &: usage can enable it safely.
Inline disables — use them surgically, not defensively
# rubocop:disable inline is the escape hatch for legitimate exceptions — not for avoiding the work of refactoring:
# rubocop:disable Metrics/MethodLength -- data declaration, not logic
FIELD_MAPPINGS = {
# ... 25 entries
}.freeze
# rubocop:enable Metrics/MethodLength
The comment after the -- is enforced by Style/DisabledComment (enable this cop). Disable annotations without explanations accumulate silently and nobody knows why they exist three years later.
Never use # rubocop:disable all. It disables every cop, including security-relevant ones. If a block of code needs multiple cops disabled, the code is the problem.
The CI integration that actually works
Two-stage setup:
rubocop --autocorrect-allruns first, commits the corrections, then tests run. Formatting violations never reach the test stage.- Complexity and style violations fail the build. No warnings — violations either block merge or they don't. Warnings become ignored noise within a week.
For large existing codebases, use rubocop --auto-gen-config once to generate a .rubocop_todo.yml that excludes all current violations. New code must comply; existing code gets a grace period. Set a policy to reduce the todo file by a fixed number of violations per sprint rather than letting it sit forever.
# .rubocop.yml
inherit_from: .rubocop_todo.yml
The todo file is technical debt made visible. Review it in your quarterly dependency-update pass.
The short list
Enable hard: Metrics/MethodLength, Metrics/AbcSize, Style/GuardClause, Style/FrozenStringLiteralComment, Rails/BulkChangeTable.
Autocorrect silently: all Layout/* cops, Style/StringLiterals, Style/RedundantReturn, Style/SymbolProc.
Disable: Layout/LineLength, Style/Documentation, Naming/MethodParameterName, Style/ClassAndModuleChildren.
Everything else: inherit from a community baseline and override only when your team has a specific reason that isn't "this is inconvenient."