What the agent does
The agent clones the app into its own cloud sandbox and reads how responses are built before it adds a header. In an Express app like OWASP/NodeGoat, server.js requires express, express-session, consolidate with swig templates and the MongoDB driver, and the lines that would harden it are already there as comments: // const helmet = require("helmet"), and a block marked "Fix for A5 - Security MisConfig" that would call app.disable("x-powered-by"), helmet.frameguard(), helmet.noCache(), helmet.contentSecurityPolicy(), helmet.hsts() and nosniff(). package.json already lists helmet at ^2.0.0 and dont-sniff-mimetype, versions old enough that the middleware names in the comment no longer match a current release, so the first change is the dependency. The session is set up with saveUninitialized: true, resave: true, a cookieSecret from config/env/all.js, and no cookie options; the httpOnly and secure settings are in another comment. The server is plain http.createServer(app) on port 4000.
A content security policy is only as good as the inventory behind it, so the agent reads the templates. app/views/layout.html loads /vendor/jquery.min.js, /vendor/bootstrap/bootstrap.js, /vendor/bootstrap/bootstrap-tour.js, the Raphael and Morris chart libraries, /js/chart/chart-data-morris.js and /js/tour/redirects-steps.js, all from app/assets served by express.static, and then loops over environmentalScripts; config/env/development.js fills that list with a livereload script fetched from port 35729, while config/env/production.js exports an empty object. app/views/login.html carries one inline <script type="application/javascript"> of its own. From that the agent writes a policy with default-src 'self', a script-src of 'self' plus a per-request nonce that the layout and login templates print on their inline script tags, style-src 'self', img-src 'self' data:, frame-ancestors 'none', form-action 'self' and object-src 'none', and in development only it adds the livereload origin to script-src and connect-src.
The other headers follow the comment but with current defaults: helmet() for X-Content-Type-Options, Referrer-Policy, X-Frame-Options and the rest, app.disable("x-powered-by"), and HSTS applied only when NODE_ENV is production, because the server itself speaks HTTP and a Strict-Transport-Security header on a plain port 4000 would be wrong in development and a trap behind a proxy that has not been told about with app.set("trust proxy", 1). The session gets cookie: { httpOnly: true, sameSite: "lax", secure: production } and a name that is not the library default. The agent leaves the deliberately vulnerable tutorial routes alone; the job is headers and the cookie, and it says so in the pull request.
Then it checks that nothing it shipped breaks a page. npm test runs grunt test, which is mochaTest:unit over test/unit/*.js, and that directory does not exist in the repository, so the agent adds test/unit/headers.js, a Mocha test that starts the app against the sandbox's MongoDB and asserts the headers on /, /login and a signed-in /dashboard, which gives npm test something to run. The real check is the browser suite: npm run test:ci runs the Cypress specs under test/e2e/integration/, from login_spec.js to tutorial_spec.js, and a policy that blocks the login page's inline script or a chart library shows up there as a failed spec. It seeds the database with npm run db:seed first, as README.md says, and runs grunt precommit for jsbeautifier and jshint under the .jshintrc rules (double quotes, four-space indent).
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Add security headers to this Express app. Read server.js, package.json, config/config.js, config/env/all.js, config/env/development.js, config/env/production.js, app/views/layout.html, app/views/login.html and app/views/signup.html, and list every script and style source the templates load, including the environmentalScripts loop and any inline script. Upgrade helmet to a current release, enable it with a content security policy built from that inventory, using a per-request nonce for the inline scripts and allowing the livereload origin only outside production, disable x-powered-by, send HSTS only in production behind trust proxy, and set httpOnly, sameSite and a production-only secure flag on the session cookie with a non-default name. Start MongoDB in the sandbox, run npm run db:seed and npm start, and curl -I the home, login and dashboard pages to show the headers. Add a Mocha test under test/unit/ that asserts them so npm test runs something, run npm run test:ci so the Cypress specs prove every page still loads under the policy, run grunt precommit, and open a pull request that lists each header with its value and the pages checked. Do not change the tutorial routes or their intended vulnerabilities.",
"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 a pull request on its own branch that changes server.js and package.json with package-lock.json, adds a nonce attribute to the inline script tags in app/views/layout.html and app/views/login.html, and adds test/unit/headers.js. The description holds a table of the response headers on /, /login and /dashboard as curl -I printed them, the content security policy with a line per directive and the template line that justifies each source, and the Cypress summary.
The transcript at GET /o/{org}/sessions/{id}/messages keeps the MongoDB start, the seed, every curl, the Mocha run and the full Cypress output, so a reviewer can see which spec exercised which page under the new policy. GET /o/{org}/p/{project}/sessions/{id}/changes shows the diff.
Before merging, a reviewer confirms the production branch of the policy has no livereload origin, that HSTS is gated on NODE_ENV and the proxy setting matches the deployment, and that the session cookie name change does not sign everyone out at a bad moment. A follow-up on the same session can tighten style-src once the inline styles in the templates are moved to files.
Run it on every Express service without helmet
Run it once per service from the terminal, then let a script do the rest. A loop on your server lists the repositories whose package.json lacks helmet, sends one POST /o/{org}/sessions per repository with this body and that repository in resources, and uses the repository name as the request_id so a rerun returns the run already started. Each run gets a sandbox of its own with its own MongoDB.
Give the key a webhook URL and Rush POSTs run.completed with data.session_url when each turn ends. A handler reads the session, pulls the pull request link and the header table from the result, and posts them where the service owners will see them; a run that ended needs_review because a Cypress spec failed under the policy is listed for a person to read.
The use case this job follows, with its automation and trigger: Hand off from your terminal.
Why run it on Rush?
A content security policy written from memory breaks the first page with an inline script, and the only way to know is to load every page with the policy on. In its own sandbox the agent runs the app against a real MongoDB, drives the Cypress suite the repository already has, and keeps the output, so the pull request shows which pages were exercised rather than asserting that the policy is safe.
You start it from a terminal and come back to a pull request. The sandbox parks when the turn ends and wakes for a follow-up, so moving the inline styles out of the templates and tightening style-src is the next message on the same session, with the same database and seed.
Frequently asked questions
Will the policy break the login page?
app/views/login.html has an inline script, so a policy with script-src 'self' alone would block it. The agent adds a per-request nonce that the template prints on that tag and the policy allows, then runs login_spec.js and the other Cypress specs to show the page still works. Any spec that fails under the policy ends the run needs_review.
Why is HSTS only sent in production?
The app listens with http.createServer on port 4000, so there is no TLS to pin in development, and a Strict-Transport-Security header sent through a proxy that terminates TLS needs trust proxy set for the secure cookie flag to work. The agent gates both on NODE_ENV and documents the proxy setting in the pull request.
Does it fix the other vulnerabilities in the app?
No. The prompt limits the job to response headers and the session cookie, and the pull request says the tutorial routes were left as they are. Escaping, CSRF tokens and the rest of the commented fixes in server.js are separate changes with their own tests.
Why add a test under test/unit?
Because npm test already points there: Gruntfile.js runs mochaTest:unit over test/unit/*.js, and the directory does not exist, so the script passes while running nothing. A small Mocha test that asserts the headers on three pages makes the command meaningful and guards the policy against a later edit.