Ruby on Rails rules for AI coding agents

Rails rewards following conventions, and agents drift from them in predictable ways: custom actions piled onto one controller, validates :email, uniqueness: true with no unique index behind it, a job enqueued inside a transaction so the worker cannot see the row yet. The Rails rules start by reading .ruby-version and the rails line in Gemfile.lock, then follow the default layout reviewers expect.

They push for new resource controllers instead of extra actions, database constraints behind important validations, includes with strict_loading to catch lazy loads in development, reversible migrations that do not reference model classes, and jobs enqueued after commit. In views, html_safe and raw stay away from user content, and the checks include bin/brakeman alongside the tests and RuboCop.

Folder: rules/rails/ · rails.mdc, CLAUDE.md, AGENTS.md

What the Rails rules tell your agent

First rule of each section in rails.mdc, quoted as shipped.

Mistakes these rules are written to prevent

Source: “Pitfalls to avoid”.

Checks the agent runs before it says done

Example commands from the Rails file (swap in your repo's own):

Run bin/rails test (or bundle exec rspec), bin/rubocop and bin/brakeman, and bin/rails db:migrate followed by a rollback check for new migrations. Report each result.

Before and after: Rails: strong parameters and record scoping

Prompt card 30 · Stack: Ruby on Rails · Type: Security review · Best model: a frontier reasoning model

What the card is for

Find mass-assignment holes (permitting role, account_id, price), lookups that are not scoped to the current user or account (insecure direct object references), and unsafe SQL fragments built from params.

Worked example from the card

Before

class InvoicesController < ApplicationController
  def index
    @invoices = Invoice.where(account_id: params[:account_id]).order("#{params[:sort]} DESC")
  end

  def update
    @invoice = Invoice.find(params[:id])
    @invoice.update(params.require(:invoice).permit(:memo, :amount_cents, :status, :account_id))
    redirect_to @invoice
  end
end

Expected AI output (abridged)

Critical - index: account_id comes from params, so any user lists any account's
invoices: GET /invoices?account_id=42.
Critical - index: params[:sort] is interpolated into ORDER BY. SQL injection through
sort=(SELECT ...) and errors that leak schema details.
Critical - update: Invoice.find is unscoped; PATCH /invoices/77 edits another account's
invoice.
High - update permits status, amount_cents and account_id: a user can mark an invoice
paid, change its amount or move it to another account.
Medium - update ignores the return value of update; validation failures redirect as if
saved.

After

class InvoicesController < ApplicationController
  SORTABLE = %w[created_at due_on amount_cents].freeze

  def index
    sort = SORTABLE.include?(params[:sort]) ? params[:sort] : "created_at"
    @invoices = current_account.invoices.order(sort => :desc)
  end

  def update
    @invoice = current_account.invoices.find(params[:id])
    if @invoice.update(invoice_params)
      redirect_to @invoice, notice: "Invoice updated"
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private

  def invoice_params = params.expect(invoice: [:memo])
end

Excerpt from card 30; the prompt itself is in the pack.

More Rails prompt cards in the pack

Using these rules in Cursor, Claude Code and AGENTS.md tools

Cursor

Put _base.mdc and rails.mdc in .cursor/rules/. Globs: Gemfile, app/**/*.rb, app/**/*.erb, config/**/*.rb, db/migrate/*.rb, lib/**/*.rb, test/**/*.rb, spec/**/*.rb.

Claude Code

Root CLAUDE.md, base first. Path-scoped variant .claude/rules/rails.md for app/**/*.{rb,erb}, db/migrate/*.rb, config/**/*.rb; PostToolUse hook: bin/rubocop -a <file>.

AGENTS.md tools (Codex, Gemini CLI, Aider)

Base then stack in one AGENTS.md. Gemini CLI needs context.fileName; Aider needs --read AGENTS.md. Put this file in the Rails root. Agents use the AGENTS.md nearest to the file being edited, so a nested copy overrides a root one for that subtree.

Related stacks

← All stacks