"CUT IT OUT, CLAUDE." doesn't work.
"Oh look, somehow you failed linting, how rough for you, better fix it! no naps!" does.
AI assistants are useful and can write functional code. They also:
- Love to nap on giant hoards of old code.
- Build defensive code fortresses EVERYWHERE.
- Handle explosive errors by adding "okay but what if we just ignored that! look, fixed it!"
- Add comments everywhere and then pretend you wrote them. π
- Reach for the world's least common Unicode symbols and then get confused when you say "stop it."
This gem tries to make them cut that out.
Add to your Gemfile:
gem 'rubocop-claude', require: falseThen run:
bundle install
rubocop-claude initThe init command will:
- Create
.claude/linting.mdwith instructions for AI assistants - Add
rubocop-claudeto your.standard.ymlor.rubocop.yml
Add to .standard.yml or .rubocop.yml:
plugins:
- rubocop-claude| Cop | Description |
|---|---|
Claude/NoFancyUnicode |
Flags emoji and fancy Unicode (curly quotes, em-dashes) |
Claude/TaggedComments |
Requires attribution on TODO/FIXME/NOTE comments |
Claude/NoCommentedCode |
Detects commented-out code blocks |
Claude/NoBackwardsCompatHacks |
Catches dead code preserved "for compatibility" |
Claude/NoOverlyDefensiveCode |
Flags rescue nil, excessive &. chains, defensive nil checks |
Claude/ExplicitVisibility |
Enforces consistent visibility style (grouped or modifier) |
Claude/MysteryRegex |
Flags long regexes that should be extracted to constants |
Claude/NoHardcodedLineNumbers |
Flags hardcoded line numbers that become stale |
All cops are enabled by default. Configure in .rubocop.yml:
Claude/NoFancyUnicode:
AllowInStrings: true # Allow emoji in user-facing strings
AllowedUnicode:
- "\u2192" # Allow specific characters
Claude/TaggedComments:
Keywords:
- TODO
- FIXME
- NOTE
- HACK
Claude/NoCommentedCode:
MinLines: 2 # Only flag multi-line blocks (set to 1 for single lines)
AllowKeep: true # Allow KEEP [@handle]: comments to preserve code
Claude/NoOverlyDefensiveCode:
MaxSafeNavigationChain: 1 # Flag 2+ chained &. operators
AddSafeNavigator: false # Autocorrect to &. instead of direct call
Claude/ExplicitVisibility:
EnforcedStyle: grouped # or 'modifier' for `private def foo`
Claude/MysteryRegex:
MaxLength: 25
Claude/NoHardcodedLineNumbers:
CheckComments: true # Check comments (default: true)
CheckStrings: true # Check string literals (default: true)Me: "Okay, let's log that success." Claude: A RAINBOW OF OBSCURE SYMBOLS EMERGES FROM THE MISTS.
# bad
puts "Deployment successful! π"
logger.info "Task completed β
"
# good
puts "Deployment successful!"
logger.info "Task completed""You added a comment, and it looks like I added a comment, and that comment is wrong."
# bad
# TODO: fix this later
# good
# TODO: [@username] fix this later"No hoarding."
# bad
# def old_implementation
# do_something_outdated
# end
# good - just delete it"NO HOARDING."
# bad
_old_value = previous_calculation # keeping for reference
OldName = NewName # backwards compatibility
# good - delete it"YOU ARE BUILDING A DOOMSDAY BUNKER FOR A METHOD THAT CAN'T FAIL, CLAUDE."
# bad
result = dangerous_call rescue nil
user&.profile&.settings&.value
user && user.name
# good
result = dangerous_call
user.profile.settings.value
user.name"The solution is NOT 'make all the private methods visible,' bud."
# grouped style (default)
class Foo
def public_method; end
private
def private_method; end
end
# modifier style
class Foo
def public_method; end
private def private_method; end
end"I have no idea what your 300-character regex does, Claude, because you dumped that on the screen, said 'okay cool fixed' and then got distracted."
# bad
if input.match?(/\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/)
# good
EMAIL_PATTERN = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/
if input.match?(EMAIL_PATTERN)"That line reference is wrong, Claude. The code moved."
# bad
# see line 42 for details
# Error at foo.rb:123
raise "check line 55"
# good
# see #validate_input for details
# Error in FooError class
raise "check validate_input method"rubocop-claude init also enables some defaults in Rubocop that are aimed at keeping AI coders from getting weird with their Ruby. Full config with rationale in config/default.yml.
| Cop | Setting | Why |
|---|---|---|
Style/DisableCopsWithinSourceCodeDirective |
Enabled | AI "fixes" linting by disabling cops. No. |
Layout/ClassStructure |
Enabled | AI scatters methods randomly. Enforce order. |
Style/CommentAnnotation |
RequireColon: true |
Works with TaggedComments. |
Lint/Debugger |
Enabled | AI leaves binding.pry in code. |
Layout/MultilineMethodCallIndentation |
indented |
Leading dot, 2-space indent. |
Style/SafeNavigation |
MaxChainLength: 1 |
Complements NoOverlyDefensiveCode. |
Metrics/CyclomaticComplexity |
Max: 7 |
Flag spaghetti logic. |
Metrics/AbcSize |
Max: 17 |
Flag bloated methods. |
Metrics/MethodLength |
Max: 10 |
AI writes 80-line methods. 10 is plenty. |
Metrics/ClassLength |
Max: 150 |
Catches god classes. |
Metrics/ParameterLists |
Max: 5 |
Too many params = needs refactoring. |
Style/GuardClause |
Enabled | Early returns > nested conditionals. |
Style/RedundantReturn |
Enabled | Ruby returns last expression. |
Style/MutableConstant |
strict |
Always .freeze constants. |
Lint/UnusedMethodArgument |
Enabled | Dead params = dead code. |
Style/NestedTernaryOperator |
Enabled | a ? (b ? c : d) : e is unreadable. |
Style/OptionalBooleanParameter |
Enabled | foo(data, true) - what's true mean? |
Naming/MethodParameterName |
MinNameLength: 2 |
No x, y, z params. Use real names. |
Style/ParallelAssignment |
Enabled | One assignment per line. |
Not enabled: Lint/SuppressedException and Style/RescueModifier - our NoOverlyDefensiveCode covers these with a unified "trust internal code" message.
rubocop-claude init will add files to .claude (or elsewhere if you provide it with a path).
These files are a bunch of reference docs for AI agents to gnaw on when they're asked to think about linting. It does not change any of your other config, and it does not try to integrate with the rest of your setup. It's just providing a structured starting point for getting AI agents to lint more effectively.
This isn't a subsitute for reviewing your code or monitoring AI assistants. Static analysis is a wonderful tool for wrangling Ai coders, but it does not replace reviewing and monitoring changes.
Trying to force static analysis tools to fully handle every single edge case is silly, and trying to make these weird, enthusiastic pattern recognition engines get everything right on the first try isn't going to work. What we're trying to do is give tools like Claude a way to efficiently remember that they shouldn't be making weird decisions, and that they should make good decisions instead.
We can't always prevent AI tools from charging off in weird directions, but we CAN scatter rakes in their way and make them stop and think. This adds more rakes.
bundle install
bundle exec rspec
bundle exec rubocopMIT License. See LICENSE.txt.