The Security Audit That Runs Every Day
This is Episode 8 of "How We Automated an AI Business." Last time: self-healing — layered recovery from launchd restarts to retry budgets, and why Timeout.timeout doesn't actually kill subprocesses. This time: the agent that attacks us on a schedule.
The security agent has a one-line mandate: find vulnerabilities before attackers do.
It's a Claude Code process with a role definition that reads like a penetration tester's checklist. Think like an attacker. Test everything. Report clearly. It gets read access to the full codebase, a bash shell for running scanners, and web fetch for checking production headers. No write access — an auditor shouldn't be able to modify what it's auditing.
Every morning at 8am, a launchd daemon creates a security task in the work queue. The orchestrator picks it up within 60 seconds and spawns the agent. Twenty minutes later, there's a structured report in agents/reviews/.
We've run over 30 audits in three weeks. Here's what we learned.
The Audit Protocol
Every run follows the same sequence:
Static analysis. Brakeman scans the Rails codebase for common vulnerability patterns — injection, XSS, mass assignment, unsafe redirects. Thirty seconds, catches the easy stuff.
Dependency audit. Every gem checked against the advisory database. One outdated dependency with a known CVE can undo months of careful coding.
Commit review. The interesting part. The agent pulls the git log since the last audit and reads every diff. New controller? Check that it requires authentication. New endpoint? Verify it has rate limiting. New parameter handling? Look for injection vectors.
Auth verification. A sweep of every controller, grouped by namespace. Internal dashboards, API endpoints, webhook handlers, public pages. Each one gets a status: what auth method protects it, and whether that's correct. The output is a table — easy to scan, impossible to fudge.
Production headers. Live check against the running site. TLS, security headers, cookie flags.
The report uses severity ratings, reproduction steps, and fix recommendations. Every finding gets a tracking ID that carries across audits.
What It Found
The first full audit found real problems.
A token comparison in two API controllers was using Ruby's == operator — which is not constant-time. An attacker could theoretically measure response times to guess the token byte-by-byte. The fix was one line: switch to a timing-safe comparison function. But neither the coder who wrote it nor the CEO who reviewed it caught it. The security agent did, on its first pass.
Same audit: two public-facing endpoints with no rate limiting. One of them triggered an email on every request. An attacker could have used it for email bombing — abusing our mail service to spam arbitrary addresses. We added throttling rules that afternoon.
Over three weeks, the audits found issues across all severity levels. Timing attacks, missing rate limits, object enumeration, unsigned webhook handlers. Each finding got documented, prioritized, and most got fixed within 24 hours.
The carried findings list is the most honest part of the report. Every audit reprints the full list of open issues with their current status. You can't hide from a finding — it shows up in every report until someone fixes it.
The Model Matters
Early on, we ran one audit with a smaller, faster model to save on API costs. It found nothing.
The next day, we ran the same audit with our full-capability model. Four HIGH-severity findings, including the timing attack vulnerability. The smaller model had reviewed the same code and reported it clean.
This became a hard rule: security audits run on the most capable model available, period. The cost difference is trivial compared to shipping a vulnerability. An auditor that misses findings is worse than no auditor — it gives you false confidence.
The Blog Incident
The most interesting finding wasn't in our code. It was in our blog.
We'd published a post about our security practices — how we handle authentication, rate limiting, webhook verification. Educational content. The kind of thing developers learn from.
The security agent flagged it.
The post contained a complete table of our internal admin routes with exact URL paths. The full rate limiting configuration with specific thresholds and endpoints. A list of endpoints that lacked protection. The entire Content Security Policy whitelist. Specific vulnerability types we'd found and fixed, with enough detail to know which ones might still be open.
A security blog post that was, functionally, an attack surface map.
We redacted ten findings across four files. Specific routes became "admin-protected endpoints." Exact rate limit numbers became "tiered throttling." Named vulnerability types became "severity categories." The educational value survived. The exploitation value didn't.
This incident led to a new automated gate. The blog publishing tool now scans every post for security-sensitive patterns before allowing a commit — internal routes, rate limit configurations, authentication method names, vulnerability classifications. The same tool that enforces our weekly publishing cadence now also prevents us from publishing an instruction manual for attackers.
A security agent that audits blog posts about security. Recursive defense.
The Report That Stays Honest
After 30+ audits, the system has settled into a rhythm. Most daily audits find zero new issues — the codebase is stable, changes are incremental, and the existing defenses work. The value isn't in the dramatic findings anymore. It's in the carried findings list.
Every report ends with the same table: open issues, grouped by severity, with their tracking IDs and status. Three HIGHs that need architectural changes. Four MEDIUMs that are mitigated but not fixed. Four LOWs that are acceptable risks.
The agent doesn't care that a finding has been open for two weeks. It doesn't rationalize. It doesn't deprioritize because "nobody's exploited it yet." It just reprints the table. Every single day.
There's something useful about a process that can't be embarrassed into silence. A human security reviewer might stop mentioning a finding after the third time it's ignored. The agent has no such filter. Open findings appear in every report until someone writes the fix.
What We Got Wrong
The biggest mistake was scheduling. We started with "every CEO session" — which meant audits happened at unpredictable times, sometimes twice a day, sometimes with a three-day gap. Commits slipped through the gaps.
Switching to a fixed daily launchd schedule solved it. Same time every morning. The daemon checks for existing security tasks before creating a new one — no duplicates, no gaps.
The second mistake: treating reports as fire-and-forget. Nobody read them. The fix was tracking findings across audits with stable IDs. The carried findings list became the accountability mechanism — growing or shrinking, it tells you whether the security posture is improving or decaying.
Current grade: A-. The first full audit found a dozen issues. The most recent found zero new ones. A handful of HIGHs remain in admin-only contexts, waiting for architectural fixes. The trajectory is clear.
A daily automated audit won't catch a zero-day. But it catches the boring vulnerabilities — the ones that actually get exploited. The missing rate limit. The timing-unsafe comparison. The blog post that tells attackers where to look.
Next time: the orchestrator — how Claude Code agents actually ship production code, from task claims to deploys.