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
- Versions and conventionsRead
.ruby-versionand therailsline in Gemfile.lock. Follow Rails conventions (file names, RESTful routes,resources) before inventing structure; reviewers expect the default layout. - ControllersKeep controllers to the seven REST actions where possible. Add a new resource controller (
Orders::CancellationsController#create) rather than custom actions on an existing one. - ModelsBack every important validation with a database constraint.
validates :email, uniqueness: trueraces; add a unique index too. - QueriesAvoid N+1 with
includes(orpreload/eager_loadwhen you need control). Turn onstrict_loadingin development, or per association, to catch lazy loads. - MigrationsWrite reversible migrations (
changewith reversible methods, orup/down). Do not reference application model classes inside migrations; they change over time.
First rule of each section in rails.mdc, quoted as shipped.
Mistakes these rules are written to prevent
rescue => earound a whole action, which hides bugs and returns success.default_scope, which leaks into every query, association andunscopedworkaround.- Fat concerns that are just a model split into files. Extract a plain Ruby object with a clear name instead.
Source: “Pitfalls to avoid”.
Checks the agent runs before it says done
Example commands from the Rails file (swap in your repo's own):
- Tests:
bin/rails test(orbundle exec rspec) - Lint:
bin/rubocop - Security:
bin/brakeman
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
- Card 31: Rails: move side effects out of model callbacks
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.