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.

  • Languages
  • $ruby-idioms
  • 196 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- ruby-idioms

Installs to ~/.agents/skills/ruby-idioms/ for every project on this machine.

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

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.

~/.agents/skills/ruby-idioms/SKILL.md

--- name: ruby-idioms description: Writes and refactors plain Ruby the way experienced Rubyists expect - Enumerable chains instead of index loops, blocks, procs and lambdas used… license: MIT ---

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

  1. Read AGENTS.md, .ruby-version / .tool-versions, Gemfile, Gemfile.lock and .rubocop.yml / .standard.yml. The Ruby version decides which features exist.
  2. Run commands through Bundler so the locked gem versions are used: bundle exec rubocop, bundle exec rspec or bundle exec rake test.
  3. If bundle install is needed and gems are missing, it needs network access; ask before installing.
  4. Note whether the file has # frozen_string_literal: true; keep it consistent with the rest of the codebase.

Feature availability

FeatureSinceUse 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.0one-expression readers
Hash#except, Enumerable#tally, filter_map3.0 / 2.7everyday collection work
Shorthand hash {x:, y:}3.1building hashes from same-named locals
Data.define3.2immutable value objects
it block parameter3.4short single-param blocks (map { it.upcase })
Chilled string literals warn on mutation3.4prepares 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 ofWrite
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 { ... }.firstxs.find { ... }
xs.map { ... }.flattenxs.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 rejectok, bad = xs.partition(&:valid?)
manual groupingxs.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

  • yield plus block_given? for a single optional block; take &block only 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 return exits the lambda; procs ignore extra args and return exits the enclosing method. Use lambdas for stored callables.
  • &:method_name for 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) over Struct for new value objects; use Struct only 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 Comparable and define <=>; include Enumerable and define each.
  • Duck type with respond_to? sparingly; prefer calling the method and letting NoMethodError surface during tests.
  • If you override method_missing, also override respond_to_missing?. Better: define methods explicitly or with define_method at 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 => e catches StandardError; never rescue Exception (it swallows Interrupt and SystemExit).
  • Re-raise with context using raise Billing::Error, "charging #{id} failed" inside a rescue: Ruby sets cause automatically.
  • Use ensure for cleanup or, better, block-form APIs (File.open(path) { |f| ... }).
  • retry only with a counter and backoff.

Strings and mutation

  • With # frozen_string_literal: true, build strings with interpolation or +"" / String.new when you need a mutable buffer; << on a literal raises FrozenError.
  • 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), dig for 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; -A applies 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.yml on RuboCop 1.72+; older configs use require:.
  • 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 rubocop passes for changed files.
  • Tests pass; new behaviour has a test (spec or minitest).
  • ruby -W:deprecated -c file.rb for syntax; run the suite with RUBYOPT=-W:deprecated on 3.4+ to surface chilled-string mutations.
  • For libraries, the gemspec’s required_ruby_version covers every syntax used.
  • If types exist (RBS with Steep, or Sorbet), run steep check or srb tc.

Gotchas

  • 0, "" and [] are truthy; only nil and false are falsy.
  • Default hash values are shared: Hash.new([]) reuses one array; use Hash.new { |h, k| h[k] = [] }.
  • attr_accessor setters need self. inside the class (self.name = x), otherwise a local variable is created.
  • private does not apply to def self.method; use private_class_method.
  • Symbols and strings are different hash keys; JSON parsing yields strings unless you pass symbolize_names: true.
  • and/or have lower precedence than =; use &&/|| in expressions.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem