What the agent does
The agent starts from the open pull request's branch and learns how this Rails app already decides who may write. In CodeTriage, Gemfile pins rails at 8.1.3.1 and Ruby comes from .ruby-version, which reads 4.0.5. The test group includes minitest, minitest-mock and rails-controller-testing. config/routes.rb is the map. Sidekiq::Web is mounted at /sidekiq inside authenticate :user with a lambda that requires u.admin?. Registration is skipped on devise_for :users, and the editable account routes are drawn by hand: get users/edit and patch users. Repo pages live under a scope with format: false and a full_name constraint of owner/name, so a new route drawn outside that scope can split a dotted repo name into a format. ApplicationController calls protect_from_forgery, and its authenticate_user! redirects to the GitHub OmniAuth path unless user_signed_in?.
Each new action is compared with the controller it sits beside. ReposController allows create, edit and update only after authenticate_user!, and edit redirects to the root unless current_user.able_to_edit_repo?(@repo). UsersController limits edit, update and destroy with omniauth_workaround_authenticate_user!, and show redirects with "User is private" unless the signed-in user is that user. IssueAssignmentsController#create does not declare a before_action: it loads current_user.repo_subscriptions.find(params[:id]) and enqueues SendSingleTriageEmailJob. A nil current_user raises there, which is a different failure from a redirect, so a new action that touches current_user without the before_action its neighbours use is called out even when a missing session would 500 rather than succeed. click_issue_redirect is the interesting public GET: it loads IssueAssignment.find(params[:id]) and redirects to assignment.issue.html_url with allow_other_host: true only when assignment.user.id matches params[:user_id]. A new redirect that copies allow_other_host and drops the user-id comparison is the finding.
The SQL pass looks for a string built from params. IssueAssigner#issues_for_sub is the local standard: a heredoc SELECT against issues, with the bound hash {sub_id, issue_state, repo_id, email_limit} passed to Issue.find_by_sql. The agent leaves that query alone. A new find_by_sql, where or order that interpolates params, a header or a path segment is the comment, and the sentence says what an attacker sends. Strong parameters are part of the same trace: a new create that passes params straight into a model, while the neighbouring action slices a permit list, is a finding even when the SQL itself is bound.
The secret pass reads the diff for credentials and for anything that would land in the browser. CI in .github/workflows/main.yml copies config/database.ci.yml over config/database.yml, creates triage_test on Postgres 16, and runs bin/rake test. database.ci.yml has adapter postgresql, username postgres and password password, which is the CI fixture, not a new leak. A pull request that commits a real database.yml, a GitHub token, or an OmniAuth secret is the comment. The agent posts one inline comment per finding and posts nothing when the diff is clean. It does not push a fix.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Review the open pull request for security problems only. Trace every new parameter to the database, the filesystem or a redirect. Compare each new action with its neighbours: Sidekiq::Web requires an admin, ReposController uses authenticate_user! and able_to_edit_repo?, and IssueAssignmentsController#click_issue_redirect checks the assignment's user id before allow_other_host. Treat IssueAssigner's find_by_sql, which binds sub_id, repo_id and email_limit, as the safe pattern, and flag a query that interpolates params. Look for secrets in the diff, and do not flag the postgres password in config/database.ci.yml. Post findings as inline review comments with the exploit in one sentence each. Post nothing if there are none. Do not push commits.",
"resources": [
{
"url": "https://github.com/your-org/your-app/pull/42"
}
]
}- Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
- Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
- Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.
What comes back
What comes back is comments on the pull request under review. A finding names the action, the line in the diff, and one sentence of exploit, such as a redirect that keeps allow_other_host while dropping the comparison with assignment.user.id. A clean diff produces no review comment. The session records the repository and that pull request, and the transcript lists the controllers, the routes file and the SQL the agent opened.
Before anyone treats a comment as a required change, they read the neighbouring action the comment cites. A missing before_action that still cannot be called logged out is a different bug from a missing before_action that runs as whoever is signed in. The reviewer also checks that the agent did not ask to rewrite IssueAssigner's bound query, and that it did not tell the author to delete the CI database password.
The default branch is unchanged. The author fixes the line, or replies on the comment. A later push to the same pull request can start another review of the new commits, and the earlier comments remain on the lines they marked.
Run it on every pull request
A Project automation triggered on pull_request.opened and pull_request.synchronize runs this review once per push. Fork pull requests do not fire it, and neither does work from someone who is not an owner, member or collaborator. The resources URL is the pull request, so the sandbox is on that branch.
The same start is one POST /o/{org}/sessions from the service that receives your GitHub webhook, with the pull request URL in resources. Point the API key at a webhook URL and it is called with run.completed, run.failed or run.cancelled when the turn ends. Past max_concurrent in-progress runs, the default is 4, the next create answers 429.
The use case this job follows, with its automation and trigger: Review every pull request.
Why run it on Rush?
The unsafe change in an app like this is small: a new member route that forgets authenticate_user!, or a redirect that keeps allow_other_host. Doing that reading by hand on every synchronize event does not keep up with the branch. The sandbox has the pull request branch and nothing else, so the review does not depend on a laptop checkout of the same repo.
What you can show afterwards is the comment text and the transcript of the files that were opened. Ten open pull requests are ten sandboxes, not ten stashes in one working copy. The agent stops at the comment. Merging stays with the people on the pull request.
Frequently asked questions
Does it run bin/rake test before it comments?
Only if the prompt says so. This review is the diff against the existing auth and the bound SQL. The workflow in .github/workflows/main.yml is where the suite runs on the pull request itself: it loads config/database.ci.yml, prepares triage_test, and runs bin/rake test. Add that command to the prompt when you want the agent to execute it inside the sandbox too.
Will a comment land on IssueAssigner's raw SQL?
Not on the query that is already there. issues_for_sub passes a bind hash to find_by_sql. The agent comments when a new query concatenates a parameter into the SQL string. The comment says which parameter, and it does not ask you to rewrite the bound form.
How does it treat click_issue_redirect?
As the pattern for a public link that leaves the site. The action compares params[:user_id] with the assignment's user, and only then redirects to issue.html_url with allow_other_host: true. A new action that redirects to a URL from the request, or that drops the user check, is a finding. The existing action is not.
Can it see a private fork?
It sees the branch your GitHub App installation can read. The automation does not start for a fork pull request. A resources URL outside that installation is refused with 422 RESOURCE_NOT_ACCESSIBLE before a sandbox is allocated.