{"slug": "after-the-sprint-a-72-hour-build-retrospective-spoiler-it-wasn-t-secure", "title": "After the Sprint: A 72-Hour Build Retrospective - Spoiler: It Wasn't Secure", "summary": "A developer built Charitas Clew, a web app that uses Gemini to translate bureaucratic documents into plain language, during a 72-hour challenge, then conducted a post-submission security audit with AI assistants Antigravity and Dr. Kahlo. The audit replaced a basic prompt-injection keyword filter with stronger structural boundaries and expanded the test suite from zero to 143 passing tests.", "body_md": "I built [Charitas Clew](https://charitas-clew.web.app/) for a 72-hour DEV Weekend Challenge. It worked. I submitted it. And then I did something slightly inconvenient: I kept poking at it.\n\nApparently one day is enough historical distance from an event to call\n\nwhat follows a \"retrospective,\" so here we are. lol \n\nCharitas Clew takes the sort of bureaucratic notice that can ruin an\n\notherwise perfectly good afternoon---a benefits letter, utility notice,\n\ncourt document, hospital bill---and uses Gemini to turn it into plainer\n\nlanguage, dates that may need attention, concrete next steps, and a\n\nspeaking script for the phone call someone may need to make next.\n\nThe challenge version did those things. It was live. It wasn't\n\ncollapsing under its own weight. I had built what I intended to build in\n\na weekend and submitted it before the deadline.\n\nThen came the uncomfortable question:\n\n**Working according to whom?**\n\nI didn't do the retrospective alone.\n\nMy role throughout this project has been the same role I usually occupy\n\nin AI-assisted development: define what the product should do, direct\n\nthe agents doing implementation work, interrogate the results, and\n\ndecide whether the evidence is good enough to accept.\n\nFor the post-submission hardening, I worked with two AI systems in\n\ndifferent roles. **Antigravity using Gemini 3.8 Flash** worked directly against the codebase: implementing changes, running tests, inspecting deployment behavior, and---critically---opening a browser and exercising the live application. **[Dr. Kahlo](https://chatgpt.com/g/g-68af555e39808191a53fcd1ef6451fda-dr-kahlo), my custom ChatGPT code-review and QA assistant**, took the adversarial review side: questioning findings, challenging proposed fixes, catching overclaims, setting the next audit boundary, and repeatedly asking some variation of, \"Yes, but what does that actually prove?\"\n\nI was the human in the loop deciding what got changed, what didn't, and\n\nwhen the evidence was sufficient.\n\nThat distinction matters because this isn't a story about an AI\n\nmagically securing another AI's code.\n\nIt's a story about using agents to **challenge the work other agents helped produce**, while keeping a human responsible for the decisions.\n\nThe first problem was obvious: the sprint version had no automated test\n\nsuite. Before changing security behavior, Antigravity added a minimal\n\nNode/Supertest regression harness so fixes could be checked against\n\nexisting behavior. The first 19 tests immediately uncovered an unrelated\n\nmalformed-input hang.\n\nBy the final audit, there were 143 passing tests.\n\nThat number becomes relevant later.\n\nNot because 143 is magical.\n\nBecause production still managed to surprise us.\n\nThe sprint version had a prompt-injection filter.\n\nIt looked for phrases such as \"ignore previous instructions,\" \"disregard\n\nprior rules,\" `system:`, and other strings commonly associated with\n\nattempts to manipulate a model. The filter was there but basic, very basic. \n\nThat sounds responsible until you remember what Charitas reads.\n\nBureaucracies also use phrases like \"disregard all prior notices.\"\n\nDocuments contain instructions. Government paperwork refers to systems.\n\nA legitimate notice can look suspicious to a keyword filter while a\n\nmildly creative attacker can simply phrase an instruction differently.\n\nSo during the audit, we stopped asking, \"How can we make the blacklist\n\nsmarter?\" and asked a better question:\n\n**Why is a blacklist responsible for this boundary at all?**\n\nIt wasn't.\n\nThe hardening removed the keyword gate and strengthened the structural\n\nboundary instead. Application-controlled instructions stayed separate\n\nfrom uploaded document content. The document was explicitly treated as\n\nuntrusted source material. Request fields were validated before entering\n\nthe model path.\n\nThe other half of that boundary was just as important.\n\nCharitas already asked Gemini for structured JSON, but requesting a\n\nschema from a model is not the same thing as validating what comes back.\n\nRuntime validation was added so types, lengths, required fields,\n\naction-step structure, deadline information, and unexpected properties\n\nhad to satisfy the application's rules before the response could reach\n\nthe browser.\n\nThe lesson wasn't that prompt injection had been \"solved.\" We were\n\ncareful not to make that claim.\n\nIt was that **security controls should enforce boundaries, not recognize scary vocabulary.**\n\nAnd model output is still input.\n\nThis was the finding that changed how I thought about the whole\n\nexercise.\n\nBy this point, the application had a substantial automated suite. Input\n\nvalidation had been tightened. Model output had a runtime contract.\n\nUnsafe rendering had been removed. Error behavior, retries, timeouts,\n\ndeployment headers, and other boundaries had regression coverage.\n\nThen Antigravity opened the deployed application.\n\nThis capability turned out to be one of the most valuable parts of the\n\nhardening process because we weren't limited to asking what the source\n\ncode *should* do. Antigravity could use the live Firebase-hosted\n\napplication: load the page, submit notices, switch languages, test\n\nuploads, generate results, and exercise sharing, printing, and\n\ntext-to-speech. We could also inspect the deployed headers and watch\n\nwhat happened across the real Firebase-to-Cloud-Run request path.\n\nMost of it worked exactly as expected.\n\nThe rate limiter did not.\n\nThe Express application had been configured with:\n\n```\napp.set('trust proxy', 1);\n```\n\nThat setting depended on an assumption about how many trusted network\n\nhops existed between the user and the application.\n\nOur assumption was wrong.\n\nThe production request traveled through Firebase Hosting and Google\n\ninfrastructure before reaching Cloud Run. The resulting\n\nforwarded-address chain meant Express could identify a proxy address as\n\nthe client instead of the actual originating user.\n\nFor an IP-based rate limiter, that's not a cosmetic error. Different\n\nusers can collide into a shared quota, while the same user's apparent\n\nidentity can vary across proxy paths.\n\nThe automated tests hadn't lied. They were correctly testing the network\n\nmodel we had given them.\n\n**The network model was wrong.**\n\nAntigravity inspected the live behavior, the proxy trust logic was\n\nnarrowed around the actual deployment boundary, and the application was\n\nredeployed. Live verification then tested multiple client paths and\n\nspoofed `X-Forwarded-For` values to make sure we hadn't \"fixed\" one\n\nproblem by creating an easier spoofing path.\n\nThat produced my favorite lesson from the entire retrospective:\n\n**Tests can prove behavior inside the world you modeled. Production can tell you that you modeled the wrong world.**\n\nThe final audit still records a limitation: rate-limit counters are held\n\nin memory per Cloud Run instance rather than globally synchronized. For\n\nthe current scale of a small stateless application, we accepted that\n\ninstead of adding distributed infrastructure merely because we knew how.\n\nSometimes knowing what **not** to build is part of the review.\n\nCharitas handles documents people may not want hanging around: court\n\nnotices, benefits letters, bills, housing paperwork, and other\n\npotentially sensitive material.\n\nDuring the privacy pass, the audit found that generated notice\n\ninformation was being persisted in browser `localStorage`.\n\nThere had once been an idea for restoring a previous result. That\n\nfeature wasn't meaningfully part of the application anymore.\n\nThe storage was.\n\nThis created one of those moments where engineering sophistication can\n\nbecome its own trap. We could have discussed encryption. We could have\n\ncreated sessions. We could have added a database and retention policies.\n\nWe could have transformed a small stateless application into a\n\nsignificantly larger security problem in the name of solving the smaller\n\none.\n\nInstead, the review asked:\n\n**Why are we keeping this data at all?**\n\nThere wasn't a good answer.\n\nSo Antigravity removed the persistence. Sensitive notice content now\n\nremains in application memory for the active session rather than being\n\nrestored from persistent browser storage. The application also cleans up\n\nthe legacy storage key from earlier versions. Only a non-sensitive\n\nlanguage preference remains persistent.\n\nThat is considerably less impressive on an architecture diagram.\n\nIt is also the design I trust more.\n\n**Sometimes the strongest data-protection feature is not having the data.**\n\nThose were the three findings worth telling as stories. They weren't the\n\nonly things we found.\n\nThe complete hardening review covered the application from browser\n\nrendering through model invocation and deployment behavior. By the final\n\naudit, all 17 findings from the original review had an explicit\n\ndisposition rather than quietly disappearing from a checklist.\n\nA few of those changes were technically small but important.\n\nModel-controlled values stopped reaching unsafe HTML rendering paths.\n\nUploads gained stricter MIME, base64, size, and file-signature\n\nvalidation. Transient frontend failures stopped masquerading as\n\npermanent shutdowns. Security headers were aligned across Firebase\n\nHosting and the Express backend. Secret handling was verified against\n\nthe deployed environment rather than inferred from the repository.\n\nOne change wasn't strictly a security fix at all.\n\nCharitas extracts dates from notices, but a date printed on a document\n\nis not automatically *the legal deadline*. Deadlines can depend on\n\nservice dates, receipt dates, procedural rules, statutes, or facts that\n\naren't present in one uploaded page.\n\nThe hardening therefore changed the product language too. Extracted\n\ndates are treated as evidence from the document, while users are\n\nreminded to confirm actual deadlines with authoritative sources.\n\nThat was an important reminder that hardening an AI product isn't only\n\nabout preventing malicious behavior.\n\nSometimes you have to harden **what the product is allowed to claim**.\n\nI'm going to resist answering that with \"yes.\"\n\nThe final Antigravity audit gave Charitas Clew a 9.0/10 production-readiness score and a verdict of:\n\n**APPROVED FOR CURRENT SCOPE.**\n\nI care more about those last three words than I do about the number.\n\nThe application still has accepted limitations. Rate limiting is per\n\nCloud Run instance. The proxy-trust configuration carries maintenance\n\ndebt because the network ranges it relies on can change. Documents\n\nnecessarily cross an external model-inference boundary. There are\n\nsecurity improvements that could still be made.\n\nThose aren't forgotten fixes. They're documented trade-offs.\n\nSecurity work has no natural finish line. There is always another\n\nscanner, dependency, service, abstraction, test, policy, or hypothetical\n\nscale problem available to consume an afternoon. Eventually, \"hardening\"\n\na 72-hour project stops being responsible maintenance and starts\n\nbecoming an elaborate way to build a different application.\n\nSo there is no Phase 7.\n\nI cannot believe I just wrote that sentence about something I started\n\nbuilding on Friday.\n\nThe sprint proved that Charitas could work.\n\nThe retrospective asked whether the boundaries around that working\n\nproduct could survive harder questions. Some couldn't. We changed them.\n\nThen we tested again---including against the deployed system rather than\n\nonly the system we imagined we had built.\n\nThat's a different standard from \"it runs.\"\n\nFor an application people may trust with documents that matter, I think\n\nit should be.\n\n*Charitas Clew was built and hardened with substantial AI assistance. I directed the product, review criteria, trade-offs, and acceptance\ndecisions; Antigravity worked directly with the codebase and live\ndeployment; Dr. Kahlo, my custom ChatGPT QA and code-review assistant,\nhelped structure and challenge the post-submission audit. The hardening\ndescribed here occurred after the DEV Weekend Challenge submission and\nis not represented as challenge-period work.*\n\n**AI Assisted. Human Approved. Powered by NLP.**", "url": "https://wpnews.pro/news/after-the-sprint-a-72-hour-build-retrospective-spoiler-it-wasn-t-secure", "canonical_source": "https://dev.to/earlgreyhot1701d/after-the-sprint-a-72-hour-build-retrospective-surprise-it-wasnt-secure-4bd", "published_at": "2026-09-08 01:28:58+00:00", "updated_at": "2026-09-08 02:00:50.584866+00:00", "lang": "en", "topics": ["generative-ai", "ai-agents", "ai-safety", "developer-tools"], "entities": ["Charitas Clew", "Gemini", "Antigravity", "Dr. Kahlo", "ChatGPT"], "alternates": {"html": "https://wpnews.pro/news/after-the-sprint-a-72-hour-build-retrospective-spoiler-it-wasn-t-secure", "markdown": "https://wpnews.pro/news/after-the-sprint-a-72-hour-build-retrospective-spoiler-it-wasn-t-secure.md", "text": "https://wpnews.pro/news/after-the-sprint-a-72-hour-build-retrospective-spoiler-it-wasn-t-secure.txt", "jsonld": "https://wpnews.pro/news/after-the-sprint-a-72-hour-build-retrospective-spoiler-it-wasn-t-secure.jsonld"}}