What the agent does
The agent reads the configuration before it sends a request. In a FastAPI app like fastapi/full-stack-fastapi-template, backend/pyproject.toml requires Python 3.14 and fastapi[standard], sqlmodel, pyjwt, pwdlib with Argon2 and bcrypt, psycopg, alembic and sentry-sdk. backend/app/core/config.py loads a .env one level up, and the committed .env sets SECRET_KEY, FIRST_SUPERUSER_PASSWORD and POSTGRES_PASSWORD to changethis. The same file refuses to start with those values unless FASTAPI_ENV=development, where it only warns; the audit records which environments set that variable, because backend/app/api/main.py also uses it to mount the private router. That router, in backend/app/api/routes/private.py, creates a user from an email and password with no authentication at all. It is meant for tests, and the first finding of the report is whether any deployment can reach it.
Then authentication. backend/app/core/security.py signs access tokens with HS256 and SECRET_KEY, and config.py sets ACCESS_TOKEN_EXPIRE_MINUTES to eight days. backend/app/api/deps.py decodes the token, loads the user and refuses inactive accounts; backend/app/api/routes/login.py issues tokens from an OAuth2 password form, always answers password recovery with the same message to prevent email enumeration, and keeps the HTML-preview route for recovery emails behind get_current_active_superuser. The agent reads each of these, then sends the request that shows the behaviour against the app running on localhost: a token with a tampered signature, an expired token, a recovery request for an unknown address, and an inactive user's login.
Access control is the item and user routes. backend/app/api/routes/items.py checks owner_id against the current user on read, update and delete and answers 403 otherwise, with superusers exempt; users.py keeps listing and creation behind the superuser dependency, answers 409 when a user changes their email to one already taken, and refuses a password change to the same password. The audit creates two ordinary users through the public registration route, has one request the other's item, and records the status code. It reads backend/app/main.py for the CORS middleware, which allows only FRONTEND_HOST with credentials, and for the OpenAPI document at /api/v1/openapi.json, which is public and lists every route the audit should have covered.
To run the app it follows the repository's own CI. .github/workflows/test-backend.yml starts the db and mailpit services from compose.yml, runs scripts/prestart.sh for the Alembic migrations and the first superuser, and then the tests under coverage. The agent does the same in its sandbox with uv, starts the API on localhost only, and never publishes a port. The prompt says change no code, so there is no fix commit: each finding in security-audit.md carries the file and line, the request sent, the response, a severity and a one-sentence fix, and a check that passed is written as passed.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Audit this FastAPI app for security problems and change no code. Read backend/pyproject.toml, .env, backend/app/core/config.py, backend/app/core/security.py, backend/app/api/deps.py, backend/app/api/main.py and every file under backend/app/api/routes/ first. Start Postgres from compose.yml, run backend/scripts/prestart.sh and start the API on localhost only, the way .github/workflows/test-backend.yml does. Check default secrets and where FASTAPI_ENV=development is set, the routes mounted only in development, token signing and lifetime, password recovery and reset, ownership checks on items, superuser-only routes, CORS, and what the public OpenAPI document exposes. For each finding write the file and line, one request that shows it against the running app, the response, a severity and the fix, in security-audit.md. Write checks that passed as passed. Do not paste secrets or tokens into the report.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
}
]
}- 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 security-audit.md as an artifact on the session, with a url on api.prix.dev that downloads it without a key until url_expires_at, and the same file through GET /o/{org}/sessions/{id}/artifacts/security-audit.md with the key. The repository gets no commit and no pull request; git status at the end of the transcript is clean.
The report has one section per area: configuration and secrets, development-only routes, tokens, password recovery, access control, CORS and exposure. Each finding is a file and line, the request the agent sent to localhost, the response that shows it, a severity, and a fix it did not apply. A check that held, such as the owner check on items answering 403, is written as held, so the reader knows it was tried rather than skipped.
A reviewer judges the report by opening the cited lines and comparing them with the recorded responses, not by the severity words. The transcript holds the compose start, the migration run, the API start and every request with its output, so a finding whose response does not match its line is visible. The sandbox parks when the turn ends, so a follow-up can ask for one route to be re-checked without installing the app again.
Run it on every release tag
When your release workflow tags a version, a job in it POSTs this body to https://api.prix.dev/o/{org}/sessions with the repository in resources and the tag in the Idempotency-Key header, so a re-run of the workflow returns the audit already started rather than a second one. The Rush GitHub App must be installed on the repository, or the resource is 422 RESOURCE_NOT_ACCESSIBLE.
Give the key a webhook_url and Rush POSTs run.completed when the turn finishes, within about 30 seconds. The handler verifies the signature with the key's whsec_ secret and downloads the artifact from the session, since the report is a file rather than the result text.
The use case this job follows, with its automation and trigger: Produce a report or file, no repo.
Why run it on Rush?
An audit that only reads code produces guesses. This one starts Postgres, migrates the schema, creates the first superuser and sends requests, which on a laptop means a database on port 5432, a .env with real-looking secrets, and an API process left running. The sandbox runs all of it on localhost and keeps every request and response on the session, and nothing stays bound on your machine.
Each finding is checkable: a line you can open and a response you can compare. Several apps or branches can be audited at once, each in its own sandbox, up to the key's max_concurrent limit. Because the report is written as passed and failed checks rather than only failures, a clean audit is also a record of what was tried.
Frequently asked questions
Will it open a pull request with fixes?
No. The prompt says change no code, so the result is the report and nothing else. Each finding ends with a one-sentence fix, and a later run can apply one and open a pull request. Keeping the audit separate from the fix lets the report be checked against the code as it was.
Is the private router a real problem or a test helper?
It is a test helper that backend/app/api/main.py mounts only when FASTAPI_ENV is development. The audit's job is to show whether any deployment sets that value, since the committed .env does. If none does, the report says the route is unreachable in production and marks the check as passed.
Does it need my production secrets or database?
No. It runs the app in its sandbox with throwaway values and the Postgres image from compose.yml, seeded by scripts/prestart.sh. The report names the files that hold default secrets and does not paste their values.
What about dependency vulnerabilities?
The prompt above is about the app's own behaviour. Add a sentence asking for uv to audit the lockfile, or run that as its own job, so the report stays one subject. The dependency versions in backend/pyproject.toml and uv.lock are what the agent would check.