DEV Community

Cover image for After the Sprint: A 72-Hour Build Retrospective - Spoiler: It Wasn't Secure
Earl Grey
Earl Grey Subscriber

Posted on AI-assisted

After the Sprint: A 72-Hour Build Retrospective - Spoiler: It Wasn't Secure

I built Charitas Clew for a 72-hour DEV Weekend Challenge. It worked. I submitted it. And then I did something slightly inconvenient: I kept poking at it.

Apparently one day is enough historical distance from an event to call
what follows a "retrospective," so here we are. lol

Charitas Clew takes the sort of bureaucratic notice that can ruin an
otherwise perfectly good afternoon---a benefits letter, utility notice,
court document, hospital bill---and uses Gemini to turn it into plainer
language, dates that may need attention, concrete next steps, and a
speaking script for the phone call someone may need to make next.

The challenge version did those things. It was live. It wasn't
collapsing under its own weight. I had built what I intended to build in
a weekend and submitted it before the deadline.

Then came the uncomfortable question:

Working according to whom?

The Sprint Ended. The Audit Started.

I didn't do the retrospective alone.

My role throughout this project has been the same role I usually occupy
in AI-assisted development: define what the product should do, direct
the agents doing implementation work, interrogate the results, and
decide whether the evidence is good enough to accept.

For the post-submission hardening, I worked with two AI systems in
different 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, 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?"

I was the human in the loop deciding what got changed, what didn't, and
when the evidence was sufficient.

That distinction matters because this isn't a story about an AI
magically securing another AI's code.

It's a story about using agents to challenge the work other agents
helped produce
, while keeping a human responsible for the decisions.

The first problem was obvious: the sprint version had no automated test
suite. Before changing security behavior, Antigravity added a minimal
Node/Supertest regression harness so fixes could be checked against
existing behavior. The first 19 tests immediately uncovered an unrelated
malformed-input hang.

By the final audit, there were 143 passing tests.

That number becomes relevant later.

Not because 143 is magical.

Because production still managed to surprise us.

Finding One: My Prompt-Injection Defense Was Mostly Theater

The sprint version had a prompt-injection filter.

It looked for phrases such as "ignore previous instructions," "disregard
prior rules," system:, and other strings commonly associated with
attempts to manipulate a model. The filter was there but basic, very basic.

That sounds responsible until you remember what Charitas reads.

Bureaucracies also use phrases like "disregard all prior notices."
Documents contain instructions. Government paperwork refers to systems.
A legitimate notice can look suspicious to a keyword filter while a
mildly creative attacker can simply phrase an instruction differently.

So during the audit, we stopped asking, "How can we make the blacklist
smarter?" and asked a better question:

Why is a blacklist responsible for this boundary at all?

It wasn't.

The hardening removed the keyword gate and strengthened the structural
boundary instead. Application-controlled instructions stayed separate
from uploaded document content. The document was explicitly treated as
untrusted source material. Request fields were validated before entering
the model path.

The other half of that boundary was just as important.

Charitas already asked Gemini for structured JSON, but requesting a
schema from a model is not the same thing as validating what comes back.
Runtime validation was added so types, lengths, required fields,
action-step structure, deadline information, and unexpected properties
had to satisfy the application's rules before the response could reach
the browser.

The lesson wasn't that prompt injection had been "solved." We were
careful not to make that claim.

It was that security controls should enforce boundaries, not recognize
scary vocabulary.

And model output is still input.

Finding Two: 143 Tests Later, Production Still Got a Vote

This was the finding that changed how I thought about the whole
exercise.

By this point, the application had a substantial automated suite. Input
validation had been tightened. Model output had a runtime contract.
Unsafe rendering had been removed. Error behavior, retries, timeouts,
deployment headers, and other boundaries had regression coverage.

Then Antigravity opened the deployed application.

This capability turned out to be one of the most valuable parts of the
hardening process because we weren't limited to asking what the source
code should do. Antigravity could use the live Firebase-hosted
application: load the page, submit notices, switch languages, test
uploads, generate results, and exercise sharing, printing, and
text-to-speech. We could also inspect the deployed headers and watch
what happened across the real Firebase-to-Cloud-Run request path.

Most of it worked exactly as expected.

The rate limiter did not.

The Express application had been configured with:

app.set('trust proxy', 1);
Enter fullscreen mode Exit fullscreen mode

That setting depended on an assumption about how many trusted network
hops existed between the user and the application.

Our assumption was wrong.

The production request traveled through Firebase Hosting and Google
infrastructure before reaching Cloud Run. The resulting
forwarded-address chain meant Express could identify a proxy address as
the client instead of the actual originating user.

For an IP-based rate limiter, that's not a cosmetic error. Different
users can collide into a shared quota, while the same user's apparent
identity can vary across proxy paths.

