Skip to main content
← Blog

2026-08-23

·

7 min read

An AI Agent I Did Not Have to Trust

I have years of photos on my Mac, most of them five or six near-identical takes of the same moment. I wanted an AI agent to cull the duplicates. I also did not want to give an AI agent the ability to delete a single photo, ever, no matter how confident it was.

Those two things sound like they are in tension. They are not, if you stop treating "do not delete anything" as an instruction and start treating it as a build constraint.

Enforcement, not a system prompt

Every AI coding agent gets told what not to do. Almost none of that is actually checked. It is a paragraph in a config file the model reads once and, under enough pressure from a plausible-looking task, can talk itself past.

So the real engineering goal for this project, photocull (a local-only macOS tool that culls an Apple Photos library down to keepers), was never "cull photos well." It was "make deletion structurally impossible for the thing doing the culling."

The mechanism is a test, not a rule. It walks the syntax tree of every source file and fails the build on any call or attribute named delete, remove, erase, unlink, rmtree, trash, or destroy, anywhere in the codebase, with an empty allowlist. There is no "avoid these unless necessary" clause. Zero exceptions, ever. The complete set of mutations the app is allowed to make to a Photos library is: set favorite, add to an album, set or add a keyword. Nothing else exists anywhere in the tree, because the test will not let it exist.

It goes one step further. The same test also greps every comment and string for three of those words (delete, remove, erase) and refuses to let one through unless there is a written justification logged for it, right there in the test file. A comment cannot actually delete anything, but a stray, unexamined mention of "delete" in a docstring is exactly the kind of thing that is one bad refactor away from becoming a call. The gate makes you explain yourself before it lets a word through, not just a function.

The gap between three verbs for prose and seven for actual calls is deliberate, not sloppy, and it is also where the agent caught its own documentation lying about itself. More on that below.

A pattern, not a vibe

None of this happened because I asked nicely and got lucky. The whole project ran on a repeatable pattern: one task per session, and nothing gets marked done without the command output that proves it, not a claim that something "should work." Every real mutation against my actual library, the first time it wrote a favorite, the first time it wrote an album, the first time it read an original file instead of a cached thumbnail, needed my explicit sign-off before it ran unattended again. The code being tested was not the point. "Tested" and "safe with someone's real, irreplaceable data" are different questions.

The part where it was wrong

Here is the story I actually want to tell, because "the agent did everything right" is not a very useful one.

Partway through, I asked for something simpler than the print-album feature I had originally specified: just group my trip photos into normal Photos.app albums, no crop, no export, so I could browse and print them myself. Built, tested, run for real: 81 albums, nearly 2,000 photos, verified independently against my actual library, not just trusted from a report.

Then I asked about a follow-up detail (adding location names to the album titles), and while scoping that, we found the feature had quietly been ignoring 83% of my library the whole time. It only ever looked at photos that had once been part of a near-duplicate cluster; anything that was simply a one-off, non-duplicated photo had never been considered at all. Worse, I noticed one of the albums, titled for a trip to Spain, had a photo taken in Greenwich sitting inside it. Date-based grouping had chained two unrelated trips together because the gap between them was short enough. That was proof the whole grouping strategy could be flatly wrong, not just imprecise.

So I killed it. Told the agent to remove every line of that feature, and it did: the CLI command, the new module, the one narrow exception it had carved into a safety allowlist to support the feature, all gone in a follow-up commit. The test suite came back to exactly the count it had before the feature ever existed, which is how I know nothing else moved. I deleted the 81 albums myself, by hand, in Photos.app, because that is the one thing the app was never able to do in the first place.

Nothing was lost, and that is the point. Being wrong here cost one afternoon and a git revert, because the one truly irreversible action (deleting a photo) was never on the table to begin with. Every other mistake was just data, and data you can undo.

A second pass caught what the first one missed

Before any of this went public, I ran it through a separate, adversarial multi-agent review: a different set of eyes, deliberately not primed by the same conversation that wrote the code. It caught two real problems. The spec docs claimed a UI zoom feature capped magnification at "one source pixel per device pixel," which contradicted what the rendering code actually does, a genuine doc-and-code mismatch on any screen with a scaling factor above 1x, not just stale wording. And the README claimed a safety test "catches those words in comments and strings," implying all seven banned verbs, when the prose check only covers three. Small, but exactly the kind of overclaim you do not want sitting in a document whose entire job is telling a stranger what your safety guarantees are.

Neither of those would have been caught by the same process that wrote the original claim. That is why the review was a separate pass, not a second read-through by the same context.

None of this required trusting the agent's judgment about what is safe. It required building a system where the unsafe action did not exist as an option, and then checking, repeatedly, from more than one angle, that the rest of it did what it claimed. I would rather have a test that makes the bad outcome impossible than an agent that is merely well-behaved. Good behaviour is something you hope for. A failing build is something you can check.

photocull is on GitHub if you want to see the guardrail test itself, or the exact commits from the story above.

We help engineering teams get real work done with AI agents, past the demo, past the first context window. Start with an audit and we will tell you what is worth automating for your team.

Book a free consultation →