Auditing Your Own Codebase: What I Found When I Asked Claude to Be Brutally Honest
I asked Claude to analyze 50,000 lines across three repos: top 5 strengths, top 5 weaknesses, no sugar-coating. Then I spent a day fixing everything it found.
I run a personal infrastructure platform across three repos: a FastAPI life dashboard with 157 MCP tools, a GitOps Kubernetes homelab on Talos Linux, and an AI agent orchestration platform. About 50,000 lines of code total, built over months of evening and weekend sessions.
I asked Claude to do a deep-dive analysis: top 5 strengths, top 5 weaknesses, no sugar-coating. Then I spent a day fixing everything it found. Here’s what happened.
The Setup
Three repos, three different stacks:
| Repo | What It Does | Stack |
|---|---|---|
life-hub | Personal life dashboard: mood, sleep, habits, goals, finances, 157 MCP tools | FastAPI, SQLite, HTMX, Jinja2 |
home-ops | GitOps K8s homelab: 15 apps, Flux, Talos Linux | YAML manifests, Helm, SOPS |
openclaw-workspace | AI agent team: 9 agents, Mission Control dashboard | Node.js/Express, vanilla JS SPA |
The analysis ran three parallel exploration agents, one per repo, each reading 15-20 representative files. The results came back in about 4 minutes.
What Was Strong
1. Security posture was production-grade across the stack
Not “good for a homelab”: genuinely solid. Every pod has seccompProfile: RuntimeDefault, runAsNonRoot where possible, allowPrivilegeEscalation: false, capabilities.drop: ["ALL"]. Every namespace has default-deny NetworkPolicy for both ingress and egress. The life-hub namespace alone has 18 network policy objects mapping every cross-namespace data flow.
On the application side: hmac.compare_digest for timing-safe auth comparison, IP-based sliding-window rate limiting, per-request CSP nonces validated by automated tests, and prompt injection mitigation via data boundary instructions in the AI engine.
The three layers (infrastructure, backend, frontend) each independently enforce security boundaries. That’s defense in depth that actually works.
2. Operational resilience engineering
The codebase consistently prioritizes “what happens when things fail” over happy-path optimization. The AI engine has a circuit breaker (3 failures → 5-minute open state) with daily budget enforcement in cents. The Mission Control data layer uses atomic three-file writes (copy→.bak, write→.tmp, rename→final) with automatic .bak fallback on read failure. The today page uses a _resilient_fetch pattern ensuring optional features never break the page render:
async def _resilient_fetch(label, coro_factory, default=None):
try:
return await coro_factory()
except Exception:
logger.debug("Resilient fetch failed: %s", label, exc_info=True)
return defaultSimple, but it means mood inference, baby tracking, and weather data can all fail without taking down the dashboard.
3. GitOps done right
The entire system is reproducible from Git. Flux two-layer dependency model ensures infrastructure (Traefik, Cilium, Longhorn, Authentik) is healthy before any app reconciles. The Authentik SSO configuration (8 forward-auth providers, 4 OIDC providers, 12 applications) lives in a single declarative blueprint ConfigMap. Agent personas and behavioral policies are Markdown files managed in Git, deployed via ConfigMap, with hash annotations triggering pod restarts on change. Prompt engineering as GitOps.
4. The three-interface architecture
Running REST API, HTMX UI, and MCP server from a single FastAPI process sounds ambitious. It works cleanly because MCP tools bypass REST and hit the DB directly via SQLAlchemy sessions: no internal HTTP overhead. The MCP server uses pure ASGI middleware (not FastAPI’s BaseHTTPMiddleware) to preserve streaming transport compatibility. 2,064 test functions across 158 files validate the whole surface.
5. Documentation quality
The CLUSTER.md in the agent workspace is a masterclass in persistence boundary documentation: exactly what survives pod restarts, what gets overwritten, what gets seeded on first boot. Network policy files double as topology maps. Security exceptions have inline comments explaining why.
What Was Weak
The pattern was unmistakable: strengths were architectural (upfront design decisions), weaknesses were operational (deferred maintenance). This is the signature of a solo engineer who builds things right the first time but doesn’t circle back for cleanup passes.
1. main.py was a 4,925-line monolith
The app factory, all 49 UI page routes, digest API endpoints, CSP middleware, Jinja2 template setup, background task scheduling, and template filters: all in one file. The today page handler alone orchestrated 15+ parallel async functions. Any change to any page required navigating a file so large it exceeded most editors’ comfortable working range.
2. No CI test execution, anywhere
Life-hub had 2,064 tests but the CLAUDE.md said CI didn’t run them. Turns out CI did run them. The docs were stale. Still counts as a weakness: if your documentation says tests don’t run, newcomers (and AI agents) will believe it. OpenClaw had zero tests. Home-ops had no manifest validation.
3. Data layer structural debt
All 47 ORM models use bare Mapped[int] columns without ForeignKey() or relationship(). No cascade deletes, no lazy loading. Dates stored as ISO strings. Schema migrations were hand-rolled via a migrate_constraints() function that had grown to 12+ sub-functions with raw SQL dedup logic.
4. Organic drift in OpenClaw
Chart.js loaded from CDN (silent failure with restricted egress). Timezone hardcoded in two places. No docs DELETE endpoint, so documents accumulated unbounded. Status normalization duplicated between server and client (turned out to be inverse mappings, not actual duplicates, but the naming was confusing enough to flag).
5. Home-ops policy gaps
The CLAUDE.md specified app.kubernetes.io/* labels but every custom app used only app: <name>. Four egress policy TODOs left unresolved. Traefik HelmRelease used an unbounded version range. A stale restartedAt annotation was committed, causing unnecessary Flux reconciliation loops.
The Fix: One Day, All Five Weaknesses
Splitting the Monoliths
life-hub main.py: 4,925 → 390 lines. Created app/ui/ with 27 focused modules. Each uses FastAPI’s APIRouter:
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from app.ui.templates import templates
router = APIRouter()
@router.get("/goals", response_class=HTMLResponse, include_in_schema=False)
async def goals_page(request: Request):
# ... page logic ...
return templates.TemplateResponse(request, "goals.html", ctx)The key insight: tests use httpx.AsyncClient with ASGI transport, so they test by URL path, not function reference. Moving routes from @app.get(...) to @router.get(...) doesn’t break anything. Only two tests broke: ones that imported functions directly from app.main by name.
openclaw server.js: 1,349 → 84 lines. Created routes/ with 14 modules plus shared context.js:
const { TASKS_FILE, writeJSON, readJSON, generateId } = require('./context');
module.exports = function(app) {
app.get('/api/tasks', (req, res) => { ... });
app.post('/api/tasks', (req, res) => { ... });
};Alembic initialization
The alembic/ directory already existed with env.py and an empty versions/ folder. Everything was set up but never used. Generated the baseline migration for all 47 models. Future schema changes go through alembic revision --autogenerate instead of adding another hand-rolled migration function.
During this work, I discovered a crash bug: _migrate_relationships_romance(conn) was called on every startup but the function was never defined. Any new SQLite deployment would hit NameError. Hidden because existing databases already had the columns.
CI and infrastructure hardening
- Added GitHub Actions workflow to home-ops:
kustomize buildvalidation on push/PR - Added 14 tests for openclaw MC using Node’s built-in test runner (zero dependencies)
- Added
app.kubernetes.io/*standard labels to all 15 app deployments - Resolved 4 egress policy TODOs (3 confirmed DNS-only, 1 added HTTPS to
push.bitwarden.com) - Bounded Traefik from
>=34.0.0to34.x - Added
readOnlyRootFilesystem: trueto it-tools - Fixed miniflux backup CronJob
restartPolicy
Bugs Found Along the Way
The refactoring surfaced four bugs invisible during normal usage:
Task card clicks not opening the detail panel
wireEvents() was called after every render(), but two addEventListener calls targeted el directly. Since el.innerHTML = ... only replaces children (el itself stays in the DOM), delegated listeners accumulated. Each card click fired N times, toggling selectedTaskId on and off.
// Fix: AbortController removes previous listeners
if (wireAbort) wireAbort.abort();
wireAbort = new AbortController();
const sig = { signal: wireAbort.signal };
el.addEventListener('click', (e) => {
// card selection logic
}, sig);Pages hijacked by task board refresh
tasks.js had setInterval timers that kept running after navigating away. The guard if (!document.contains(el)) never triggered because el is document.getElementById('content'), always in the DOM. Only its innerHTML changes.
// Fix: cleanup registry in the SPA router
const _pageCleanups = [];
function onPageLeave(fn) { _pageCleanups.push(fn); }
function navigate(path, push = true) {
while (_pageCleanups.length) _pageCleanups.pop()();
// ... load next page
}The missing migration function
_migrate_relationships_romance(conn) was called but never defined. A latent crash for any new deployment.
Cross-layer coupling
council.py (API router) imported scope/cooldown helpers directly from mcp_server.py. Extracted to a shared council_helpers.py module.
Lessons Learned
Background AI agents expand beyond their scope. I launched three parallel agents to extract routes from main.py. One also gutted htmx_routes.py (7,492 → 20 lines) and mcp_server.py (9,341 → 39 lines), created unauthorized modules, changed test dates across 15 files, and committed it all. Three separate git checkout -- restoration sessions. Always diff before committing after agents complete.
Import sorting is the #1 CI friction point. Ruff I001 failures caused three re-push cycles. A five-line git pre-commit hook fixed it permanently.
CSS attribute selectors can override inline styles. Many SPA modules use inline style="display:flex;gap:24px" which media queries can’t reach, unless you use [style*="gap:24px"] with !important. Unconventional but effective for retrofitting responsive behavior without touching JS.
The weekend logging gap is structural, not motivational. Saturday habit completion at 25% vs 100% weekdays. The Character pillar sat at 1.0/10 not because effort wasn’t happening, but because it wasn’t being recorded. Missing a cue, not missing motivation.
The Meta-Pattern
The most interesting finding wasn’t any individual weakness. It was the pattern across all of them. Every strength was an architectural decision made upfront. Every weakness was deferred maintenance.
This is characteristic of a solo engineer who builds things right but doesn’t always circle back. The architecture is sound; the guardrails to keep it that way as it grows are what’s missing.
The fix isn’t more features. It’s the boring stuff: CI pipelines, pre-commit hooks, periodic refactoring sprints, and, most importantly, the discipline to run the audit in the first place.
The whole session produced 145 commits across 3 repos. Every weakness addressed except one (ORM foreign keys: careful migration for another day). Two monoliths split. Fourteen tests where there were zero. A crash bug found and fixed before it ever hit a user.
Not bad for asking an AI to be honest about your code.