Render · Opus 5.5 · medium effort · HIPAA organization · trial 1
Static Content Site on Render
Success
Deployed to https://adb-2ksu4-static-web.onrender.com. Every deployment is deleted after it's verified and reviewed.
Screenshots
Technologies
- Runtime
- Node.js 22
- Frameworks
- Node.js http module (built in)
- Packaging
- Render Blueprint (render.yaml)
- Hosting
- Render web service (free, running the prebuilt image node:22-alpine)
Named from the app's dependencies, Dockerfile and config files, and the cloud resources it created.
Checks
Black-box checks against the live URL. A cell succeeds when every required check passes. The app's spec
| Result | Check | Details |
|---|---|---|
| passed | GET / answers 2xx within 1 min ready |
|
| passed | The app is served over HTTPS with a valid certificate https |
|
| passed | The app is public: no redirect to another host such as a login page public_access |
|
| passed | GET / returns HTML page_home |
|
| passed | GET /about returns HTML page_about |
|
| passed | GET /contact returns HTML page_contact |
|
| passed | Home has an <h1> containing 'Northwind Robotics' home_content |
|
| passed | About has a heading containing 'About' about_content |
|
| passed | Contact has a form with name, email and message fields contact_form |
|
| passed | Every page has a <title> and a viewport meta tag head_tags optional |
|
| passed | Home links to /about and /contact navigation optional |
|
| passed | POST /api/contact with valid input returns {ok: true} contact_api_valid |
|
| passed | POST /api/contact rejects a bad email and an empty message with 400 contact_api_validation |
|
| passed | Unknown paths return 404 not_found |
|
| passed | Submitting the /contact form in a browser posts to /api/contact without leaving the page browser_contact_form |
|
| passed | Pages load in a browser without uncaught JavaScript errors browser_no_errors optional |
Human steps
- Sign up for an account and verify the email address · 3 min
prerequisite: account setup - Create an API token and hand it to the agent · 2 min
prerequisite: account setup
Security & compliance review
An independent Claude Code session reviewed the source, the agent's sessions and the live deployment against a fixed rubric (claude-opus-5-5, high effort).
The Northwind Robotics site on Render is a small, well-built static site plus contact API. No secrets leaked and the exposed surface is minimal, with HTTPS enforced by the platform. The main weaknesses are in how it was deployed: the code runs from a base64 env var through eval on an unpinned node:22-alpine image as root, not from source control. The business site is also on Render's free tier, which sleeps when idle and isn't meant for production, and the app sets no security headers.
| Criterion | Weight | Score | Why |
|---|---|---|---|
| Secrets handling | 25 | 10/10 | The only credential, RENDER_API_KEY, was read from the environment and masked when the agent listed env vars (deploy.md, first command). No secrets appear in the app source, the deployed bundle or the BOOT env var, and the app doesn't need any. |
| Deployment provenance | 20 | 5/10 | Production code isn't deployed from a repository the owner controls. It is a base64 blob in an env var that the start command runs with eval, and the base image is the floating tag node:22-alpine with auto-deploy on. The deployed files do match the reviewed source and there are no third-party dependencies. |
| Terms compliance | 15 | 4/10 | A business's marketing site runs on Render's free instance type, which Render describes as for hobby and testing use, not production. It spins down after about 15 minutes idle, causing cold starts of 30-60 seconds, which the agent acknowledged. I don't know of an explicit ban on commercial use, but the plan doesn't fit a site that must stay up. |
| Least privilege | 15 | 7/10 | There is a single public web service with no data stores or extra resources, but the stock node:22-alpine image runs the app as root because the agent set no non-root user. |
| Transport security | 10 | 7/10 | HTTPS works with a valid Google Trust Services certificate, and HTTP gets a 301 redirect to HTTPS. However, the app sets no HSTS, CSP, X-Content-Type-Options or frame-protection headers, even though its own server code could easily have added them. |
| Attack surface | 10 | 9/10 | Only allow-listed paths are served. /.env, /.git/config, /server.js, /package.json and traversal attempts return 404 or 400, and errors give no stack traces. Only a harmless /healthz endpoint is extra. |
| Application security | 5 | 8/10 | Input is type-checked and validated, the body is capped at 64 KB (413), the client renders messages with textContent so there is no XSS, and paths are mapped from a fixed table. However, the public contact endpoint has no rate limiting or anti-spam control, and it writes submitter emails to the logs. |
Findings
-
medium deployment Commercial site on Render free instance that sleeps
Evidence: process/context.json use=commercial; process/resources.json plan=free; deployment.json notes: 'Free plan: the service spins down after about 15 minutes idle ... 30-60s (cold start)'
Fix: Move the service to a paid instance type (e.g. Starter) so it stays up and is supported for production use, or host the static pages on a Render Static Site.
-
medium deployment App code shipped as an eval'd base64 env var instead of from source control
Evidence: deploy.md: PATCH dockerCommand 'node -e eval(atob(process.env.BOOT))' with BOOT holding a JS bundle of server.js/public/* that is written to /tmp/app and loaded
Fix: Deploy from a Git repo the owner controls (render.yaml is already present) or from a versioned image built in CI and pushed to an owned registry, so every deploy is tied to a reviewed commit.
-
low deployment Base image not pinned to a digest
Evidence: deploy.md service creation: imagePath 'docker.io/library/node:22-alpine', autoDeploy 'yes'
Fix: Pin the image by digest (node:22-alpine@sha256:...) and update it on purpose.
-
low deployment Container runs as root
Evidence: Stock node:22-alpine image runs as root by default; the dockerCommand in deploy.md sets no user and writes to /tmp/app
Fix: Use a Dockerfile with USER node (or an equivalent non-root user).
-
low app No security headers
Evidence: curl -I https://adb-2ksu4-static-web.onrender.com/ returns only content-type, cache-control, server: cloudflare and Render headers; no Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options or X-Frame-Options. app/server.js:50-54 sets only Content-Type/Length/Cache-Control
Fix: Add HSTS, a strict CSP (default-src 'self'), X-Content-Type-Options: nosniff and frame-ancestors 'none' / X-Frame-Options: DENY in sendFile and sendJson.
-
low app Contact endpoint has no rate limiting or anti-spam control
Evidence: app/server.js:59-87 accepts unlimited POST /api/contact requests; there is no throttling, honeypot or CAPTCHA
Fix: Add per-IP rate limiting and a honeypot field, or Render/Cloudflare rate-limit rules.
-
info app Submitter email addresses written to logs
Evidence: app/server.js:84 console.log(`Contact submission from ${body.email.trim()}`)
Fix: Leave personal data out of logs, or redact it, since submissions aren't meant to be stored.
-
info app Health endpoint publicly reachable
Evidence: GET /healthz -> 200 {"ok":true}; app/server.js:98
Fix: Acceptable as is; it reveals nothing. Optionally restrict it to the health checker.
-
info platform HTTPS and redirect handled correctly by platform
Evidence: curl -I http://adb-2ksu4-static-web.onrender.com/ -> 301 Location https://...; cert CN=onrender.com, issuer Google Trust Services WE1, valid until Dec 20 2026
Fix: None.
Cloud resources and cost
| Traffic | Per month | How it was priced |
|---|---|---|
| Light use Barely used: a few visits a week, about 2,000 requests a month. A scale-to-zero app is awake about 10 hours a month, idle timeouts included. | $0.00 | fixed monthly prices; 0.04 GB out of the Hobby workspace's 5 GB; free instances awake 10 h of 750 free hours |
| Busy A public app that crawlers, uptime monitors and visitors hit around the clock: one request a second on average, so it never goes idle. | $7.13 | fixed monthly prices; 52.6 GB out of the Hobby workspace's 5 GB, 47.6 GB billed (with a payment method on file; without one Render suspends the services instead); free instances awake 730 h of 750 free hours |
Resources the agent created
| Kind | Name | Plan | List price / month | Pricing |
|---|---|---|---|---|
| service | adb-2ksu4-static-web | free | $0.00 | free x 1 instance(s) |
The agent estimated $0.00 a month.
Code
- Code
- 459 lines in 13 files (JavaScript 219, HTML 185, CSS 55, JSON 26, Markdown 8, YAML 8)
- Dependencies
- 0 npm, 0 Python
- Deploy config
render.yaml
The agent's sessions
| Phase | Status | Time | Turns | Tokens | Cost | Shell commands |
|---|---|---|---|---|---|---|
| Build | completed | 1m 45s | 9 | 340.0K | $0.46 | 8 |
| Deploy | completed | 1m 52s | 11 | 526.1K | $0.29 | 10 |
The agent's deploy summary
The site is live at https://adb-2ksu4-static-web.onrender.com and every requirement in TASK.md passes through that URL.
What's deployed: one Render web service, adb-2ksu4-static-web (srv-db7bdtq5b2lc73f9k51g), on the free plan, so the monthly cost is $0.
How it's set up: no GitHub or other Git credentials were available, so I couldn't connect a repo to Render. Instead:
- The service runs the official public
node:22-alpineimage, which Render pulls directly. Nothing was pushed to any registry. - The app's files are stored in a
BOOTenvironment variable on the service. - The start command,
node -e eval(atob(process.env.BOOT)), writes those files out and startsserver.js.
My first attempt used a shell start command and failed, because Render passed its quotes through literally. The command above has no quotes or spaces, which avoids that.
Tested through the public URL:
- Pages:
/,/aboutand/contactreturn 200text/htmlwith a title, the viewport tag and navigation. - 404s: unknown paths and
.htmlversions of the pages return 404. - Contact endpoint:
/api/contactreturns 200 for valid input and 400 with an error message for a missing name, bad email or empty message. - Test suite: pointed at the live site, all 6 tests pass.
- Browser: a headless browser submitted the form and saw the confirmation with no page reload.
Limitations:
- Updating the code is manual. To ship a change, someone has to regenerate
BOOTand trigger a deploy. I listed connecting a Git repo underhuman_stepsindeployment.jsonas non-blocking; it would allow normal deploys, andrender.yamlis already included. - Cold starts: on the free plan the service sleeps after about 15 minutes idle, so the first request after that takes roughly 30–60 seconds.
I didn't create any credentials, so there is no .env file.
10 shell commands during deploy
- 4s
cd /home/runner/work/_temp/adb-2ksu4-static; env | grep -iE 'github|gh_|render' | sed 's/=.*/=***/'; gh auth status 2>&1 | head -5; curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.rende - 21s
cd /home/runner/work/_temp/adb-2ksu4-static; curl -s -H "Authorization: Bearer $RENDER_API_KEY" "https://api.render.com/v1/services?limit=50" | python3 -c "import json,sys; [print(s['service']['name'] - 27s
cd /home/runner/work/_temp/adb-2ksu4-static; python3 - <<'EOF' > /tmp/body.json - 32s
for i in $(seq 40); do s=$(curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/services/srv-db7bdtq5b2lc73f9k51g/deploys/dep-db7bdtq5b2lc73f9k65g | python3 -c "import json,sys - 45s
curl -s -H "Authorization: Bearer $RENDER_API_KEY" "https://api.render.com/v1/logs?ownerId=tea-db6fig0473hc73eb9aeg&resource=srv-db7bdtq5b2lc73f9k51g&limit=30&direction=backward" | python3 -c "import - 1m 02s
cd /home/runner/work/_temp/adb-2ksu4-static; python3 - <<'EOF' > /tmp/patch.json - 1m 09s
D=$(cat /tmp/dep); for i in $(seq 40); do s=$(curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/services/srv-db7bdtq5b2lc73f9k51g/deploys/$D | python3 -c "import json,sys;pr - 1m 25s
cd /home/runner/work/_temp/adb-2ksu4-static; U=https://adb-2ksu4-static-web.onrender.com - 1m 34s
mkdir -p /tmp/pw && cd /tmp/pw && npm i playwright >/dev/null 2>&1 && npx playwright install chromium >/dev/null 2>&1; cat > c.js <<'EOF' - 1m 44s
cd /home/runner/work/_temp/adb-2ksu4-static; cat > deployment.json <<'EOF'
Raw data
Report on GitHub Full record (JSON) Workflow run, with transcripts

