What the agent does
The agent checks out the pull request's branch in its own sandbox and learns the app's authorization pattern before it reads the new code. In a Django app like healthchecks/healthchecks, requirements.txt pins Django[argon2]==6.1.2, and hc/front/views.py defines the access helpers every dashboard view uses: _get_check_for_user, _get_channel_for_user and _get_project_for_user return the object for a superuser or the project owner, otherwise look up a Member row with get_object_or_404, so a stranger gets a 404, and the _get_rw_* variants raise PermissionDenied for read-only members. Views stack @require_POST and @login_required above them, as update_name does.
The JSON API has its own pattern in hc/api/decorators.py. @authorize takes a 32-character key from the X-Api-Key header or the api_key field of a POST body and accepts only read-write keys; @authorize_read also accepts read-only keys and sets request.readonly when the key starts with hcr_ or matches the project's read-only key. Every API view then scopes its query with Check.objects.filter(project=request.project). With both patterns in hand, the agent knows what a new route must do, so a missing check stands out as a difference rather than an opinion.
Then it traces each new input. For every new URL in hc/front/urls.py or hc/api/urls.py it follows each request.GET, request.POST, form field and URL parameter to where it lands. A value reaching .raw(), .extra(), RawSQL or a cursor().execute() built with an f-string or % is SQL injection; the repository's own raw queries are the safe pattern, such as the docs search, which passes the query to sqlite3 as a bound parameter. A value reaching subprocess, as the shell integration in hc/integrations/shell/transport.py does behind SHELL_ENABLED, or reaching a filesystem path, is checked the same way. A new view that looks up a Check by code with Check.objects.get instead of the helper, or a write behind @authorize_read, is missing authorization.
It proves what it can rather than asserting it. It runs ./manage.py test for the apps the pull request touches, against the default SQLite database hc/settings.py falls back to, and for each authorization finding writes a short throwaway test in its sandbox that logs in as a user from another project and calls the new route, recording the status code it gets. Last, it scans the diff for secrets: keys, tokens and passwords committed in settings, fixtures or tests. hc/settings.py reads secrets through envsecret, from an environment variable or a _FILE path, so a literal value in its place is a finding.
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. First read the views, helpers and decorators that existing routes use to authorize access. Trace every new request parameter to where it reaches the database, the filesystem or a shell, check that each new route enforces the same authorization as its neighbours, and look for secrets in the diff. Run ./manage.py test for the touched apps, and for each authorization finding, confirm it with a throwaway test in the sandbox that calls the route as a user from another project. Post findings as inline review comments with the exploit in one sentence each and the status code you observed; post nothing if there are none. Do not push commits.",
"resources": [
{
"url": "https://github.com/your-org/your-django-app/pull/123"
}
]
}- 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 either nothing or a set of inline review comments, one per finding, on the exact line where the input reaches a query, a shell or a path, or where a view skips the access helper. Each comment states the exploit in one sentence, such as a member of another project reading a check's ping log by changing the UUID in the URL, and gives the fix in the repository's own idiom: call _get_rw_check_for_user, use @authorize instead of @authorize_read, pass the value as a query parameter.
For authorization findings the comment includes the status code the throwaway test saw, so the claim is observed rather than inferred. The session's transcript keeps the test runs, the throwaway tests and the trace for each input, and the review says which findings were confirmed by a request and which come from reading the code alone.
Before merging, a reviewer confirms each exploit is reachable in production settings, for example whether an integration is enabled there, and adds a permanent test for every confirmed finding. An authorization comment backed by a 200 from another project's user is the one to fix first.
Run it on every pull request that adds a route
An automation with a GitHub trigger on pull_request.opened and pull_request.synchronize runs this review on each push. The prompt posts nothing when there are no findings, so it can run on every pull request without adding noise; to narrow it, check on your server whether urls.py, views.py or forms.py changed before sending the request with the pull request URL in resources.
Set a webhook URL on the API key, and when run.completed arrives read the session for the result. A run that found nothing says so in its final answer, which gives you a record that the pull request was checked even when the review thread stays empty.
The use case this job follows, with its automation and trigger: Review every pull request.
Why run it on Rush?
A security review that matters has to read the whole path from a request to the database, compare it with how every neighbouring view authorizes, and ideally call the route as the wrong user. Doing that by hand for every pull request is exactly the work that gets skipped. On Rush each review runs in its own sandbox with the branch, the test suite and a throwaway database, so the agent can run ./manage.py test and send a real request as another project's user without any risk to your data.
Findings come with evidence: the trace, the test it wrote, the status code it saw. Because each run is isolated, it can review every pull request in parallel, and the session's record shows a security reviewer exactly what was checked on a change that later turns out to matter.
Frequently asked questions
Will it flag every raw SQL query in the codebase?
No. It reviews only what the pull request adds or changes, and it distinguishes a parameterized query, like the existing docs search that binds the user's input, from one built with string formatting. An unchanged query elsewhere is out of scope.
How does it know what the right authorization is?
It reads the helpers and decorators the neighbouring views use, such as _get_rw_check_for_user and @authorize, and compares the new route with them. A new view that fetches an object without the helper, or allows a write with a read-only key, is a finding.
Does it need a production database or real secrets?
No. It runs the app's own tests against a throwaway database in its sandbox, which for healthchecks is the SQLite file hc/settings.py uses by default. No production URL or secret is needed or given.
What happens when there is nothing to report?
It posts no review, as the prompt asks, and says in its final answer that it found nothing and which routes and inputs it traced. That answer is in the session and the run.completed webhook, so you keep a record without a comment on the pull request.