I used AI. It worked. I hated it.
I used Claude Code to build a tool I needed. It worked great, but I was miserable. I need to reckon with what it means.
'm as anti-genAI as it gets. And yet, this past month, I have used generative coding to complete a project. It works. I hated making it.
<!--more-->
The Apologia
Let's get this out of the way: my feelings about using generative models at all are...fraught. And if you are ready to call me a monster or a hypocrite right now, I understand. I'm navigating some tensions about this and I fully own that I may have made the wrong choices here.
My actual day job, the one that puts food on the table for my family, has metamorphosed into an "AI security expert" role, in which I am not only responsible for testing AI-enabled applications, but I am also expected to be an expert in their operation. I hope, perhaps naively, that standing between these applications and deployment, I can do whatever is possible to make them safer—and to say "no" as loudly as I can to ideas that are too dangerous for production. I can't do any of this without using and knowing these tools intimately.
I nevertheless recognize the societal and environmental harms posed by these tools. I want them to unexist. I even recognize the cognitive hazards to which I expose myself in their use (more on that later). I do not want to use them. And yet, I must understand them. If that damns me in your eyes, so be it.
That suffices, I hope, for the self-flagellation component of our proceedings.
The Project
I've been migrating The Taggart Institute off two commercial platforms—Teachable and Discord—at the same time. Doing so has required bending Discourse, our new platform of choice, into something resembling a Learning Management System. Discourse is forum software, and it forums about as well as anything I've ever used. It's actually kind of fun to administer! And owing to its flexibility and extensibility, Discourse mostly does what we need of a learning platform—with a few tweaks and remaining quirks. But one feature remained stubbornly difficult to reproduce: course completion certificates. Although our policy is to honor self-assessment, our community of learners has demonstrated their desire for these documents. Our primary source of new learners is LinkedIn, and LinkedIn people are always thrilled to announce their accomplishments.
Do I think LinkedIn is the digital River Styx, where damned souls clamber over each other and claw at the boat passing overhead in the dim hope of salvation from those who have escaped the shambling horde? I do. But if we're all in hell together, we might as well try to lift each other up.
Teachable, which now costs an arm and a leg, keeps AI-ifying, and has a shocking security issue that I can't yet disclose (stay tuned!), has a very well-implemented certificate generator. That is one comfort I sacrifice in this move. No existing solution I could find met my needs exactly. But therein was an opportunity to build my own certificate generator—perhaps even an open source general solution that anyone who needs publicly verifiable certificates could use.
The challenge: I was already swamped with actual work, the other migration efforts, and a brand new project taking a lot of free time. Most importantly though, my free time is minimal because I'm trying to be a good dad to this amazing toddler. I can't code all day and night like I used to. Such is life.
So on the one hand, I have to understand genAI coding tools for work. On the other, here's this missing feature I need implemented to complete the TTI migration. I decided to test development using Claude Code for this project.
If it works, I'll have my certificate solution, I thought. If it doesn't, at least I'll know more about the technology and its implications.
Well, spoiler alert: it works. It's even, near as I can tell, reasonably secure. But good lord, building this way was miserable, even if it was faster than coding it all myself.
The Design
My idea was fairly simple: a webhook interceptor that received course completion details (student name, email, and course name), and generated a PDF certificate with a unique/verifiable ID. The certificate would be emailed to the user, and the cert itself would contain a QR code linking to a verification page on the app. This could be as simplistic as a Python Flask app that calls out to some shell scripts, or as complicated as...what it turned out to be. But I have to admit, in the planning process, the model's suggestions for creature comforts were rather appealing, so I decided to roll with them. If I decided I hated it, there was nothing stopping me from blowing all the code away and rebuilding a lighter solution.
The Process
Before setting off on this misadventure, I tried reading as much as I could about "best practices" in using AI coding tools. I'm purposefully avoiding the term "vibe coding," because that's not really what I was pursuing here. In fact, the single most common kernel of wisdom I received was to proceed in such a way that maximized your chances of keeping the model on-task and within expected parameters. That's not just about writing prompts, but building in such a way that the model's output at any given time has deterministic measures of success or failure, and that it keeps track of its own progress with external context.
Planning
Claude Code has a "Plan Mode" which is critical to how Anthropic themselves believe the tool is best used to build new projects. Essentially, the model's instructions shift from beginning with writing code to writing a plan for itself, which becomes a semi-permanent part of its context to guide changes. That's how I began every new feature in the project. I also prompted the model to output the plan to an external Markdown file for future reference (context disappears over time). I also maintained a TASKS.md file that I used to track features already implemented and yet to come. In fact, almost all the hands-on-keyboard time for myself in this project was in Markdown syntax. I love me some Markdown, but it didn't feel great writing for a model as an audience instead of human beings.
Technologies
To nobody's surprise, I chose to build the application in Rust, but not just because I'm a Rust dork. I know the language well, so it would be easier for me to see mistakes, anti-patterns, and code smells. Moreover, it was my hope that Rust's built-in safety features (type safety, compile-time checks, robust testing) would aid in keeping the model on track. The frontend was built with Svelte, in part because I'd always wanted to do a Svelte project, and in part because I knew the mostly HTML syntax would be easier to debug than React or Next.js.
The PDF generation is probably my favorite part. I use Typst templates, accessed via the Typst API, to generate the PDFs. This makes it simple to change templates later. Also, I'm kind of becoming a Typst nerd?
For the model/coding agent, I used Claude Code with Sonnet 4.6. I've experimented a lot with Ollama and open-weight models, but I wanted the same experience as developers who are swearing up and down about this revolution in their work.
Methodology
To maximize determinism, each step of the build used test-driven development (TDD). Using the Markdown planning file as a starting point, the model generated tests for functions that would define the features, then implemented each in turn. After each coding round, cargo check and cargo test were run to confirm compilation and test passing. I reviewed every line of code the model generated. For initial drafts, very little had to change. Now to be fair, this is not a particularly complex app. It's basic CRUD app with some specialized requirements. Still, getting it all right, including auth and data handling, really mattered.
After the initial drafting phase, I went through the entire app and made a list of tasks for improvement/change in the codebase. This TODO.md became the new starting point for model context in plan creation.
Unexpectedly, as items were addressed in the document, the model updated the file with checkmarks and details of implementations. This was not an instruction I gave the model, but it was behavior I liked, since it created a trail of accountability.
After all the features I wanted were functional, context was cleared entirely and new instructions were provided to the model. Instead of acting as a software developer, I instructed the model to perform as a security auditor and secure code expert, finding vulnerabilities in the code and recommending remediations. The findings would be written to a FINDINGS.md file, keeping with our established "Plan, Document, Execute, Log" pattern established in earlier rounds.
Results
Here's the part you actually wanted. How did it all go? How did I feel about it?
Mad at Me? Read This
This is where we need to be grown-ups and entertain some seemingly contradicting ideas at the same time, okay? I'm going to be talking about what worked and what didn't, and how it all felt. What I am not doing is endorsing this technology or hand-waving away the significant legal and ethical issues with its use. My position on its danger to society has not changed, but my understanding of its capacity for software development has. I don't think fast code creation is worth the world, any more than wax fruit.
Okay, on to the results.
Functionality
Well, the thing works. The code is in production today, serving certificates for TTI. The only direct changes I made to the codebase were for elegance. The core logic was solid from the jump, owing I believe as much to Rust's safeties in development as to the model's capabilities.
You can review the code here. I intentionally put the link down here so interested readers were more likely to find it.
The application I ended up with is far more robust and feature-filled than what I would have built on my own. I have to acknowledge that. Audit logging, GDPR data deletion, cryptographic verification of uploads, optional HMAC for incoming webhooks—I probably would not have both
Did you enjoy this article?
Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.