“This controller has a security problem” is not an actionable finding. “This file is slow” is not a root cause. Most tooling — and most ad hoc debugging — stops at the file. Scryer is built around a narrower, more useful claim: not just which file, but which method, which line, which call site, and — once you’ve applied a fix — proof that the specific thing causing the problem is actually gone.
Static findings point at the method, not the file
Scryer parses every file once with Ruby’s stdlib Ripper, walks the resulting syntax tree, and runs each check (a Scryer::Rule subclass) against the shapes it finds. Because it’s walking a real AST rather than grepping for a pattern, a rule knows exactly which method body a flagged call lives inside — not just that the string params appears somewhere in a 400-line controller:
def create @invoice = Invoice.create(params[:invoice])end[CRITICAL] mass_assignment — app/controllers/invoices_controller.rb:3`create` receives `params` (or a subscript of it) directly, with no `.permit(...)` call.fix: Wrap the params in a strong-parameters method, e.g. `create(order_params)` with`def order_params; params.require(:order).permit(:status, :total); end`Line 3 isn’t “somewhere near the top of the file” — it’s the exact line inside the exact method (create) where the unsafe call happens. Every one of the 32 security rules carries the same shape: a CWE ID, an OWASP Top 10 category, a severity, and a confidence level, because a rule that’s only right 60% of the time needs to say so rather than presenting every hit with the same certainty as one that’s right 99% of the time.
Note
This is deliberately not data-flow/taint analysis. Scryer doesn’t trace whether the value
reaching Invoice.create actually originated from user input the way Brakeman does — it matches
the shape of the call itself. That’s a real precision tradeoff in exchange for a full scan in
about 2.5 seconds on a 236-file app, with zero runtime dependencies and no Rails boot required.
Ranking decides which method’s root cause to chase first
Pointing at a line is only useful if you know which of the forty lines Scryer found is actually worth looking at first. The four category scores (Security, Performance, Style, Dependency) are computed independently via exponential decay from 100, weighted by severity and confidence — one critical finding alone can move a clean score from 100 down to roughly 86. Those four scores then feed a single cross-category “Top priorities” list:
Scryer Audit — 236 files scanned────────────────────────────────Security Score: 11/100 (F)Performance Score: 62/100 (D)Style Score: 100/100 (A)Dependency Score: 88/100 (B)
Top priorities: 1. [critical] security — mass_assignment (app/helpers/api/v1/create_order_helper.rb:14) ...That ranking is the actual root-cause-triage step. Brakeman, RuboCop, and bundler-audit each rank findings within their own category, if at all — none of them tells you that the one critical security finding in a performance-heavy file is worth more attention right now than the eleven style violations in the same file.
Runtime root cause: catching the exact call site, not “a query happened”
Static analysis can tell you a method could cause a problem. It can’t tell you that a specific association load, triggered from a specific action, actually fired an N+1 query during a real request — that requires watching the app run. Scryer’s two opt-in runtime watchers exist for exactly that gap, both built on the same two primitives: ActiveSupport::Notifications for “this happened,” and Module#prepend for wrapping a method without touching its source.
if Rails.env.development? || Rails.env.test? require "scryer/query_watcher" Scryer::QueryWatcher.enable! Rails.application.config.middleware.use Scryer::QueryWatcher::MiddlewareendQuery Watcher subscribes to sql.active_record notifications and prepends association loaders so it sees an N+1 as it actually happens — tied to the specific association load and the specific request that triggered it, not a generic “this model might N+1 somewhere” static guess. That’s a different detection mechanism from Bullet, not a replacement for it, and it’s precise about the method that caused it because it’s watching that method execute.
Authorization Watcher flags write requests (POST/PUT/PATCH/DELETE) that complete successfully without Pundit’s authorize/policy_scope or CanCanCan’s authorize! ever being invoked during that request. The root cause it surfaces is specific: not “this app might have an authorization gap,” but “OrdersController#destroy completed this request without an authorization check firing” — the exact controller action, the exact missing call, on a request that actually ran.
Warning
Both watchers are opt-in and gated to development/test by convention in the generated initializer. They’re instrumentation, not a feature to ship running in production without thinking about the overhead first.
Proving the root cause is actually gone, not just quieter
Identifying the method isn’t the end of the loop — scryer fix closes it. A small set of rules (frozen_string_literal, SQL injection via bind-parameter substitution, a handful of Rails security config flips) get fixed mechanically with no AI involved. Everything else goes through whatever LLM client you configure, applied to that specific method:
scryer fix -r ./scryer_config.rbFixed: mass_assignment — app/controllers/orders_controller.rb:8 Switches to strong parameters so only explicitly permitted attributes reach `Order.new`.Re-scanning to verify every applied fix...Verified: all 2 applied fix(es) confirmed clean on a full re-scan.The re-scan step matters more than it looks. A fix that merely silences a finding — renaming a variable, suppressing a warning comment — would still show up again on the next scan, because the rule re-runs against the actual code, not against a diff. “Verified” here means the exact method that caused the finding no longer matches the rule that flagged it, confirmed the same way the original root cause was identified in the first place.
Tracking when a method became the root cause
Baseline mode matters for the same reason: it diffs against a saved snapshot and matches findings by rule + file + the offending source text itself, not file/line. If an unrelated formatting change shifts a vulnerable method three lines down, Scryer still recognizes it as the same finding in the same method — not a new one — which keeps the “when did this method become a problem” trail intact across refactors instead of resetting every time someone touches the file.
Scryer is MIT-licensed and available as a gem (gem install scryer). The pattern underneath all of this — point at the specific method, rank it against everything else that’s wrong, and verify a fix actually cleared it rather than just moved it — is the same whether the root cause is a missing .permit, an N+1 fired by one association load, or a controller action that’s quietly never checking authorization at all.
For the full rule reference and architecture notes, see the detailed Scryer documentation, or read more on what Scryer scans for and why.