Claude Skill

sota-ruby

State-of-the-art Ruby engineering rules (2026 baseline, Ruby 3.4+ / 4.0) that Claude applies when writing or auditing Ruby. Covers modern idioms (frozen string literals, pattern matching, Data/Struct, RBS/Sorbet/Steep typing), security (SQL injection via ActiveRecord/Sequel, ERB/

LLM Mart · 0 points · 7 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download martinholovsky-SOTA-skills-skills_sota-ruby-ec2abf6.zip · 28 KB
Part of martinholovsky/sota-skills — 39 skills

Install

skills CLI npx skills add https://github.com/martinholovsky/SOTA-skills/tree/main/skills/sota-ruby
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install martinholovsky-sota-skills@llmmart
Git git clone https://github.com/martinholovsky/SOTA-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole martinholovsky/sota-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

SOTA Ruby (2026)

Expert-level rules for producing and auditing production Ruby. Baseline language line: Ruby 3.4+, with Ruby 4.0 (released 2025-12-25) as the latest major line. Per the official branches page: 4.0 and 3.4 are in normal maintenance; 3.3 is security-maintenance only (expected EOL 2027-03); 3.2 and older are EOL (3.2 since 2026-04-01) — running them is itself a finding. Feature notes: Data.define and Regexp.timeout from 3.2, it block parameter and chilled-string warnings from 3.4, Ractor::Port and experimental ZJIT from 4.0 — noted where relevant. Every rules file ends with an audit checklist of grep/lint patterns.

Purpose

Two consumers, one source of truth:

  • BUILD mode — generating new Ruby code: follow the rules as defaults, not suggestions. Deviate only with an explicit comment justifying it.
  • AUDIT mode — reviewing existing Ruby code: hunt violations using the audit checklists, classify by severity, report in the finding format below.

BUILD mode

  1. Before writing code, read the rules files relevant to the task (see index). A web endpoint touching the DB and a background job needs 02, 03, 05.
  2. Apply the top-10 non-negotiables (below) unconditionally.
  3. Establish context first: .ruby-version, Gemfile/Gemfile.lock, RuboCop or StandardRB config, framework and test runner in use. Match the project's floor (no it block param on a 3.3 project).
  4. New projects: pin the Ruby version (.ruby-version), commit Gemfile.lock, add RuboCop or StandardRB, bundler-audit, and the test suite to CI from day one (see rules/04). Rails apps add Brakeman.
  5. Security posture is non-optional even when unrequested: parameterized SQL, argv-form process spawning, YAML.safe_load semantics, SecureRandom, escaped output (see rules/02, rules/03).
  6. Write tests alongside the code (RSpec or Minitest — match the project). Anything with threads or jobs gets an idempotency/concurrency test.
  7. When code must violate a rule for a legitimate reason, leave a # NOTE(sota): comment explaining the trade-off so auditors don't flag it.

AUDIT mode

Work through each relevant rules file's audit checklist against the target repo. Run the listed grep/lint commands; confirm each hit manually before reporting (greps are recall-oriented, expect false positives). Useful mechanical sweeps: bundle exec rubocop, bundler-audit check --update, brakeman -q (Rails), plus the per-file greps.

Severity conventions

Severity Meaning Examples
CRITICAL Exploitable now, or data loss SQL built with #{} interpolation, Marshal.load/YAML.unsafe_load on external data, command injection via backticks with user input, html_safe on user input
HIGH Exploitable with preconditions, or production-breaking Missing CSRF protection on state-changing routes, permit!, ^/$ anchors in validation regexes, rand for tokens, Timeout.timeout around DB work, EOL Ruby in production
MEDIUM Correctness/maintenance hazard, latent bug N+1 on a hot path, non-idempotent retried jobs, no Gemfile.lock in an app, mutable shared state across threads without a lock, rescue Exception
LOW Deviation from SOTA, friction Missing frozen-string-literal comments, Struct where Data fits, stringly-typed booleans, unpinned dev tooling
INFO Worth knowing, no action forced YJIT not enabled, typing (RBS/Sorbet) absent, newer-Ruby features available after a floor bump

Finding format

file:line | rule violated (rules/NN §section) | severity | effort | fix

Severity: Critical / High / Medium / Low / Info. Effort: trivial / small / medium / large. Borderline severities state the deciding assumption; unconfirmed findings are marked "needs verification", never asserted. Group findings by severity, CRITICAL first; end with counts per severity, the three highest-leverage fixes, and which checklists were run.

Rules index

File Read this when...
rules/01-language-idioms.md Choosing/verifying the Ruby version baseline; frozen string literals; pattern matching; Data vs Struct; exception design; nil over a sentinel, and to_i silently returning 0 for garbage; typing with RBS/Sorbet/Steep; general idioms and pitfalls
rules/02-security.md Any input crossing a trust boundary: SQL injection (ActiveRecord/Sequel), command injection (system/backticks/Open3), deserialization (Marshal, YAML/Psych), ReDoS and regex anchors, eval/send/constantize, secrets and randomness, path traversal
rules/03-web-hardening.md Building or auditing anything web-facing: XSS/ERB escaping, mass assignment and strong params, CSRF, sessions/cookies, security headers, open redirects, SSRF, file uploads — framework-neutral
rules/04-supply-chain-tooling.md Bundler and Gemfile.lock discipline, lockfile checksums, bundler-audit, RuboCop/StandardRB, Brakeman, RSpec/Minitest mechanics, CI gates, gem authoring/publishing
rules/05-concurrency-performance.md Threads, fibers, Ractors, and the GVL; background-job idempotency; YJIT/ZJIT; GC and memory (allocator, RSS); N+1 detection; profiling workflow

Top-10 non-negotiables

  1. Supported Ruby only — 3.4+ in production (3.3 accepted short-term with an upgrade plan; ≤3.2 is a finding). Pin it in .ruby-version and CI. (rules/01)
  2. SQL only via parameterized queries — hash conditions or ?/named placeholders in ActiveRecord/Sequel; string-interpolated SQL is CRITICAL, no exceptions for "internal" values. (rules/02)
  3. Never Marshal.load, YAML.unsafe_load, or eval-family on data you don't fully control; YAML.load is safe-by-default only on Psych 4+ (Ruby 3.1+) — verify the runtime. (rules/02)
  4. Processes spawn with argv lists (system("cmd", arg), Open3.capture2), never a shell string containing external input; no Kernel#open/URI.open on user-supplied names. (rules/02)
  5. All HTML output escaped by default; every raw/html_safe is reviewed; non-Rails ERB configured to auto-escape. (rules/03)
  6. Mass assignment goes through an attribute allowlist (strong params / params.expect; explicit attribute lists elsewhere); permit! is HIGH. (rules/03)
  7. CSRF protection on every cookie-authenticated state-changing endpoint; \A/\z (never ^/$) to anchor validation regexes. (rules/02, rules/03)
  8. Gemfile.lock committed and CI installs frozen (BUNDLE_FROZEN=true); bundler-audit gates CI; git-sourced gems pinned to a SHA. (rules/04)
  9. SecureRandom (never rand/Random) for anything security-relevant; constant-time comparison for secrets. (rules/02)
  10. Background jobs are idempotent and enqueue after commit — at-least-once delivery and retries are the contract; jobs take IDs, not objects. (rules/05)
Files (sota-skills)
  • rules
    • 01-language-idioms.md 11.9 KB
      # 01 — Language baseline & idioms
      
      Modern Ruby (2026 baseline): a supported interpreter, frozen string literals
      declared everywhere, pattern matching for structured data, `Data` for value
      objects, exceptions designed as an API, and (optionally but increasingly)
      gradual typing via RBS-based tooling or Sorbet.
      
      ## 1. Version baseline and support policy
      
      Per the official [maintenance branches page](https://www.ruby-lang.org/en/downloads/branches/)
      (checked 2026-07):
      
      | Line | Status | Notes |
      |---|---|---|
      | **4.0** | normal maintenance | current stable; released 2025-12-25 |
      | **3.4** | normal maintenance | released 2024-12-25 |
      | **3.3** | security maintenance only | expected EOL 2027-03 |
      | **≤ 3.2** | **EOL** | 3.2 reached EOL 2026-04-01 |
      
      Rules:
      
      - **Target 3.4+ for new code; 4.0 for new projects.** An app in production on
        an EOL line (≤3.2) is a HIGH finding on its own — no CVE fixes.
      - Pin the version in `.ruby-version` and reference it from CI; the `Gemfile`
        states a compatible `ruby` requirement (`ruby file: ".ruby-version"`).
      - Feature floor map (use only what the project's floor allows):
        - **3.2**: `Data.define`, `Regexp.timeout=`/`Regexp.linear_time?`
          ([3.2 release](https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/))
        - **3.3**: Prism parser available; per-line GC/perf work
        - **3.4**: `it` implicit block parameter, chilled-string deprecation
          warnings, Prism as default parser
          ([3.4 release](https://www.ruby-lang.org/en/news/2024/12/25/ruby-3-4-0-released/))
        - **4.0**: `Ractor::Port`, experimental ZJIT, `Set` as a core class,
          reserved `Ruby` namespace module
          ([4.0 release](https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/))
      
      ## 2. Frozen string literals
      
      String literals are **still not frozen by default, even in Ruby 4.0** (the
      [4.0 release notes](https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/)
      contain no change; the multi-release migration plan is
      [Feature #20205](https://bugs.ruby-lang.org/issues/20205)). Since 3.4,
      literals in files *without* the magic comment are "chilled": mutation works
      but emits a deprecation warning when `Warning[:deprecated] = true`.
      
      Rules:
      
      - **Every file starts with `# frozen_string_literal: true`** (after the
        shebang, before code). Enforce with RuboCop
        `Style/FrozenStringLiteralComment` (StandardRB includes an equivalent).
      - Need a mutable string? Be explicit: `+"literal"`, `String.new`, or `dup`.
      - Build strings with interpolation or `<<` on an explicitly-unfrozen buffer,
        never `+=` in a loop (allocates a new string per iteration).
      - In BUILD mode, run test suites with `RUBYOPT="-W:deprecated"` periodically
        so chilled-string mutations surface before the eventual frozen default.
      
      ## 3. Pattern matching
      
      `case/in` (stable since 3.1) is the idiomatic way to destructure nested
      Hash/Array/JSON-shaped data — prefer it over chained `dig`/`is_a?`/`key?`.
      
      ```ruby
      case parsed_event
      in { type: "payment", amount: Integer => cents, currency: String => cur }
        record_payment(cents, cur)
      in { type: "refund", **rest }
        handle_refund(rest)
      else
        raise UnknownEventError, parsed_event.inspect
      end
      ```
      
      Rules:
      
      - **Always handle the fall-through**: a bare `case/in` raises `NoMatchingPatternKeyError`/
        `NoMatchingPatternError` on no match — that's often *desired* (fail fast on
        unexpected shapes); otherwise write an `else`.
      - Use the **pin operator** `^` to match against an existing variable
        (`in { user_id: ^current_id }`); without it you *bind*, not compare —
        a classic logic bug.
      - Rightward assignment + destructure for one-shot extraction:
        `response => { data: { id: } }` (raises if the shape is wrong — a free
        schema assert at trust boundaries).
      - Custom classes participate via `deconstruct` (array patterns) and
        `deconstruct_keys` (hash patterns) — implement them on domain value objects.
      - Keep patterns shallow; three-plus levels of nesting means the parsing
        belongs in a dedicated mapper object.
      
      ## 4. Data vs Struct
      
      - **`Data.define` (3.2+) is the default for value objects**: immutable
        (members can't be reassigned), keyword-initialized, value equality,
        `#with` for updated copies, `deconstruct_keys` for pattern matching.
      
      ```ruby
      Money = Data.define(:cents, :currency) do
        def +(other) = with(cents: cents + other.cents)
      end
      ```
      
      - `Struct` remains for legacy code and when you genuinely need mutability or
        positional construction. Pitfalls: `Struct.new(...)` without
        `keyword_init: true` takes positional args (silent nil members if you pass
        too few), and members are mutable by default.
      - Neither replaces a real class once behavior dominates data.
      - Don't use `OpenStruct` in new code — slow, defeats typing and method
        resolution; a `Data`, `Hash`, or class is always better.
      
      ## 5. Exception design
      
      - **Library/base exceptions inherit from `StandardError`**, never `Exception`
        directly. Define one root per gem/app (`class MyApp::Error < StandardError`)
        and subclass from it so callers can `rescue MyApp::Error`.
      - **Never `rescue Exception`** — it swallows `SignalException`,
        `SystemExit`, and `NoMemoryError`. Bare `rescue` catches `StandardError`
        (acceptable but be explicit).
      - **No `rescue nil`** or `rescue => e; nil` around logic that matters —
        silenced failure is the number-one source of "impossible" production state.
      - Re-raise with context: `raise MyApp::FetchError, "user #{id}: #{e.message}"`
        inside a `rescue e` keeps the `#cause` chain automatically — never
        `raise e.message` (loses class and cause).
      - `retry` only with a bounded counter and backoff; unbounded `retry` is a
        spin loop.
      - `ensure` blocks must not `return` or `raise` new errors casually — both
        swallow the in-flight exception.
      - **Exceptions are for exceptional flow**, not control flow: `Hash#fetch`
        with a default, `find` vs `find!`-style APIs — pick the non-raising variant
        when absence is normal.
      
      ## 5a. In-band sentinels — the stdlib is clean, the converters are not
      
      Ruby's search methods return `nil`, not `-1`: `"abc".index("z")` and
      `[1,2].index(3)` are both `nil` (verified, Ruby 4.0.6). So the class in
      `sota-architecture` rules/02 §8a rarely arrives from the stdlib's search API — it
      arrives from **conversion** and from hand-rolled returns.
      
      - `"x".to_i` is `0` and `"12abc".to_i` is `12` (verified) — garbage and a partial
        parse both succeed silently, and `0` is indistinguishable from `"0".to_i`. Use
        `Integer(str)`, which raises `ArgumentError` (verified), or
        `Integer(str, exception: false)` which returns `nil`. Same for `to_f`/`Float()`.
      - `nil` is falsy and `0`/`-1` are **truthy** in Ruby, so `if n` is a correct presence
        check against `nil` and a broken one against a numeric sentinel. That asymmetry is
        the argument for keeping absence as `nil` all the way down.
      - Returning `-1`/`0`/`false` from your own method where `nil` fits: don't. `nil` +
        `&.`/`then`/pattern matching (§3) is the idiom; a sentinel opts out of all three.
      - RBS/Sorbet: type it `Integer?`, and the checker will make callers handle it. A
        magic `-1` inside `Integer` is invisible to Steep and Sorbet alike.
      - Audit: `grep -rnE '\.to_i\b|\.to_f\b' --include='*.rb' app/ lib/ | grep -v 'to_i\.to_s'`
        and `grep -rnE 'return (-1|0)$' --include='*.rb' app/ lib/`.
      
      ## 6. Typing: RBS, Sorbet, Steep
      
      Gradual typing is optional but SOTA for libraries and large apps. Two
      ecosystems (neutral examples — match what the project already uses):
      
      - **RBS** — the standard signature format, bundled with Ruby; signatures live
        in `sig/*.rbs` next to code. Checked by **Steep**; **TypeProf** can
        generate draft signatures.
      - **Sorbet** — inline `sig { params(x: Integer).returns(String) }`
        annotations, fast whole-program checker (`srb tc`), optional runtime
        checks; `# typed:` sigil per file (`false`/`true`/`strict`).
      
      Rules:
      
      - Pick **one** checker and gate CI with it; mixed half-adopted setups rot.
      - Type the public API first (boundaries where wrong shapes enter); internals
        can stay untyped longer.
      - Don't fight the checker with casts (`T.unsafe`, `T.untyped` everywhere, or
        `untyped` in RBS) — an escape-hatch density above a few per file means the
        design, not the checker, is wrong.
      - No checker? Then at minimum: keyword arguments for 3+-arg methods, `fetch`
        over `[]` at boundaries, and pattern-matching shape asserts on parsed input.
      
      ## 7. General idioms and pitfalls
      
      - **Keyword arguments** for any method where call-site meaning isn't obvious;
        required keywords (`def pay(amount:, currency:)`) over option hashes.
      - **`&.` (safe navigation) only when nil is a valid domain state** — chains of
        `&.` hide broken invariants; prefer failing fast.
      - **Predicate methods end in `?` and return booleans**; bang methods `!` are
        the *more dangerous* variant of an existing method (mutates, raises), not
        a naming decoration.
      - **Enumerable over manual loops**: `map`/`select`/`sum`/`each_slice`;
        `each_with_object` over `inject` for building collections; lazy
        (`.lazy`) for large/infinite chains.
      - **Comparable**: implement `<=>` + `include Comparable` instead of six
        operators.
      - **Monkey patching core classes is forbidden in app code**; if unavoidable
        in a gem, use a `Refinement` or a clearly-namespaced module prepend, and
        document it.
      - **`require_relative` within a project, `require` for gems**; no code
        execution at require time beyond definitions (side-effectful requires break
        autoloading and testing).
      - Time: **`Time.now.utc` / monotonic clocks for durations**
        (`Process.clock_gettime(Process::CLOCK_MONOTONIC)`); never subtract two
        `Time.now` calls for measuring elapsed time in production code.
      - Equality: `==` for values, `equal?` only for identity, `eql?`+`hash` pair
        when used as Hash keys.
      - `method_missing` requires a matching `respond_to_missing?`; prefer
        `define_method` metaprogramming that produces real, introspectable methods.
      
      ## Audit checklist
      
      Run from repo root; verify each hit manually.
      
      **In-band sentinels** (§5a) — Ruby's search API returns `nil`, so these arrive from
      conversion and from hand-rolled returns:
      
      ```bash
      grep -rnE '\.to_i\b|\.to_f\b' --include='*.rb' app/ lib/     # 0 on garbage, 12 on "12abc" — use Integer(s)
      grep -rnE 'return (-1|0)$' --include='*.rb' app/ lib/         # prefer nil, which is falsy and pattern-matchable
      ```
      
      ```bash
      # Interpreter floor — EOL Ruby is HIGH
      cat .ruby-version 2>/dev/null; grep -n "^ruby" Gemfile 2>/dev/null
      # (compare against the branches page table above)
      
      # Missing frozen_string_literal comments — LOW (bulk-fix with rubocop -a)
      grep -rL "frozen_string_literal: true" --include='*.rb' app/ lib/ 2>/dev/null | head
      
      # rescue Exception — MEDIUM (HIGH if it wraps a main loop)
      grep -rn "rescue Exception" --include='*.rb' .
      
      # Silenced errors — MEDIUM+
      grep -rn "rescue nil" --include='*.rb' .
      grep -rnE "rescue(\s+StandardError)?\s*(=>\s*_?e?)?\s*$" --include='*.rb' . | head
      
      # raise losing the original class/cause
      grep -rnE "raise\s+e\.message" --include='*.rb' .
      
      # OpenStruct in new code — LOW
      grep -rn "OpenStruct" --include='*.rb' .
      
      # Struct without keyword_init (positional-arg hazard) — INFO/LOW
      grep -rn "Struct.new" --include='*.rb' . | grep -v keyword_init
      
      # Pattern matching without pin where comparison was intended (manual review)
      grep -rnE "in \{[^}]*: [a-z_]+ *\}" --include='*.rb' . | head
      
      # Monkey patches on core classes — MEDIUM in app code
      grep -rnE "^\s*class (String|Array|Hash|Integer|Symbol|Object)\b" --include='*.rb' app/ lib/ 2>/dev/null
      
      # method_missing without respond_to_missing?
      grep -rln "def method_missing" --include='*.rb' . | xargs grep -L "respond_to_missing?" 2>/dev/null
      
      # Wall-clock durations — LOW
      grep -rnE "Time\.now.*-.*Time\.now|=\s*Time\.now\b.*# .*(elapsed|duration)" --include='*.rb' . | head
      
      # Typing posture — INFO
      ls sig/ sorbet/ 2>/dev/null; grep -rn "# typed:" --include='*.rb' . | head -3
      ```
      
      Severity guide: EOL interpreter HIGH; `rescue Exception`/`rescue nil` around
      critical logic MEDIUM–HIGH; missing frozen-string comments LOW (bulk-fixable);
      `OpenStruct`/`Struct` misuse LOW; absent typing INFO.
      
    • 02-security.md 10.3 KB
      # 02 — Security: injection, deserialization, ReDoS, secrets
      
      Ruby-specific rules for input crossing a trust boundary. Primary references:
      [OWASP Ruby on Rails Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Ruby_on_Rails_Cheat_Sheet.html)
      and the framework security guides (e.g.
      [Rails Securing guide](https://guides.rubyonrails.org/security.html)) — the
      patterns below apply to any Ruby codebase, framework or not. Web-surface
      issues (XSS, CSRF, mass assignment) live in `rules/03`; generic secure-coding
      depth in `sota-code-security`.
      
      ## 1. SQL injection — parameterize everywhere
      
      String interpolation into SQL is CRITICAL regardless of where the value
      "comes from" — trust levels change over refactors.
      
      **ActiveRecord** (neutral example):
      
      ```ruby
      # BAD — injectable
      User.where("name = '#{params[:name]}'")
      User.order(params[:sort])                       # column-name injection
      User.pluck(Arel.sql(params[:col]))              # attacker-chosen SQL
      
      # GOOD
      User.where(name: params[:name])                  # hash conditions
      User.where("email = ?", params[:email])          # positional placeholder
      User.where("created_at > :since", since: since)  # named placeholder
      User.order(Arel.sql(SORTS.fetch(params[:sort]))) # allowlist -> literal
      ```
      
      - `order`, `group`, `select`, `pluck`, `joins`, `having` with raw strings are
        injection sinks too — anything user-influenced goes through an **allowlist
        lookup**, never straight in. Rails raises on unrecognized raw SQL in some
        of these unless wrapped in `Arel.sql` — treat every `Arel.sql` as an audit
        point.
      - `LIKE` patterns: escape with `sanitize_sql_like(term)` before binding,
        or `%` / `_` become wildcards (data disclosure, DoS-y scans).
      
      **Sequel** (neutral example): use placeholders (`where("name = ?", n)`) or
      virtual rows (`where { created_at > since }`); every `Sequel.lit`
      with interpolation is the same CRITICAL as raw string SQL.
      
      Raw drivers (`pg`, `mysql2`, `sqlite3`): use `exec_params`/prepared
      statements; never `exec("... #{v}")`.
      
      ## 2. Command injection — argv form, never shell strings
      
      The shell-string forms of `system`, backticks/`%x`, `exec`, `spawn`,
      `IO.popen`, and `Open3.*` all pass through `/bin/sh` when given a single
      string with metacharacters.
      
      ```ruby
      # BAD — all CRITICAL with external input
      system("convert #{upload_path} out.png")
      `git clone #{repo_url}`
      IO.popen("grep #{pattern} log.txt")
      
      # GOOD — argv form, no shell involved
      system("convert", upload_path, "out.png")
      out, status = Open3.capture2("git", "clone", "--", repo_url)
      IO.popen(["grep", "--", pattern, "log.txt"])
      ```
      
      - Prefer `Open3.capture2/capture3` (argv form) when you need output + status;
        backticks give no status separation and invite interpolation.
      - `--` before positional user args stops option injection (`-oProxyCommand=`
        class attacks).
      - `Shellwords.escape` is a last resort for legacy shell-string call sites —
        argv form is strictly safer.
      - **`Kernel#open` / `URI.open` (open-uri) execute a subprocess when the
        argument starts with `|`** — never call them with an external filename;
        use `File.open` for files and `Net::HTTP`/an HTTP client for URLs.
      
      ## 3. Insecure deserialization
      
      - **`Marshal.load` on external data is CRITICAL** — arbitrary object
        instantiation leading to RCE gadgets. That includes data from cookies,
        caches (e.g. a shared Redis/Memcached a less-trusted system can write to),
        message queues, and files users can influence. Use JSON or MessagePack for
        interchange. Rails note: cache/cookie serializers default to safer JSON
        formats in recent versions — flag any explicit `serializer: :marshal`
        fed by semi-trusted writers.
      - **YAML/Psych**: since Psych 4 (bundled from Ruby 3.1),
        [`YAML.load` has `safe_load` semantics](https://docs.ruby-lang.org/en/master/Psych.html)
        — only basic types, **aliases disabled** by default. Rules:
        - `YAML.unsafe_load` / `YAML.load_stream(..., unsafe: ...)` on external
          data is CRITICAL (same gadget class as Marshal).
        - Extra classes go through `permitted_classes: [Date, Symbol, ...]`, never
          a switch to `unsafe_load`.
        - Alias-using config files: `YAML.safe_load(s, aliases: true)` — note
          aliases enable billion-laughs-style expansion, so only for trusted files.
        - Code still on Ruby ≤3.0/Psych 3 (EOL anyway): `YAML.load` there is
          unsafe-by-default — treat every call as `unsafe_load`.
      - **JSON**: `JSON.parse` is safe; `JSON.load` / `create_additions: true`
        enables object revival via `json_class` — don't use it on external input.
      - **CSV**: `CSV` with `converters: :all` can build unexpected types; also
        remember spreadsheet formula injection (`=cmd|...`) when *emitting* CSV
        from user data — prefix `'` on `=`, `+`, `-`, `@` cells.
      
      ## 4. eval, send, and reflection sinks
      
      - `eval`, `instance_eval`/`class_eval` **with string arguments**, and
        `Binding#eval` on anything user-influenced is CRITICAL. Block forms
        (`instance_eval { ... }`) don't interpolate input and are fine.
      - `send`/`public_send` with a user-controlled method name lets callers reach
        any method (`send(params[:action])` → `send(:destroy_all)`). Allowlist:
        `ACTIONS.fetch(params[:action])`, and prefer `public_send` always.
      - `constantize`/`safe_constantize` (or `Object.const_get`) on user input is
        unsafe reflection — instantiating an attacker-chosen class is a gadget
        entry point. Allowlist class names explicitly.
      - ERB/template injection: `ERB.new(user_supplied_template).result(binding)`
        is code execution by design. User-editable templates need a sandboxed
        engine (e.g. Liquid as a neutral example), never ERB/Haml/Slim.
      
      ## 5. ReDoS and regex correctness
      
      - **Anchor validations with `\A` and `\z` — never `^`/`$`.** In Ruby, `^`/`$`
        match *line* boundaries, so `/^https?:\/\/\S+$/` accepts
        `"javascript:x\nhttp://ok"` — a validation bypass, HIGH. (`\Z` allows a
        trailing newline; almost always you want `\z`.)
      - ReDoS: nested quantifiers / overlapping alternations
        (`/(a+)+$/`, `/(\w+\s?)*$/`) explode on crafted input. Since Ruby 3.2 most
        patterns are memoized to linear time, and two guards exist
        ([3.2 release](https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/)):
        - Set a **global budget**: `Regexp.timeout = 1.0` (seconds) at boot;
          per-regex override `Regexp.new(src, timeout: 0.1)`.
        - Check hot, input-facing patterns with `Regexp.linear_time?(re)`.
      - Don't build regexes by interpolating user input; if unavoidable,
        `Regexp.escape(input)` first.
      
      ## 6. Secrets, randomness, comparison
      
      - **`SecureRandom`** (`hex`, `uuid`, `urlsafe_base64`, `alphanumeric`) for
        tokens, nonces, password-reset codes, API keys. `rand`, `Random`,
        `Array#sample`, `shuffle` are predictable (Mersenne Twister) — HIGH when
        used for anything security-relevant.
      - Compare secrets in constant time:
        `OpenSSL.fixed_length_secure_compare(a, b)` (or Rack/ActiveSupport
        `secure_compare` as neutral examples). `==` on HMACs/tokens is a timing
        oracle.
      - Passwords: bcrypt/argon2 via a maintained gem (`has_secure_password` uses
        bcrypt as a neutral example) — never `Digest::SHA256` of a password.
      - No secrets in code or `ENV`-committed files; load via the deployment
        platform or an encrypted store (see `sota-secrets-management`). Grep
        targets: `_key = "`, `password = "`, `Aws.config`.
      
      ## 7. Files and paths
      
      - Path traversal: anything joining user input into a path needs
        canonicalize-then-check:
      
      ```ruby
      base = File.expand_path("uploads")
      path = File.expand_path(name, base)
      raise SecurityError unless path.start_with?(base + File::SEPARATOR)
      ```
      
      - `File.basename(user_name)` before storing uploads; never trust
        client-supplied filenames or content types (see `rules/03` §uploads).
      - Archive extraction (zip/tar gems): validate each entry name against the
        same expand-and-prefix check — zip-slip.
      - Temp files: `Tempfile`/`Dir.mktmpdir`, not hand-built `/tmp/#{name}`.
      
      ## Audit checklist
      
      Run from repo root; verify each hit manually. `brakeman -q` (Rails) and
      `bundle exec rubocop --only Security` cover several of these mechanically.
      
      ```bash
      # SQL injection — CRITICAL on any hit with non-literal interpolation
      grep -rnE '\.(where|order|group|having|select|joins|pluck|find_by_sql|update_all)\s*\(\s*["'"'"'][^)]*#\{' --include='*.rb' .
      grep -rn "Arel.sql" --include='*.rb' .
      grep -rn "Sequel.lit" --include='*.rb' . | grep '#{'
      grep -rnE '\.(exec|query)\s*\(\s*["'"'"'][^)]*#\{' --include='*.rb' .
      
      # Command injection — CRITICAL with external input
      grep -rnE '(system|exec|spawn)\s*\(\s*["'"'"'][^,)]*#\{' --include='*.rb' .
      grep -rnE '`[^`]*#\{|%x[({\[][^)}\]]*#\{' --include='*.rb' .
      grep -rnE 'IO\.popen\s*\(\s*["'"'"']' --include='*.rb' .
      grep -rnE '(Kernel#?open|URI\.open|[^.]open)\s*\(\s*(params|.*user|.*input)' --include='*.rb' . | head
      
      # Deserialization — CRITICAL on external data
      grep -rn "Marshal.load\|Marshal.restore" --include='*.rb' .
      grep -rn "unsafe_load\|YAML.load_documents" --include='*.rb' .
      grep -rn "create_additions" --include='*.rb' .
      grep -rnE "YAML\.(safe_)?load[^_]" --include='*.rb' . | grep "aliases: true"
      
      # eval / reflection sinks
      grep -rnE '\beval\s*\(|instance_eval\s*\(\s*["'"'"']|class_eval\s*\(\s*["'"'"']' --include='*.rb' .
      grep -rnE '\b(public_)?send\s*\(\s*params' --include='*.rb' .
      grep -rn "constantize\|const_get" --include='*.rb' . | grep -iE "params|input|name"
      grep -rn "ERB.new" --include='*.rb' . | grep -vE "erb\"|template_file|File.read\(\s*Rails"
      
      # Regex: ^/$ anchors in validations — HIGH; ReDoS candidates
      grep -rnE 'format:.*(\^|\$)|match\?\(/\^' --include='*.rb' . | grep -v '\\\\A'
      grep -rn "Regexp.timeout" --include='*.rb' config/ . 2>/dev/null | head -1  # absent = note it
      grep -rnE '\((\.\*|\\w\+|\[[^]]+\]\+)\)[+*]' --include='*.rb' . | head      # nested quantifiers
      
      # Randomness / comparison — HIGH for security uses
      grep -rnE '\brand\(|Random\.(rand|new)|\.sample\b' --include='*.rb' . | grep -viE "spec|test|seed"
      grep -rnE '(token|hmac|signature|digest)\s*==' --include='*.rb' .
      
      # Path traversal
      grep -rnE 'File\.(open|read|write|join)\([^)]*params' --include='*.rb' .
      ```
      
      Severity guide: interpolated SQL / shell string with external input,
      `Marshal.load`/`unsafe_load` on external data, string `eval` — CRITICAL.
      `^$` validation anchors, `rand` tokens, `send(params[...])`,
      non-constant-time secret compare — HIGH. Missing `Regexp.timeout` on an
      input-facing regex service, `aliases: true` on semi-trusted YAML — MEDIUM.
      
    • 03-web-hardening.md 9.5 KB
      # 03 — Web hardening (framework-neutral)
      
      Rules for any Ruby web surface — Rack app, Rails, Sinatra, Hanami, Grape,
      Roda all appear only as neutral examples; never assume which one a codebase
      uses. Establish the stack first (`Gemfile`, `config.ru`), then map each rule
      to that stack's mechanism. References:
      [OWASP Ruby on Rails Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Ruby_on_Rails_Cheat_Sheet.html),
      [Rails Securing guide](https://guides.rubyonrails.org/security.html),
      [Rack::Protection](https://github.com/sinatra/sinatra/tree/main/rack-protection).
      
      ## 1. XSS and output escaping
      
      Escaping behavior **differs by framework** — verify, don't assume:
      
      - **Rails ERB auto-escapes** by default. The escape hatches are the audit
        surface: every `raw(...)`, `.html_safe`, `<%== ... %>`, and
        `content_tag`/`tag` with interpolated attributes gets reviewed. `html_safe`
        on anything user-influenced is CRITICAL.
      - **Plain ERB / Sinatra do NOT auto-escape by default.** Enable it:
        Sinatra `set :erb, escape_html: true` (Erubi), or escape explicitly with
        `Rack::Utils.escape_html(x)` / `ERB::Util.html_escape(x)` at every
        interpolation. An unescaped-by-default template layer is a standing HIGH.
      - Hanami templates escape by default; the `raw` helper is the audit point.
      - Rich text/user HTML: sanitize with an allowlist sanitizer
        (`Rails::HTML5::Sanitizer` / `sanitize` helper, or the `sanitize` gem as
        neutral examples) — never regex-strip tags yourself.
      - Context matters: HTML-escaping does not make data safe inside
        `<script>`, inline event handlers, CSS, or URLs. JSON into script:
        `json_escape`/`.to_json` with escaping enabled; URLs: validate scheme
        (`http`/`https` allowlist — `javascript:` URLs pass naive checks).
      - Ship a Content-Security-Policy (framework DSL or a Rack middleware) —
        defense in depth, not a substitute for escaping.
      
      ## 2. Mass assignment
      
      Any endpoint that feeds a params hash into model creation/update must go
      through an **attribute allowlist**:
      
      ```ruby
      # Rails strong parameters (neutral example)
      params.require(:user).permit(:name, :email)
      params.expect(user: [:name, :email])   # Rails 8.0+, raises 400 on bad shape
      ```
      
      - `params.expect` (added in
        [Rails 8.0](https://guides.rubyonrails.org/8_0_release_notes.html)) also
        hardens against type-confusion (array-vs-hash) parameter attacks — prefer
        it on 8.0+.
      - **`permit!` (permit everything) is HIGH**, as is passing raw
        `params`/parsed JSON into `new`/`update`/`create`/`assign_attributes`.
      - Privilege fields (`role`, `admin`, `account_id`, `state`) never come from
        the request — set them server-side from the authenticated context; a
        separate admin flow has its own explicit permit list.
      - Non-Rails stacks: same rule, manual mechanism — `payload.slice(:name,
        :email)` (plus type validation via dry-validation/dry-schema or a contract
        object as neutral examples) before it touches the model. Sequel:
        `set_fields(params, [:name, :email])` over mass `set`.
      
      ## 3. CSRF
      
      - Every **cookie/session-authenticated, state-changing** endpoint needs CSRF
        protection. Rails: `protect_from_forgery with: :exception` (on by default
        in generated apps) — audit every `skip_before_action
        :verify_authenticity_token` and every `protect_from_forgery with:
        :null_session` on non-API controllers. Sinatra/Rack: `Rack::Protection`
        (`use Rack::Protection, :authenticity_token`) — plain Sinatra without it
        has **no CSRF protection**.
      - Token-authenticated APIs (Authorization header, no cookies) don't need
        CSRF tokens — but an "API" that also accepts session cookies does; that
        hybrid is the classic gap.
      - `SameSite=Lax` (or `Strict`) on session cookies is the second layer, not a
        replacement — older clients and subdomain issues remain.
      - GET routes must be side-effect free; CSRF middleware only guards
        non-idempotent verbs.
      
      ## 4. Sessions and cookies
      
      - Session cookies: `secure: true`, `httponly: true`, `same_site: :lax`
        minimum. Rack example:
        `use Rack::Session::Cookie, secure: true, httponly: true, same_site: :lax,
        secret: ENV.fetch("SESSION_SECRET")`.
      - **Rotate the session on privilege change** (`reset_session` at login /
        logout / role elevation) — session fixation otherwise.
      - The cookie-signing/encryption secret (`secret_key_base` in Rails; the Rack
        session secret elsewhere) is a production secret: ≥64 random bytes, never
        committed, rotated via the framework's rotation mechanism, distinct per
        environment.
      - Don't store authorization-deciding state client-side (even signed) if it
        must be revocable — server-side session or short-lived tokens.
      - Cookie size and content: no PII dumps in cookies; they traverse every
        request and end up in logs/CDNs.
      
      ## 5. Headers and transport
      
      - Force TLS: HSTS + redirect (Rails `config.force_ssl = true`; elsewhere the
        proxy/middleware). Behind a proxy, trust `X-Forwarded-Proto` only from the
        proxy you control.
      - Baseline headers (framework defaults or `Rack::Protection` /
        secure_headers-style middleware as neutral examples):
        `X-Content-Type-Options: nosniff`, `frame-ancestors` via CSP (or
        `X-Frame-Options: DENY`), `Referrer-Policy`, a real
        `Content-Security-Policy`.
      - Match `Host`/origin checking to deployment: Rails
        `config.hosts`; elsewhere validate `Host` against an allowlist — DNS
        rebinding and cache-poisoning use wildcard hosts.
      
      ## 6. Redirects and SSRF
      
      - **Open redirects**: `redirect_to params[:return_to]` is HIGH. Rails 7.0+
        raises on cross-host redirects unless `allow_other_host: true` — audit
        every `allow_other_host: true`. Neutral fix: allowlist paths
        (`redirect_to URI(raw).path`) or map named targets.
      - **SSRF**: any server-side fetch of a user-supplied URL
        (`Net::HTTP`, `URI.open`, Faraday/HTTParty as neutral examples) must:
        allowlist schemes (`https`), resolve and reject private/link-local/
        metadata ranges (127.0.0.0/8, 10/8, 172.16/12, 192.168/16, 169.254/16 —
        cloud metadata 169.254.169.254), cap redirects and re-validate each hop,
        and set open/read timeouts. `URI.open` on user input additionally risks
        `|command` execution (see `rules/02` §2).
      - Webhook/callback URL registration is SSRF-by-design — same checks plus
        egress via a dedicated proxy where available.
      
      ## 7. File uploads and downloads
      
      - Validate **server-side**: size cap, extension allowlist, and content
        sniffing (e.g. Marcel as a neutral example) — never trust the client
        `Content-Type` or filename (`File.basename` it, then generate your own
        name).
      - Store outside the served docroot (or object storage); serve with an
        explicit `Content-Type` and `Content-Disposition: attachment` for
        user-supplied files; never `send_file params[:path]` (traversal — see
        `rules/02` §7).
      - Image processing on untrusted files is an RCE-history hotspot
        (ImageTragick class) — keep processors current, restrict formats, consider
        sandboxing the worker (see `sota-sandboxing`).
      
      ## 8. Auth-adjacent essentials
      
      Deep authn/authz design lives in `sota-code-security`; the Ruby-shaped
      minimums:
      
      - Passwords via bcrypt/argon2 (`has_secure_password` as a neutral example);
        no home-rolled digests. Constant-time comparison for tokens (`rules/02` §6).
      - Rate-limit login/signup/reset (Rack::Attack middleware or Rails 7.2+
        `rate_limit` as neutral examples).
      - Authorization checked **per record**, not per route: loading
        `Model.find(params[:id])` without scoping to the authenticated
        tenant/owner (`current_user.things.find(...)`) is the standard IDOR.
      - Don't leak stack traces or framework error pages in production; error
        handlers return generic bodies and log the detail server-side.
      
      ## Audit checklist
      
      Run from repo root; verify each hit manually. Rails apps: run `brakeman -q`
      first — it covers XSS/mass-assignment/redirect sinks mechanically.
      
      ```bash
      # Escaping bypasses — CRITICAL if user-influenced
      grep -rnE '\.html_safe\b|raw\s*\(|<%==' --include='*.erb' --include='*.rb' app/ lib/ views/ 2>/dev/null
      # Sinatra/plain-ERB apps: is auto-escape on? absent = HIGH
      grep -rn "escape_html" --include='*.rb' . | head -3
      
      # Mass assignment
      grep -rn "permit!" --include='*.rb' .
      grep -rnE '(new|create|update|assign_attributes)\s*\(\s*params\b' --include='*.rb' . | grep -v permit
      grep -rnE 'permit\([^)]*(:role|:admin|:account_id|:state)' --include='*.rb' .
      
      # CSRF
      grep -rn "skip_before_action :verify_authenticity_token" --include='*.rb' .
      grep -rn "protect_from_forgery" --include='*.rb' . | head
      grep -rn "Rack::Protection" --include='*.rb' config.ru 2>/dev/null | head -1  # Sinatra: absent = HIGH
      
      # Sessions / cookies
      grep -rnE "Rack::Session::Cookie" --include='*.rb' config.ru 2>/dev/null | grep -v "secure: true"
      grep -rn "reset_session" --include='*.rb' . | head -1   # absent around login = MEDIUM
      grep -rn "secret_key_base\|SESSION_SECRET" --include='*.rb' --include='*.yml' . | grep -vE "ENV|credentials"
      
      # Redirects / SSRF
      grep -rnE 'redirect(_to)?\s*\(?\s*params' --include='*.rb' .
      grep -rn "allow_other_host: true" --include='*.rb' .
      grep -rnE '(Net::HTTP|URI\.open|Faraday|HTTParty)[^#]*params' --include='*.rb' .
      
      # Uploads / downloads
      grep -rnE 'send_file\s*\(?\s*params|send_file[^,]*#\{' --include='*.rb' .
      grep -rn "original_filename" --include='*.rb' . | grep -v basename
      
      # Transport
      grep -rn "force_ssl" --include='*.rb' config/ 2>/dev/null | head -1
      ```
      
      Severity guide: `html_safe`/`raw` on user input, `send_file params` —
      CRITICAL. `permit!`, missing CSRF on cookie-auth state changes, unescaped
      template layer, open redirect, unguarded SSRF fetch — HIGH. Missing
      `reset_session` on login, absent CSP/HSTS, client-trusted content type —
      MEDIUM.
      
    • 04-supply-chain-tooling.md 8.3 KB
      # 04 — Supply chain & tooling: Bundler, linting, scanning, CI
      
      The Ruby supply chain runs through RubyGems + Bundler; the quality gates are
      a linter, a security scanner, a dependency auditor, and the test suite —
      all wired into CI from day one.
      
      ## 1. Bundler and Gemfile discipline
      
      - **Applications commit `Gemfile.lock`.** Gems (libraries) don't ship it in
        the package but committing it for dev reproducibility is fine; keep the
        gemspec constraints permissive either way.
      - **CI and production install frozen**: `bundle config set --local frozen
        true`, `BUNDLE_FROZEN=true`, or `bundle install --frozen` — the build fails
        if `Gemfile` and lockfile drift instead of silently re-resolving.
      - Deployment installs also set `BUNDLE_WITHOUT=development:test`.
      - Version constraints: pessimistic (`~> 7.2`) for frameworks, exact pins only
        with a reason; `>=`-only constraints on security-sensitive gems are drift.
      - **Git-sourced gems pinned to a full SHA** (`git: ..., ref: "<sha>"`), never
        a branch — branches are mutable supply chain.
      - One global `source "https://rubygems.org"`; private gems go in a scoped
        `source "https://gems.example.internal" do ... end` block so a public gem
        can't shadow an internal name (dependency-confusion class).
      - Keep `bundle outdated` visible (report job), and update via PRs from
        Dependabot/Renovate (neutral examples) rather than bulk manual bumps.
      
      ## 2. Lockfile checksums
      
      Bundler 2.6 (2024-12) shipped lockfile checksum verification: a `CHECKSUMS`
      section records each gem's SHA-256 and installs fail if a downloaded gem no
      longer matches — protecting against registry tampering or a compromised
      mirror ([Bundler 2.6 announcement](https://bundler.io/blog/2024/12/19/bundler-v2-6.html)).
      
      ```bash
      bundle lock --add-checksums          # add CHECKSUMS to an existing lockfile
      bundle config lockfile_checksums true  # include in newly generated lockfiles
      ```
      
      - **Enable it** on apps: one command, no workflow change afterward.
      - On Bundler/RubyGems 4 (released 2025-12,
        [upgrade notes](https://blog.rubygems.org/2025/12/03/upgrade-to-rubygems-bundler-4.html)),
        existing lockfiles still don't get checksums automatically — `--add-checksums`
        remains the explicit opt-in. Verify current behavior when Bundler major
        versions change.
      
      ## 3. Dependency vulnerability auditing
      
      - **bundler-audit** checks `Gemfile.lock` against the community
        [ruby-advisory-db](https://github.com/rubysec/ruby-advisory-db):
      
      ```bash
      gem install bundler-audit
      bundle audit check --update    # --update pulls the latest advisory DB
      ```
      
        Gate CI on it; handle a genuinely-unfixable advisory with an explicit
        `--ignore CVE-...` entry plus a tracking issue, never by dropping the gate.
      - Complementary scanners (neutral examples): OSV-Scanner, Trivy, or GitHub
        Dependabot alerts — any is fine; at least one must be on and acted upon.
      - Beyond CVEs: before adopting a gem, check maintenance (last release,
        open CVE history, bus factor) — a transitively-pulled unmaintained gem is
        a finding at MEDIUM.
      
      ## 4. Lint and style: RuboCop or StandardRB
      
      - Pick **one**:
        - **RuboCop** — configurable; pair with plugins matching the stack
          (`rubocop-performance`, `rubocop-rails`, `rubocop-rspec`,
          `rubocop-minitest` as applicable). Set `TargetRubyVersion` to the real
          floor. New projects: start from `AllCops: NewCops: enable` and prune,
          rather than a 400-line inherited config.
        - **StandardRB** — zero-config RuboCop distribution (`standardrb --fix`);
          the right call when style debate costs more than the defaults.
      - CI runs it in check mode (`rubocop --parallel` / `standardrb`); local
        pre-commit runs autofix. A `.rubocop_todo.yml` is a paydown backlog, not a
        permanent mute — flag todo files older than ~6 months.
      - **Never silence Security/* cops** to get green; each `rubocop:disable
        Security/...` needs a justification comment.
      
      ## 5. Static security analysis
      
      - **Brakeman** for Rails apps (neutral example; it is Rails-specific):
        `brakeman -q --no-pager --exit-on-warn` in CI. Manage false positives via
        `brakeman.ignore` with a note per entry — an ignore file nobody can explain
        is a finding.
      - Non-Rails codebases: RuboCop's `Security/*` cops plus the greps in
        `rules/02`/`rules/03`; Opengrep with a Ruby ruleset is a good neutral
        supplement.
      - Secret scanning (gitleaks/trufflehog as neutral examples) runs on every
        push, on the full history at least once.
      
      ## 6. Tests: RSpec / Minitest mechanics
      
      Suite *strategy* — shape, TDD, doubles, test data, flake policy — lives in
      `sota-testing`; load it for any build that writes logic. Ruby runner
      mechanics only:
      
      - Match the project's existing runner (RSpec or Minitest); don't mix.
      - **Run randomized**: RSpec `config.order = :random` (+ `Kernel.srand
        config.seed`), Minitest randomizes by default — a suite that only passes
        in-order has hidden coupling. Record the seed in CI output for replays.
      - Parallelize (`parallel_tests`, Rails' built-in parallel testing, or
        `flatware` as neutral examples) once the suite exceeds a few minutes;
        DB-per-process is the usual prerequisite.
      - Coverage via SimpleCov with a ratchet (fail if coverage drops), not a
        vanity threshold.
      - Time-dependent code: freeze time (`ActiveSupport::Testing::TimeHelpers`
        or the timecop gem as neutral examples); no `sleep`-based assertions.
      - HTTP in tests: stub at the boundary (WebMock/VCR as neutral examples) and
        **disable real network** (`WebMock.disable_net_connect!`).
      
      ## 7. CI gates (the minimum green wall)
      
      Every PR runs, in rough cost order:
      
      1. `bundle install` **frozen** against the committed lockfile (checksums on);
      2. lint: `rubocop --parallel` or `standardrb`;
      3. security: `bundle audit check --update`, plus `brakeman` on Rails;
      4. tests with randomized order and the coverage ratchet;
      5. (typed projects) `srb tc` or `steep check`.
      
      The Ruby version in CI comes from `.ruby-version` (setup-ruby-style actions
      read it) — never a hardcoded duplicate that can drift.
      
      ## 8. Authoring and publishing gems
      
      - `gemspec` metadata complete (`homepage`, `source_code_uri`,
        `changelog_uri`); `required_ruby_version` reflects the tested floor.
      - **Semantic versioning honestly** — breaking changes bump major; deprecate
        with warnings one minor ahead.
      - Don't vendor secrets or `.gem` credentials in the repo; publishing uses
        **RubyGems trusted publishing (OIDC)** from CI or an MFA-protected account
        — [rubygems.org supports both](https://guides.rubygems.org/trusted-publishing/);
        enable MFA on the account regardless.
      - Keep the file list tight (`spec.files` via `git ls-files` minus tests/CI);
        users install what you ship.
      
      ## Audit checklist
      
      Run from repo root; verify each hit manually.
      
      ```bash
      # Lockfile discipline
      ls Gemfile.lock 2>/dev/null | grep -q . || echo "NO LOCKFILE (app = MEDIUM)"
      grep -c "CHECKSUMS" Gemfile.lock || echo "no checksums section (LOW, easy win)"   # stderr kept: no file != no match
      grep -rn "BUNDLE_FROZEN\|--frozen\|frozen.*true" .github/ .gitlab-ci.yml Gemfile 2>/dev/null | head -3
      
      # Mutable git sources — MEDIUM
      grep -nE "git:|github:" Gemfile | grep -v "ref:"
      
      # Multiple top-level sources (dependency confusion) — HIGH
      grep -c "^source " Gemfile   # >1 without scoped blocks = investigate
      
      # Vulnerability gates present?
      grep -rn "bundler-audit\|bundle audit" .github/ Gemfile* Rakefile 2>/dev/null | head -2
      grep -rn "brakeman" .github/ Gemfile* 2>/dev/null | head -2   # Rails apps only
      
      # Advisory scan (live)
      bundle audit check --update 2>/dev/null | tail -5
      
      # Lint posture
      ls .rubocop.yml .standard.yml 2>/dev/null
      grep -rn "rubocop:disable Security" --include='*.rb' .
      find . -name .rubocop_todo.yml -newermt "6 months ago" 2>/dev/null | head -1
      
      # Brakeman ignores without justification (manual review)
      python3 -c "import json;d=json.load(open('config/brakeman.ignore'));print(len(d.get('ignored_warnings',[])))" 2>/dev/null
      
      # CI Ruby version drift
      grep -rn "ruby-version\|ruby:" .github/workflows/ 2>/dev/null | grep -v ".ruby-version" | head
      
      # Test determinism
      grep -rn "order = :random\|--seed" .rspec spec/spec_helper.rb 2>/dev/null | head -2
      grep -rn "disable_net_connect" spec/ test/ 2>/dev/null | head -1
      ```
      
      Severity guide: no lockfile / unfrozen production installs MEDIUM (HIGH if
      deploys resolve fresh); unpinned git gems, missing vulnerability gate MEDIUM;
      multiple unscoped sources HIGH; missing checksums, in-order-only tests LOW.
      
    • 05-concurrency-performance.md 11.4 KB
      # 05 — Concurrency & performance: GVL, jobs, JIT, GC, N+1
      
      Ruby's concurrency story is shaped by the GVL; its performance story by the
      GC, the allocator, and (increasingly) the JITs. Rules of engagement: know
      which resource you're bound on, design jobs for at-least-once delivery, and
      profile before optimizing.
      
      ## 1. The GVL and the concurrency decision table
      
      CRuby's Global VM Lock lets **one thread execute Ruby code at a time per
      process**; the GVL is released during blocking I/O and by many C extensions.
      Consequences:
      
      | Workload | Right tool |
      |---|---|
      | I/O-bound, moderate concurrency (HTTP calls, DB waits) | Threads (or a threaded server/job runner) |
      | I/O-bound, very high concurrency | Fiber scheduler / event-driven (async gem as a neutral example) |
      | CPU-bound | **Multiple processes** (forking server/job workers); Ractors only experimentally |
      | Mixed web serving | Processes × threads (e.g. a forking+threaded server), sized per §6 |
      
      - Threads still buy real parallelism for I/O — a GVL-bound app is *not* a
        reason to avoid threads for network-heavy work.
      - CPU-heavy request paths don't get faster with more threads — they get
        slower (GVL contention + context switching). Move the work to more
        processes or out of band.
      
      ## 2. Thread correctness
      
      - **Any mutable state reachable from two threads needs a lock** (`Mutex`) or
        a concurrency-safe structure (`Queue`/`SizedQueue`; the
        concurrent-ruby gem's `Concurrent::Map`/atomics as neutral examples).
        Core `Hash`/`Array` are not thread-safe for concurrent mutation; "it's
        fine because of the GVL" is not a guarantee — context switches happen
        between bytecodes.
      - Lazy memoization (`@x ||= build`) is a benign-looking race under threads:
        compute may run twice and, worse, a *partially built* object may be
        visible. Initialize eagerly at boot, or guard with a `Mutex`.
      - **`Timeout.timeout` is dangerous around anything with state** — it kills
        the block via an async exception raised at an arbitrary bytecode
        (mid-transaction, mid-cleanup). Prefer native timeouts: DB statement
        timeouts, HTTP client `open_timeout`/`read_timeout`, `IO.select`. Audit
        every `Timeout.timeout` wrapping DB/file/network state as MEDIUM+.
      - Pools for shared clients: DB/Redis/HTTP connections come from a pool
        (`connection_pool` gem as a neutral example) sized ≥ thread count — a
        shared bare client across threads corrupts protocol state.
      - **`Thread#[]`/`Thread#[]=` are fiber-local, not thread-local** — under a
        fiber scheduler or streaming server this "thread-local" silently resets;
        true thread-locals are `Thread#thread_variable_get/set`.
      - Spawned threads: handle exceptions (a dead worker thread fails silently
        unless `abort_on_exception`/`report_on_exception` or a join checks it) and
        join on shutdown.
      
      ## 3. Fibers and Ractors
      
      - **Fiber scheduler** (Ruby 3.0+): with a scheduler installed (async gem as
        the common neutral example), blocking I/O in fibers yields automatically —
        thousands of concurrent I/O waits per thread. Constraints: everything on
        the loop must actually be non-blocking (a C extension that holds the GVL
        blocks the whole loop); fiber-per-request servers (e.g. Falcon as a
        neutral example) need fiber-safe, not just thread-safe, libraries.
      - **Ractors are still experimental as of Ruby 4.0** — the
        [4.0 release notes](https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/)
        say the team "aim[s] to remove its experimental status next year". 4.0
        reworked communication: `Ractor::Port` replaces the **removed**
        `Ractor.yield`/`Ractor#take`. Rules: fine for isolated CPU-bound
        experiments (only shareable — deep-frozen — objects cross boundaries);
        don't build production hot paths on Ractors yet; audit any Ractor use on
        pre-4.0 code for the removed APIs before an upgrade.
      
      ## 4. Background jobs: at-least-once means idempotent
      
      Queue backends (Sidekiq, SolidQueue, GoodJob, Resque as neutral examples)
      deliver **at least once**: crashes and retries re-run jobs. Design contract:
      
      - **Jobs are idempotent.** Techniques: natural idempotency (set-to-state,
        not increment), a uniqueness key checked/recorded in the DB
        (`INSERT ... ON CONFLICT DO NOTHING` on a dedup table), or state-machine
        guards (`return if order.shipped?`).
      - **Arguments are IDs and primitives, never objects.** Serialized objects go
        stale, bloat the queue, and break on deploys that change the class. The
        job refetches; a missing record is usually a *discard*, not a retry.
      - **Enqueue after commit.** Enqueuing inside a DB transaction races the
        worker against the commit (job runs, record not visible) and enqueues for
        rolled-back work. Use the framework's after-commit enqueueing or a
        transactional-outbox pattern; a DB-backed queue in the *same* database
        (SolidQueue/GoodJob style) makes enqueue naturally transactional.
      - Retries: bounded with exponential backoff (the runner's default is fine);
        a dead-letter/discard queue that someone actually monitors; alert on queue
        depth and oldest-job age, not just failures.
      - Timeouts: jobs get an execution budget enforced by the runner or by
        native timeouts inside the job — not `Timeout.timeout` (§2).
      - Don't do slow work in the request cycle: anything > ~100ms and not needed
        for the response body belongs in a job.
      
      ## 5. YJIT and ZJIT
      
      - **YJIT is the production JIT.** Mature since the 3.2/3.3 era; enable with
        `--yjit` / `RUBY_YJIT_ENABLE=1` / `RubyVM::YJIT.enable` (4.0 adds
        `mem_size:` and `call_threshold:` options to `enable` — see the
        [4.0 release notes](https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/)).
        It speeds up CPU-bound *Ruby* execution (typical real-app gains are
        double-digit percent); it does not help I/O waits or C-extension time.
        Verify it's actually on in production (`RubyVM::YJIT.enabled?`) and give
        it headroom — JIT code costs extra memory per process.
      - **ZJIT (new in 4.0) is experimental**: per the release notes it is "faster
        than the interpreter, but not yet as fast as YJIT" and the guidance is to
        "hold off on deploying it in production for now". Benchmark it in staging
        if curious; ship YJIT.
      - Measure with the app's own workload (`benchmark-ips` for micro, production
        latency percentiles for real) — never adopt or tune a JIT on faith.
      
      ## 6. Memory, GC, allocator
      
      - **Measure first**: `GC.stat` (heap pages, `major_gc_count`,
        `old_objects`), RSS per process over time, and an allocation profile
        (memory_profiler gem) before touching knobs.
      - Most "Ruby memory leaks" are **glibc-malloc retention/fragmentation** in
        long-lived multithreaded processes, not Ruby-object leaks. First,
        cheap mitigation: **`MALLOC_ARENA_MAX=2`** (a platform default on some
        PaaSes — [Heroku changelog](https://devcenter.heroku.com/changelog-items/1683)).
      - **jemalloc caveat (status changed):** the classic "just use jemalloc"
        advice needs re-checking — the upstream
        [jemalloc repo](https://github.com/jemalloc/jemalloc) was archived in
        June 2025 and its future maintenance path is unclear as of 2026-07 (needs
        verification at adoption time). Existing jemalloc deployments keep
        working; for *new* setups, start with `MALLOC_ARENA_MAX=2` and adopt an
        alternative allocator only with your own RSS benchmarks and a maintained
        package source.
      - GC tuning (`RUBY_GC_HEAP_*`) is a last resort with before/after
        measurements committed next to the config; out-of-band GC between requests
        and periodic worker recycling (e.g. a worker-killer middleware as a
        neutral example) are legitimate operational tools for slow RSS growth.
      - Sizing threaded/forking servers and workers: threads per process stay
        small for CPU-heavy apps (GVL, §1); total memory = workers × (base +
        JIT + heap growth) — leave allocator headroom before the container limit.
      - Avoid allocation churn on hot paths: frozen literals (`rules/01` §2),
        `String#<<` over `+=`, precomputed constants over per-call construction.
      
      ## 7. N+1 queries and data-access performance
      
      - **Detection beats vigilance**: run a detector in development/CI —
        bullet or prosopite (neutral examples) — and fail tests on new N+1s
        (`Prosopite.raise = true` style) rather than reviewing by eye.
      - Fix with the ORM's eager loading: ActiveRecord
        `includes`/`preload`/`eager_load` (Sequel: `eager`); verify the fix by
        counting queries in a test, not by reading code.
      - ActiveRecord `strict_loading` (6.1+) makes lazy loading raise — enable per
        model/association for hot paths so N+1s can't creep back.
      - Select what you use on wide tables (`select(:id, :email)`/`pluck`);
        `find_each`/`in_batches` for large scans, never `Model.all.each`.
      - Cache derived values with explicit invalidation rules; "cache it" without
        an invalidation story is a future correctness bug (see `sota-databases`
        for the deeper rules).
      
      ## 8. Profiling workflow
      
      1. Reproduce with a realistic workload (production-like data volume).
      2. Profile CPU with a sampling profiler — stackprof or vernier (neutral
         examples; vernier understands GVL/GC pauses) — or rack-mini-profiler
         per-request in development.
      3. Allocation hotspots: memory_profiler / `GC.stat` deltas around the
         suspect region.
      4. Fix the top item, re-measure, repeat. No optimization PR without
         before/after numbers in the description.
      
      ## Audit checklist
      
      Run from repo root; verify each hit manually.
      
      ```bash
      # Timeout.timeout around stateful work — MEDIUM+ (HIGH around transactions)
      grep -rn "Timeout.timeout\|Timeout::timeout" --include='*.rb' .
      
      # Unsynchronized shared mutable state (class-level accumulators) — manual review
      grep -rnE "@@\w+|class << self" --include='*.rb' app/ lib/ 2>/dev/null | head
      grep -rnE "\|\|=" --include='*.rb' . | grep -viE "spec|test" | head   # memoization under threads?
      
      # Fiber-local mistaken for thread-local
      grep -rnE "Thread\.current\[" --include='*.rb' . | head
      
      # Threads without exception handling / join (manual review)
      grep -rn "Thread.new" --include='*.rb' . | grep -v join | head
      
      # Ractor use — verify experimental caveats & removed APIs (Ractor.yield/#take gone in 4.0)
      grep -rnE "Ractor\.(new|yield)|\.take\b" --include='*.rb' . | head
      
      # Jobs: object args (GlobalID mitigates for AR models; raw objects = MEDIUM)
      grep -rnE "perform_(async|later)\(" --include='*.rb' . | grep -vE "\(\s*[a-z_]*id|\(\s*\d|\(\s*\)" | head
      # Enqueue inside transactions — race, MEDIUM+
      grep -rn -B3 "perform_later\|perform_async" --include='*.rb' app/ lib/ 2>/dev/null | grep "transaction do" | head
      # Idempotency signals absent (manual: look for guards/upserts in job bodies)
      grep -rln "def perform" app/jobs/ 2>/dev/null | head
      
      # JIT posture — INFO
      grep -rn "yjit\|YJIT" Dockerfile* config/ Procfile* .github/ 2>/dev/null | head -3
      grep -rn "zjit" Dockerfile* config/ 2>/dev/null | head -1   # experimental in prod = MEDIUM
      
      # Allocator / memory posture — INFO
      grep -rn "MALLOC_ARENA_MAX\|jemalloc" Dockerfile* config/ Procfile* 2>/dev/null | head -3
      
      # N+1 guards present? absent detector = note it
      grep -rn "bullet\|prosopite" Gemfile 2>/dev/null | head -2
      grep -rn "strict_loading" --include='*.rb' app/ config/ 2>/dev/null | head -2
      grep -rnE "\.all\.each\b" --include='*.rb' . | head
      
      # Unbounded scans
      grep -rnE "\.(map|each)\b" --include='*.rb' app/ 2>/dev/null | grep -vE "find_each|in_batches" | grep -E "\.(all|where\([^)]*\))\." | head
      ```
      
      Severity guide: non-idempotent retried job with side effects (payments,
      emails) HIGH; enqueue-in-transaction, `Timeout.timeout` around transactions,
      shared client without a pool MEDIUM–HIGH; Ractors or ZJIT on a production hot
      path MEDIUM; YJIT off, no N+1 detector INFO/LOW.
      
  • SKILL.md 7.9 KB
    ---
    name: sota-ruby
    description: >-
      State-of-the-art Ruby engineering rules (2026 baseline, Ruby 3.4+ / 4.0) that Claude
      applies when writing or auditing Ruby. Covers modern idioms (frozen string literals,
      pattern matching, Data/Struct, RBS/Sorbet/Steep typing), security (SQL injection via
      ActiveRecord/Sequel, ERB/XSS escaping, mass assignment, CSRF, Marshal/YAML
      deserialization, command injection, ReDoS), framework-neutral web hardening
      (Rails/Sinatra/Hanami as neutral examples), supply chain and tooling (Bundler lockfile
      and checksums, bundler-audit, RuboCop/StandardRB, Brakeman, RSpec/Minitest, CI gates),
      and concurrency/performance (GVL, threads vs fibers vs Ractors, background-job
      idempotency, YJIT/ZJIT, GC and memory, N+1 queries). Trigger keywords: Ruby, gem,
      Gemfile, bundler, Rails, Sinatra, Hanami, Rack, ERB, ActiveRecord, Sequel, RSpec,
      minitest, RuboCop, Sorbet, RBS, Sidekiq, YJIT, Ractor, rake, ruby-lang. Use for BOTH
      building Ruby services/gems/CLIs and reviewing or auditing Ruby codebases.
    ---
    
    # SOTA Ruby (2026)
    
    Expert-level rules for producing and auditing production Ruby. Baseline language
    line: **Ruby 3.4+**, with Ruby 4.0 (released 2025-12-25) as the latest major
    line. Per the [official branches page](https://www.ruby-lang.org/en/downloads/branches/):
    4.0 and 3.4 are in normal maintenance; 3.3 is security-maintenance only
    (expected EOL 2027-03); 3.2 and older are EOL (3.2 since 2026-04-01) — running
    them is itself a finding. Feature notes: `Data.define` and `Regexp.timeout`
    from 3.2, `it` block parameter and chilled-string warnings from 3.4,
    `Ractor::Port` and experimental ZJIT from 4.0 — noted where relevant. Every
    rules file ends with an audit checklist of grep/lint patterns.
    
    ## Purpose
    
    Two consumers, one source of truth:
    
    - **BUILD mode** — generating new Ruby code: follow the rules as defaults, not
      suggestions. Deviate only with an explicit comment justifying it.
    - **AUDIT mode** — reviewing existing Ruby code: hunt violations using the
      audit checklists, classify by severity, report in the finding format below.
    
    ## BUILD mode
    
    1. Before writing code, read the rules files relevant to the task (see index).
       A web endpoint touching the DB and a background job needs `02`, `03`, `05`.
    2. Apply the **top-10 non-negotiables** (below) unconditionally.
    3. Establish context first: `.ruby-version`, `Gemfile`/`Gemfile.lock`, RuboCop
       or StandardRB config, framework and test runner in use. Match the project's
       floor (no `it` block param on a 3.3 project).
    4. New projects: pin the Ruby version (`.ruby-version`), commit
       `Gemfile.lock`, add RuboCop **or** StandardRB, `bundler-audit`, and the
       test suite to CI from day one (see `rules/04`). Rails apps add Brakeman.
    5. Security posture is non-optional even when unrequested: parameterized SQL,
       argv-form process spawning, `YAML.safe_load` semantics, `SecureRandom`,
       escaped output (see `rules/02`, `rules/03`).
    6. Write tests alongside the code (RSpec or Minitest — match the project).
       Anything with threads or jobs gets an idempotency/concurrency test.
    7. When code must violate a rule for a legitimate reason, leave a
       `# NOTE(sota):` comment explaining the trade-off so auditors don't flag it.
    
    ## AUDIT mode
    
    Work through each relevant rules file's audit checklist against the target
    repo. Run the listed grep/lint commands; confirm each hit manually before
    reporting (greps are recall-oriented, expect false positives). Useful
    mechanical sweeps: `bundle exec rubocop`, `bundler-audit check --update`,
    `brakeman -q` (Rails), plus the per-file greps.
    
    ### Severity conventions
    
    | Severity | Meaning | Examples |
    |---|---|---|
    | **CRITICAL** | Exploitable now, or data loss | SQL built with `#{}` interpolation, `Marshal.load`/`YAML.unsafe_load` on external data, command injection via backticks with user input, `html_safe` on user input |
    | **HIGH** | Exploitable with preconditions, or production-breaking | Missing CSRF protection on state-changing routes, `permit!`, `^`/`$` anchors in validation regexes, `rand` for tokens, `Timeout.timeout` around DB work, EOL Ruby in production |
    | **MEDIUM** | Correctness/maintenance hazard, latent bug | N+1 on a hot path, non-idempotent retried jobs, no `Gemfile.lock` in an app, mutable shared state across threads without a lock, `rescue Exception` |
    | **LOW** | Deviation from SOTA, friction | Missing frozen-string-literal comments, `Struct` where `Data` fits, stringly-typed booleans, unpinned dev tooling |
    | **INFO** | Worth knowing, no action forced | YJIT not enabled, typing (RBS/Sorbet) absent, newer-Ruby features available after a floor bump |
    
    ### Finding format
    
    ```
    file:line | rule violated (rules/NN §section) | severity | effort | fix
    ```
    
    Severity: Critical / High / Medium / Low / Info. Effort: trivial / small /
    medium / large. Borderline severities state the deciding assumption;
    unconfirmed findings are marked "needs verification", never asserted. Group
    findings by severity, CRITICAL first; end with counts per severity, the three
    highest-leverage fixes, and which checklists were run.
    
    ## Rules index
    
    | File | Read this when... |
    |---|---|
    | `rules/01-language-idioms.md` | Choosing/verifying the Ruby version baseline; frozen string literals; pattern matching; `Data` vs `Struct`; exception design; **`nil` over a sentinel, and `to_i` silently returning `0` for garbage**; typing with RBS/Sorbet/Steep; general idioms and pitfalls |
    | `rules/02-security.md` | Any input crossing a trust boundary: SQL injection (ActiveRecord/Sequel), command injection (`system`/backticks/`Open3`), deserialization (`Marshal`, YAML/Psych), ReDoS and regex anchors, `eval`/`send`/`constantize`, secrets and randomness, path traversal |
    | `rules/03-web-hardening.md` | Building or auditing anything web-facing: XSS/ERB escaping, mass assignment and strong params, CSRF, sessions/cookies, security headers, open redirects, SSRF, file uploads — framework-neutral |
    | `rules/04-supply-chain-tooling.md` | Bundler and Gemfile.lock discipline, lockfile checksums, bundler-audit, RuboCop/StandardRB, Brakeman, RSpec/Minitest mechanics, CI gates, gem authoring/publishing |
    | `rules/05-concurrency-performance.md` | Threads, fibers, Ractors, and the GVL; background-job idempotency; YJIT/ZJIT; GC and memory (allocator, RSS); N+1 detection; profiling workflow |
    
    ## Top-10 non-negotiables
    
    1. **Supported Ruby only** — 3.4+ in production (3.3 accepted short-term with
       an upgrade plan; ≤3.2 is a finding). Pin it in `.ruby-version` and CI.
       (`rules/01`)
    2. **SQL only via parameterized queries** — hash conditions or `?`/named
       placeholders in ActiveRecord/Sequel; string-interpolated SQL is CRITICAL,
       no exceptions for "internal" values. (`rules/02`)
    3. **Never `Marshal.load`, `YAML.unsafe_load`, or `eval`-family on data you
       don't fully control**; `YAML.load` is safe-by-default only on Psych 4+
       (Ruby 3.1+) — verify the runtime. (`rules/02`)
    4. **Processes spawn with argv lists** (`system("cmd", arg)`,
       `Open3.capture2`), never a shell string containing external input; no
       `Kernel#open`/`URI.open` on user-supplied names. (`rules/02`)
    5. **All HTML output escaped by default**; every `raw`/`html_safe` is
       reviewed; non-Rails ERB configured to auto-escape. (`rules/03`)
    6. **Mass assignment goes through an attribute allowlist** (strong params /
       `params.expect`; explicit attribute lists elsewhere); `permit!` is HIGH.
       (`rules/03`)
    7. **CSRF protection on every cookie-authenticated state-changing endpoint**;
       `\A`/`\z` (never `^`/`$`) to anchor validation regexes. (`rules/02`, `rules/03`)
    8. **`Gemfile.lock` committed and CI installs frozen** (`BUNDLE_FROZEN=true`);
       `bundler-audit` gates CI; git-sourced gems pinned to a SHA. (`rules/04`)
    9. **`SecureRandom` (never `rand`/`Random`) for anything security-relevant**;
       constant-time comparison for secrets. (`rules/02`)
    10. **Background jobs are idempotent and enqueue after commit** — at-least-once
        delivery and retries are the contract; jobs take IDs, not objects.
        (`rules/05`)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related