Ruby Idioms skill for Codex
Ruby that reads like Ruby, not translated Java. Codex checks the project's Ruby version and RuboCop config, then rewrites manual accumulators into filter_map, tally, partition and each_with_object, models values with Data.define, and destructures API payloads with case/in. It knows the lambda versus proc return rules, frozen string behaviour in Ruby 3.4, and when unsafe autocorrect needs a test run before it is trusted.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- ruby-idioms Installs to ~/.agents/skills/ruby-idioms/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install ruby-idioms The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project ruby-idioms Installs to .agents/skills/ruby-idioms/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/ruby-idioms.zipunzip -o ruby-idioms.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) ruby-idioms PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $ruby-idioms, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes and refactors plain Ruby the way experienced Rubyists expect - Enumerable chains instead of index loops, blocks, procs and lambdas used correctly, keyword arguments, Data and Struct value objects, pattern matching with case/in, frozen strings, exception hierarchies - and keeps it clean with RuboCop or Standard. Use when writing or reviewing Ruby gems, scripts or service objects, when RuboCop flags pile up, or when Ruby code reads like translated Java.
Use it when
- Writing or reviewing plain Ruby: gems, scripts, service objects, POROs
- RuboCop offenses or a .rubocop_todo.yml need working down
- Code uses index loops, mutable accumulators or rescue Exception
- Upgrading code to Ruby 3.x features
Use something else when
- The question is about Rails conventions, controllers or Hotwire: use rails-apps
- The slowdown is Active Record queries: use active-record-performance
- You are writing or fixing RSpec specs: use rspec-testing
How to check the result
bundle exec rubocop passes on changed files and the existing test suite still passes, with new tests for any changed behaviour.
The mistake it prevents
Running rubocop -A across the codebase and committing unsafe autocorrections without reading the diff or running the tests.
Idiomatic Ruby reads top to bottom as a description of the data transformation: small methods, Enumerable instead of manual accumulators, explicit value objects, exceptions that callers can rescue by class, and no surprise mutation. RuboCop (or Standard) passes with the project’s config, and the test suite still passes.
Before changing code
- Read
AGENTS.md,.ruby-version/.tool-versions,Gemfile,Gemfile.lockand.rubocop.yml/.standard.yml. The Ruby version decides which features exist. - Run commands through Bundler so the locked gem versions are used:
bundle exec rubocop,bundle exec rspecorbundle exec rake test. - If
bundle installis needed and gems are missing, it needs network access; ask before installing. - Note whether the file has
# frozen_string_literal: true; keep it consistent with the rest of the codebase.
Feature availability
| Feature | Since | Use for |
|---|---|---|
case/in pattern matching, rightward => | 3.0 (stable 3.1) | destructuring hashes/arrays from JSON or APIs |
Endless methods def full = "#{a} #{b}" | 3.0 | one-expression readers |
Hash#except, Enumerable#tally, filter_map | 3.0 / 2.7 | everyday collection work |
Shorthand hash {x:, y:} | 3.1 | building hashes from same-named locals |
Data.define | 3.2 | immutable value objects |
it block parameter | 3.4 | short single-param blocks (map { it.upcase }) |
| Chilled string literals warn on mutation | 3.4 | prepares for frozen-by-default strings |
Do not introduce a feature newer than the project’s minimum Ruby (check the gemspec’s
required_ruby_version for libraries).
Collections: reach for Enumerable
| Instead of | Write |
|---|---|
result = []; xs.each { |x| result << f(x) if ok?(x) } | xs.filter_map { |x| f(x) if ok?(x) } |
h = {}; xs.each { |x| h[x.id] = x } | xs.to_h { |x| [x.id, x] } or xs.index_by(&:id) (Rails) |
counts = Hash.new(0); xs.each { |x| counts[x] += 1 } | xs.tally |
xs.select { ... }.first | xs.find { ... } |
xs.map { ... }.flatten | xs.flat_map { ... } |
xs.sort { |a, b| a.age <=> b.age } | xs.sort_by(&:age) |
xs.inject(0) { |s, x| s + x.total } | xs.sum(&:total) |
| two passes with select and reject | ok, bad = xs.partition(&:valid?) |
| manual grouping | xs.group_by(&:status), xs.each_slice(100), xs.chunk_while { |a, b| b == a + 1 } |
Use each_with_object when building a hash or set with conditional logic; avoid
inject returning the accumulator from every branch. Use .lazy for large or infinite
sources so first(10) does not materialise everything.
Blocks, procs and lambdas
yieldplusblock_given?for a single optional block; take&blockonly when you store or forward it.- Return an Enumerator when no block is given, so callers can chain:
def each_page(client)
return enum_for(:each_page, client) unless block_given?
cursor = nil
loop do
page = client.list(cursor:)
page.items.each { |item| yield item }
break unless (cursor = page.next_cursor)
end
end
each_page(client).lazy.select(&:active?).first(20)
- Lambdas check arity and
returnexits the lambda; procs ignore extra args andreturnexits the enclosing method. Use lambdas for stored callables. &:method_namefor simple sends;method(:name)to pass an existing method.
Objects and values
Money = Data.define(:cents, :currency) do
def +(other)
raise ArgumentError, "currency mismatch" unless currency == other.currency
with(cents: cents + other.cents)
end
def to_s = format("%.2f %s", cents / 100.0, currency)
end
total = Money.new(cents: 1250, currency: "EUR") + Money.new(cents: 50, currency: "EUR")
- Prefer
Data.define(immutable, keyword or positional construction, value equality) overStructfor new value objects; useStructonly when mutation is wanted. - Keyword arguments for anything with more than two parameters or any boolean flag:
def export(path, format: :csv, overwrite: false). - Include
Comparableand define<=>; includeEnumerableand defineeach. - Duck type with
respond_to?sparingly; prefer calling the method and lettingNoMethodErrorsurface during tests. - If you override
method_missing, also overriderespond_to_missing?. Better: define methods explicitly or withdefine_methodat load time. - Prefer refinements over reopening core classes; never monkey-patch stdlib in a gem.
Pattern matching
case response
in {status: 200, body: {data: [first, *]}}
handle(first)
in {status: 404}
nil
in {status: 500.. => code}
raise UpstreamError, "upstream returned #{code}"
end
A case/in with no matching branch raises NoMatchingPatternError; add an else
when unknown shapes are expected. Hash patterns match subsets; use **nil to require
no extra keys.
Errors
module Billing
class Error < StandardError; end
class CardDeclined < Error
attr_reader :code
def initialize(code)
@code = code
super("card declined: #{code}")
end
end
end
- One base error per library or bounded area, subclasses for cases callers handle differently.
rescue => ecatchesStandardError; neverrescue Exception(it swallowsInterruptandSystemExit).- Re-raise with context using
raise Billing::Error, "charging #{id} failed"inside a rescue: Ruby setscauseautomatically. - Use
ensurefor cleanup or, better, block-form APIs (File.open(path) { |f| ... }). retryonly with a counter and backoff.
Strings and mutation
- With
# frozen_string_literal: true, build strings with interpolation or+""/String.newwhen you need a mutable buffer;<<on a literal raisesFrozenError. - Methods ending in
!mutate or are the dangerous variant; do not mutate arguments the caller still owns. Return new objects instead. Hash#fetch(key)for required keys (fails loudly),digfor optional nested reads,&.only where nil is a legitimate value.
RuboCop
bundle exec rubocop # report
bundle exec rubocop -a path/ # safe autocorrect only
bundle exec rubocop --auto-gen-config # legacy codebase: freeze existing offenses in .rubocop_todo.yml
- Use
-a(safe) by default;-Aapplies unsafe corrections that can change behaviour, so review that diff and run tests. - Extensions (rubocop-rails, rubocop-rspec, rubocop-performance) load through
plugins:in.rubocop.ymlon RuboCop 1.72+; older configs userequire:. - Disable a cop inline only with a reason:
# rubocop:disable Metrics/MethodLength -- generated mapping table. - Do not churn style across untouched files in a feature change; fix offenses in lines you touch.
Verification
-
bundle exec rubocoppasses for changed files. - Tests pass; new behaviour has a test (spec or minitest).
-
ruby -W:deprecated -c file.rbfor syntax; run the suite withRUBYOPT=-W:deprecatedon 3.4+ to surface chilled-string mutations. - For libraries, the gemspec’s
required_ruby_versioncovers every syntax used. - If types exist (RBS with Steep, or Sorbet), run
steep checkorsrb tc.
Gotchas
0,""and[]are truthy; onlynilandfalseare falsy.- Default hash values are shared:
Hash.new([])reuses one array; useHash.new { |h, k| h[k] = [] }. attr_accessorsetters needself.inside the class (self.name = x), otherwise a local variable is created.privatedoes not apply todef self.method; useprivate_class_method.- Symbols and strings are different hash keys; JSON parsing yields strings unless you
pass
symbolize_names: true. and/orhave lower precedence than=; use&&/||in expressions.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Ruby Idioms"
short_description: "Ruby that reads like Ruby, not translated Java"
default_prompt: "$ruby-idioms Refactor this Ruby service object to be more idiomatic" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.