Logs, errors and crashes, fixed by your agent
See every log, error and crash from every app and environment in one place, then let your coding agent read them over MCP, fix the code and push. Every hour, if you like.
What you'll learn
- ✓Capture console logs, errors and crashes with one preload (crash capture is new in 8.13)
- ✓Tag each deployment with its environment and query one or all of them
- ✓Ask your agent for the last six hours of errors and let it fix, test, commit and push
- ✓Run the same loop every hour, with one pull request per new root cause
Logs, errors and crashes, fixed by your agent
Northstar is secure. Then our coding agent shipped three small features: a notes CSV export, shareable invite links, and a background job that expires old invites. Within the hour, the server had crashed 36 times in each environment, exports were failing, and staging couldn't create a single invite. Nobody noticed.
This episode closes that gap: every log, error and crash lands in SecureNow, and your coding agent reads them over MCP, finds the causes in your code, fixes them and pushes. Everything below is from a real run, on the companion branch tutorial/ep7-logs.
The three bugs (planted on purpose)
| Feature | Bug | Where it hurt |
|---|---|---|
| Notes CSV export | counts words with match(...).length, and match() returns null on an empty note | production + staging |
| Shareable invite links | builds the link from INVITE_LINK_BASE, which staging never set (TypeError: Invalid URL) | staging only |
| Invite expiry job | queries a column called expires (the real one is expires_at) with no try/catch, so the rejection kills the process every minute | production + staging |
Step 1 — Capture logs, errors and crashes
If you followed Episode 2, this is already running. If not, it's one package and one flag:
npm install securenow@8.13.0
node -r securenow/register src/server.js
- Every
console.log,warnanderrorbecomes a log record, linked to the request that produced it through itstrace_id. - New in 8.13: crashes. An uncaught exception or an unhandled promise rejection is recorded as a
FATALlog with its full stack trace, sent, and then the process exits exactly the way Node would (stack on stderr, exit code 1). If your app has its ownuncaughtExceptionhandler, the error is recorded and your handler decides what happens.
Here is one real crash record from this run:
{
"severity_text": "FATAL",
"body": "Unhandled promise rejection, process crashed: error: column \"expires\" does not exist …",
"exception.message": "column \"expires\" does not exist",
"exception.stacktrace": "… at async Timeout._onTimeout (src/server.js:22:24)",
"exception.origin": "unhandledRejection",
"log.source": "crash",
"environment": "production"
}
Step 2 — Tag each deployment with its environment
One line in each deployment's .securenow/runtime.json:
"runtime": { "deploymentEnvironment": "staging" }
Production says production; preview, local and test work the same way. In this run the same build ran twice, as staging (port 3101) and production (port 3102), both reporting to one SecureNow app. Every query can then target one environment, or all of them.
Step 3 — Log the error, not the payload
Pass the error object itself, so the message and stack travel with it:
console.error('[notes.export] export failed', error); // ✓ message + stack
console.error('export failed', req.body); // ✗ the payload
Request bodies and secret headers are redacted on traces, but log text is stored exactly as you wrote it. Keep passwords, tokens and request bodies out of your log lines.
Step 4 — Connect your agent
claude mcp add securenow -- npx securenow mcp
Cursor and Codex connect to the same server. A hosted endpoint is also available at https://api.securenow.ai/mcp.
Step 5 — Ask for the last six hours, and the fix
The exact prompt from the run:
Use the SecureNow MCP tools to pull the last 6 hours of error logs for the lab-auth app, across all environments.
Group them by root cause and tell me which environments each one hits. Then find each cause in this repo, fix it
with the smallest correct change, add a regression test for each fix, and run the test suite. Commit each fix
separately with a clear message, then push.
The agent called securenow_logs_list with the app, environment: "all", a six-hour window and severity: "error" (which also returns FATAL crash records), paged through about 370 records, and grouped them:
| Root cause | Environments | Count |
|---|---|---|
Crash loop: column "expires" does not exist | production + staging | 36 in each |
Notes export: Cannot read properties of null (reading 'length') | production + staging | ~231 |
Invite creation: TypeError: Invalid URL | staging only | 70 |
It also checked git: every error started at 12:22, right after the commit that shipped the three features.
Step 6 — Follow a trace when a stack isn't enough
Every error logged during a request carries a trace ID. Two more MCP calls, securenow_logs_for_trace and securenow_traces_show, open the whole request:
GET /api/notes/export 500 56.3 ms
├─ middleware · session 18.7 ms
│ └─ pg SELECT sess FROM "sessions" WHERE sid = $1 17.9 ms
├─ pg SELECT * FROM users WHERE id=$1 3.7 ms
├─ pg SELECT id,title,body,created_at FROM notes …
└─ ERROR [notes.export] export failed TypeError … app.js:486
request headers: { cookie: "[REDACTED]", … }
Step 7 — The fixes
Three commits, a line or two each, no new dependencies:
- const words = note.body.match(/\S+/g).length;
+ const words = note.body.match(/\S+/g)?.length ?? 0;
- new URL(`/invite/${token}`, process.env.INVITE_LINK_BASE)
+ new URL(`/invite/${token}`, process.env.INVITE_LINK_BASE || config.appOrigin)
- … WHERE status='pending' AND expires < now()
+ … WHERE status='pending' AND expires_at < now()
+ try { … } catch (error) { console.error(… 'INVITE_EXPIRY_ERROR' …) } // log instead of crashing
Each fix got a regression test, and the whole suite ran against a test database: 21 passed, 0 failed. The agent also noticed that the feature commit had gone in with three failing tests. Then it committed each fix separately and pushed: 4 minutes 40 seconds from question to push.
Step 8 — Prove it
After deploying the fix to both environments, ask again from the moment of the deploy:
securenow_logs_list environment: "all" severity: "error" from: "<the deploy>"
→ { "logs": [] }
Zero errors, zero crashes, traffic still flowing, and staging creates invites again.
Step 9 — Every hour
In a Claude Code session, keep checking while you work:
/loop 1h Pull the last hour of SecureNow error logs for lab-auth, fix what you find with a test, and push.
Unattended, the companion branch ships .github/workflows/hourly-error-fixes.yml. Every hour an agent reads the last hour of errors from the hosted MCP endpoint with a Read Only SecureNow API key, skips any cause that already has an open pull request, and opens one pull request per new root cause. Nothing merges until you review it.
on:
schedule:
- cron: "7 * * * *" # every hour
env:
SECURENOW_API_KEY: ${{ secrets.SECURENOW_API_KEY }} # Read Only key
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
It needs two repository secrets, and GitHub runs scheduled workflows from the default branch only.
All your apps, or the terminal
The same MCP call takes a list of app keys: your API, your web app, your workers. One SDK in each, one query for all of them. From the terminal:
securenow logs --level error --since 6h --env all
What you just did
You captured every log, error and crash from two environments in one place, let your agent read about 370 records, find three root causes, fix them with tests and push, and proved the fix with the same query. Then you put it on a schedule. See it. Fix it. Ship it.
Try it on your own app
Start the threat model, then plug SecureNow in — the whole series works on your project today.