The automated tests hadn't lied. They were correctly testing the network
model we had given them.

The network model was wrong.

Antigravity inspected the live behavior, the proxy trust logic was
narrowed around the actual deployment boundary, and the application was
redeployed. Live verification then tested multiple client paths and
spoofed X-Forwarded-For values to make sure we hadn't "fixed" one
problem by creating an easier spoofing path.

That produced my favorite lesson from the entire retrospective:

Tests can prove behavior inside the world you modeled. Production
can tell you that you modeled the wrong world.

The final audit still records a limitation: rate-limit counters are held
in memory per Cloud Run instance rather than globally synchronized. For
the current scale of a small stateless application, we accepted that
instead of adding distributed infrastructure merely because we knew how.

Sometimes knowing what not to build is part of the review.

Finding Three: The Best Privacy Architecture Was a Delete Key

Charitas handles documents people may not want hanging around: court
notices, benefits letters, bills, housing paperwork, and other
potentially sensitive material.

During the privacy pass, the audit found that generated notice
information was being persisted in browser localStorage.

There had once been an idea for restoring a previous result. That
feature wasn't meaningfully part of the application anymore.

The storage was.

This created one of those moments where engineering sophistication can
become its own trap. We could have discussed encryption. We could have
created sessions. We could have added a database and retention policies.
We could have transformed a small stateless application into a
significantly larger security problem in the name of solving the smaller
one.

Instead, the review asked:

Why are we keeping this data at all?

There wasn't a good answer.

So Antigravity removed the persistence. Sensitive notice content now
remains in application memory for the active session rather than being
restored from persistent browser storage. The application also cleans up
the legacy storage key from earlier versions. Only a non-sensitive
language preference remains persistent.

That is considerably less impressive on an architecture diagram.

It is also the design I trust more.

Sometimes the strongest data-protection feature is not having the
data.

The Rest of the Audit

Those were the three findings worth telling as stories. They weren't the
only things we found.

The complete hardening review covered the application from browser
rendering through model invocation and deployment behavior. By the final
audit, all 17 findings from the original review had an explicit
disposition rather than quietly disappearing from a checklist.

A few of those changes were technically small but important.
Model-controlled values stopped reaching unsafe HTML rendering paths.
Uploads gained stricter MIME, base64, size, and file-signature
validation. Transient frontend failures stopped masquerading as
permanent shutdowns. Security headers were aligned across Firebase
Hosting and the Express backend. Secret handling was verified against
the deployed environment rather than inferred from the repository.

One change wasn't strictly a security fix at all.

Charitas extracts dates from notices, but a date printed on a document
is not automatically the legal deadline. Deadlines can depend on
service dates, receipt dates, procedural rules, statutes, or facts that
aren't present in one uploaded page.

The hardening therefore changed the product language too. Extracted
dates are treated as evidence from the document, while users are
reminded to confirm actual deadlines with authoritative sources.

That was an important reminder that hardening an AI product isn't only
about preventing malicious behavior.

Sometimes you have to harden what the product is allowed to claim.

So, Is It Secure Now?

I'm going to resist answering that with "yes."

The final Antigravity audit gave Charitas Clew a 9.0/10 production-readiness score and a verdict of:

APPROVED FOR CURRENT SCOPE.

I care more about those last three words than I do about the number.

The application still has accepted limitations. Rate limiting is per
Cloud Run instance. The proxy-trust configuration carries maintenance
debt because the network ranges it relies on can change. Documents
necessarily cross an external model-inference boundary. There are
security improvements that could still be made.

Those aren't forgotten fixes. They're documented trade-offs.

Security work has no natural finish line. There is always another
scanner, dependency, service, abstraction, test, policy, or hypothetical
scale problem available to consume an afternoon. Eventually, "hardening"
a 72-hour project stops being responsible maintenance and starts
becoming an elaborate way to build a different application.

So there is no Phase 7.

I cannot believe I just wrote that sentence about something I started
building on Friday.

The sprint proved that Charitas could work.

The retrospective asked whether the boundaries around that working
product could survive harder questions. Some couldn't. We changed them.
Then we tested again---including against the deployed system rather than
only the system we imagined we had built.

That's a different standard from "it runs."

For an application people may trust with documents that matter, I think
it should be.


Charitas Clew was built and hardened with substantial AI assistance. I
directed the product, review criteria, trade-offs, and acceptance
decisions; Antigravity worked directly with the codebase and live
deployment; Dr. Kahlo, my custom ChatGPT QA and code-review assistant,
helped structure and challenge the post-submission audit. The hardening
described here occurred after the DEV Weekend Challenge submission and
is not represented as challenge-period work.

AI Assisted. Human Approved. Powered by NLP.

Top comments (0)