rails-style — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited rails-style (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Order methods in classes: class methods first, then public methods (initialize at the top), then private methods.
class SomeClass
# 1. Class methods first
class << self
def deliver_all_later
DeliverAllJob.perform_later
end
end
# 2. Public methods (initialize first if present)
def initialize(user)
@user = user
end
def deliver
# ...
end
# 3. Private methods
private
def deliverable?
# ...
end
endWithin each visibility section, order methods vertically by invocation: callers before the methods they call.
class SomeClass
def some_method
method_1
method_2
end
private
def method_1
method_1_1
method_1_2
end
def method_1_1
# ...
end
def method_1_2
# ...
end
def method_2
# ...
end
endPrefer expanded conditionals over guard clauses.
# Bad
def todos_for_new_group
ids = params.require(:todolist)[:todo_ids]
return [] unless ids
@bucket.recordings.todos.find(ids.split(","))
end
# Good
def todos_for_new_group
if ids = params.require(:todolist)[:todo_ids]
@bucket.recordings.todos.find(ids.split(","))
else
[]
end
endException: a guard clause is acceptable when the return is right at the beginning of the method and the main body is non-trivial:
def after_recorded_as_commit(recording)
return if recording.parent.was_created?
if recording.was_created?
broadcast_new_column(recording)
else
broadcast_column_change(recording)
end
endNo newline under private; indent content under it. For modules with only private methods: private at the top, extra newline after, no indent.
class SomeClass
def some_method
# ...
end
private
def some_private_method
# ...
end
endmodule SomeModule
private
def some_private_method
# ...
end
endModel web endpoints as CRUD on resources; when an action doesn't map to a CRUD verb, introduce a new resource instead of a custom action:
# Bad
resources :cards do
post :close
post :reopen
end
# Good — create = close, destroy = reopen
resources :cards do
resource :closure
endFull pattern (controllers, more resource examples): [[rails-controllers]] Pattern 3.
Thin controllers directly invoke a rich domain model — plain ActiveRecord (@card.comments.create!(comment_params)) or intention-revealing model APIs (@card.gild). No services or other artifacts connecting the two; a form/service object like Signup.new(email_address:).create_identity is the rare justified exception. See [[rails-controllers]] Pattern 8 and [[rails-philosophy]].
Only use ! for methods that have a corresponding counterpart without !. Don't use ! to flag destructive actions.
record.save! # Good: save! has a counterpart save
@card.gild! # Bad: no gild counterpart exists
@card.gild # CorrectShallow job classes delegating to domain models, with _later for the enqueueing method and _now for the synchronous version when both exist:
module Event::Relaying
extend ActiveSupport::Concern
included do
after_create_commit :relay_later
end
def relay_later
Event::RelayJob.perform_later(self)
end
def relay_now
# actual logic here
end
end
class Event::RelayJob < ApplicationJob
def perform(event)
event.relay_now
end
endFull job patterns (recurring, queues, error handling): [[rails-jobs]].
<% # comment %> looks harmless but trips ERB parsers and template tooling (Herb reports it as a parsing error). Always use the dedicated ERB comment tag <%#:
<%# Good: dedicated ERB comment tag %>
<% # Bad: Ruby comment inside an execution tag — causes parsing issues %>If the project uses Herb (ERB language server / linter):
bin/herb analyze app/views app/components checks all templates for parsing errors — run it after template changes and before committing..herb.yml (accessibility rules, HTML validity, ERB best practices).Everything above this point is taste — and a skill or CLAUDE.md only asks the agent to follow it. A Claude Code hook guarantees the check runs. Deterministic beats hopeful: don't rely on the model remembering the style guide, run RuboCop and make a clean result a condition of finishing. (Technique adapted from thoughtbot's Enforcing Your Ruby Style Guide on AI-Generated Code.)
rails-toolkit ships a gentle version of this for free. When the plugin is installed, aPostToolUsehook (bin/rubocop-autocorrect-hook) runs after each.rb/.rakeedit, safe-autocorrects that one file, and reports any remaining offenses back as context — but only in a project that opted into RuboCop (a.rubocop.yml) with a resolvable runner; it no-ops everywhere else. It never blocks. TheStophook below is the stronger, blocking variant you add per-project when you want the agent's completion gated on a clean lint.
Drop this into your own Rails project (not into a plugin — a project-local hook only fires for that repo). A Stop hook runs RuboCop over the Ruby files in the diff once the agent thinks it is done, autocorrects what it can, and — if offenses remain — exits non-zero so Claude Code feeds the message back and the agent gets exactly one corrective pass before it is allowed to stop.
.claude/settings.json:
{
"hooks": {
"Stop": [
{ "hooks": [ { "type": "command", "command": ".claude/hooks/rubocop.sh" } ] }
]
}
}.claude/hooks/rubocop.sh:
#!/usr/bin/env sh
# Stop hook: lint the Ruby files the agent touched, autocorrect, then re-check.
# Exit 2 + stderr -> Claude Code blocks the stop and feeds the message back,
# giving the agent one chance to fix what --autocorrect could not.
set -e
files=$(git diff --name-only --diff-filter=d HEAD -- '*.rb' '*.rake' | tr '\n' ' ')
[ -z "$files" ] && exit 0
bundle exec rubocop --autocorrect $files >/dev/null 2>&1 || true
if ! out=$(bundle exec rubocop --format simple $files 2>&1); then
printf 'RuboCop offenses remain — fix them (do not disable cops):\n%s\n' "$out" >&2
exit 2
fiA lighter alternative is a PostToolUse hook on Edit|Write that just runs bundle exec rubocop -a "$file" per edit — cheaper, but it only autocorrects and never blocks on what it can't fix. The Stop version above is what catches the offenses that need the agent's judgment.
Use `rubocop-rails-omakase`: it is the Rails 8 default and the same 37signals house style the rest of this toolkit follows. Do not hand-author a sprawling custom `.rubocop.yml` — omakase's own README calls that bikeshedding, and a big bespoke cop set cuts against [[rails-philosophy]] §1 ("Vanilla Rails Is Plenty"). Two things to know about what omakase does and doesn't do:
case/end alignment, double-quoted strings, trailing whitespace. It cannot express the taste rules in this skill (method ordering, caller-before-callee, expanded conditionals, CRUD-as-resources, bang / _later / _now conventions, thin controllers). So the hook and this skill are complementary, not redundant: RuboCop enforces the mechanical layer; the agent follows the taste layer because this skill is loaded.Rails/OutputSafety is off by default. To get the html_safe/raw XSS guard referenced below, re-enable a small targeted subset on top of omakase — not a full rule set:# .rubocop.yml
inherit_gem:
rubocop-rails-omakase: rubocop.yml
Rails/OutputSafety: # html_safe / raw are XSS vectors — keep this on
Enabled: true
Lint/Debugger: # no stray binding.irb / debugger in committed code
Enabled: true.claude/rules/rubocop.md)A hook stops the agent from silently shipping offenses; a rules file stops it from cheating past them. Add .claude/rules/rubocop.md telling the agent:
# rubocop:disable or # rubocop:todo to make a violation go away.html_safe and raw are XSS vectors; if a specific use is truly safe, surface it for the user to approve rather than disabling the cop. See [[rails-security]] for CSP/XSS context.When writing or reviewing code, verify:
private, content indented under it_later/_now naming convention with shallow jobs<%#, never <% ## rubocop:disable-d (and Rails/OutputSafety is never silenced)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.