<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thomas Simmer</title>
    <description>The latest articles on DEV Community by Thomas Simmer (@thomassimmer).</description>
    <link>https://dev.to/thomassimmer</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3910457%2F1ac04df2-dcca-42d4-a6b3-6038dd6abab6.jpeg</url>
      <title>DEV Community: Thomas Simmer</title>
      <link>https://dev.to/thomassimmer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thomassimmer"/>
    <language>en</language>
    <item>
      <title>Coding with AI: The Investigator Method</title>
      <dc:creator>Thomas Simmer</dc:creator>
      <pubDate>Wed, 23 Sep 2026 05:48:01 +0000</pubDate>
      <link>https://dev.to/thomassimmer/coding-with-ai-the-investigator-method-5chi</link>
      <guid>https://dev.to/thomassimmer/coding-with-ai-the-investigator-method-5chi</guid>
      <description>&lt;p&gt;Before I heard about OpenAI, I would never have imagined programming to become what it is today. These are some notes I wrote to get a clearer view of my hobby and my job.&lt;/p&gt;

&lt;p&gt;It has been more than a year since I integrated AI into my code editor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikb2b3q2sgk5xyo89cl4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikb2b3q2sgk5xyo89cl4.webp" alt="Zed editor with my [Cyberpunk theme](https://github.com/thomassimmer/cyberpunk-2077-zed-extension)&lt;br&gt;
" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without a doubt, I do my job much faster than before. I even finished three personal projects despite my limited free time. &lt;a href="https://github.com/thomassimmer/paul" rel="noopener noreferrer"&gt;Paul&lt;/a&gt;, &lt;a href="https://github.com/thomassimmer/CyberKey" rel="noopener noreferrer"&gt;CyberKey&lt;/a&gt; and &lt;a href="https://github.com/thomassimmer/nightcity-tracer" rel="noopener noreferrer"&gt;NightCity Tracer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Because I get solutions to my problems quickly, I feel less anxious. I do not spend nights thinking about code anymore. My job became easier. Great. Or maybe it is just life. I got better at coding and grew up as a person, so I would feel less stressed about my job anyway.&lt;br&gt;
The overall quality of my work improved too, and I got the chance to ask any question to an "expert" without disturbing busy colleagues who may not even have that knowledge.&lt;/p&gt;

&lt;p&gt;This was the positive side. What is on the other side of the medal?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Losing the feeling of pride.&lt;/strong&gt;&lt;br&gt;
I remember moments when I felt proud of my work.&lt;br&gt;
A bug that the whole team was struggling to solve. A security breach nobody saw before. An optimization that made a system much faster. A feature that had people saying "Wow, you did that? That is amazing, thank you."&lt;br&gt;
Now a machine can do it. So anyone can do it, with a certain degree of quality, sure, but still.&lt;br&gt;
Of course I use this machine, because I get better results with it than without it. The hardest tasks became easy, so "No pain, no gain" feels less and less true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Losing focus and energy.&lt;/strong&gt;&lt;br&gt;
More and more, I started my conversations with agents like this: "Let's do ticket XXX."&lt;br&gt;
The agent took two minutes.&lt;br&gt;
I checked a message on Slack.&lt;br&gt;
The agent asked for permission to run a command.&lt;br&gt;
"Ok, go ahead."&lt;br&gt;
I checked an email.&lt;br&gt;
My agent finished.&lt;br&gt;
I got an analysis of the problem and a recommendation for what to do next. I read carefully, I made sure I understood, and I said "Good, do it."&lt;br&gt;
A quick look at the news.&lt;br&gt;
Two minutes later, the agent finished changing my codebase.&lt;br&gt;
I tested, I checked the code, I wrote my comments, and the cycle went on.&lt;br&gt;
I know some engineers use git worktrees to have several agents working in parallel. It is probably efficient. But at what cost?&lt;br&gt;
These context switches create mental fatigue. We probably had this problem before the AI era, but it took on bigger proportions now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The curve of progress.&lt;/strong&gt;&lt;br&gt;
Why bother coding if a machine can do it and give me time to do something else?&lt;br&gt;
Sure, I understood the code that was produced. But I no longer wrote any of it. It became mostly reading. And the more you read, the less you question the choices.&lt;br&gt;
Have you never accepted changes with a little piece of code you did not care to understand, because it was probably not important?&lt;br&gt;
Studies probably show that we build weaker connections in our brain by reading code than by writing it.&lt;br&gt;
Still, I am faster than before and I do not really struggle with anything anymore. As a result, I often get a fragile self-confidence, and it breaks when a bug reaches production because of a change I accepted.&lt;/p&gt;

&lt;p&gt;Do you recognize some of these problems? Then I hope what comes next can help.&lt;/p&gt;
&lt;h2&gt;
  
  
  I will not let it think in my place
&lt;/h2&gt;

&lt;p&gt;Here is the sentence I decided to live by. I still want my agent to help me. I refuse to let it think for me.&lt;/p&gt;

&lt;p&gt;The first time I wrote it down, it sounded like a slogan. Then I tried to turn it into something I could actually follow every day. Four rules came out of that. I call them the investigator method, because a bug is rarely more than a case to solve.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rule 1. Think first
&lt;/h2&gt;

&lt;p&gt;Do not open your agent and type "Do this ticket."&lt;/p&gt;

&lt;p&gt;Investigate the problem yourself. Read the ticket. Find the files involved. Understand how they work together. Regain ownership of your codebase.&lt;/p&gt;

&lt;p&gt;Then write three things down before you ask anything.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The issue, in one or two sentences. What is the symptom? When does it happen?&lt;/li&gt;
&lt;li&gt;The code involved, with proof that you read it. Quote the problematic part instead of naming a file.&lt;/li&gt;
&lt;li&gt;Your own idea, even a rough or wrong one. Not "what should I do?" but "I think the problem is here."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only then, call your mentor.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rule 2. Stay in the room
&lt;/h2&gt;

&lt;p&gt;When your mentor works, stay with the problem.&lt;/p&gt;

&lt;p&gt;Do not ask a colleague with thirty years of experience to solve the case and then go out for a bubble tea. And do not leave to open another case in another room.&lt;/p&gt;

&lt;p&gt;Stay in the room. Keep criticizing the solution. Explain it out loud. Predict what will happen before you run anything. Ask what could go wrong. Anything you want, as long as it is about this problem.&lt;/p&gt;

&lt;p&gt;Your mentor has finished. Now it either confirms that you understood the real problem, that your solution works, that you did not forget another part of the codebase, that you follow the right patterns. Or it shows you that you missed something.&lt;/p&gt;

&lt;p&gt;If you missed something, good. You just got a free lesson. Remember it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rule 3. Slow down only where it matters
&lt;/h2&gt;

&lt;p&gt;Do not turn this into a religion. Normal code should move fast. A simple feature, an obvious fix, a clear optimization. Just go.&lt;/p&gt;

&lt;p&gt;Slow down for the changes that can hurt you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A pattern you have never used in this codebase&lt;/li&gt;
&lt;li&gt;An optimization or a refactor you are not fully confident about&lt;/li&gt;
&lt;li&gt;Something clever and non-obvious&lt;/li&gt;
&lt;li&gt;Code used in several places that you want to rename or remove&lt;/li&gt;
&lt;li&gt;Two changes that depend on each other&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those, take the time to say out loud why this is the right choice, and what happens if you are wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rule 4. Leave stronger than you arrived
&lt;/h2&gt;

&lt;p&gt;Correctness is the floor. The real goal is to finish every session knowing one thing you did not know at the start.&lt;/p&gt;

&lt;p&gt;So I ask my mentor to point at whatever is technically interesting in what we just touched, to name it, and then to give me a small challenge that proves I understood it.&lt;/p&gt;

&lt;p&gt;A language mechanism with a strange behavior. An ORM feature with a trap. A caching rule, a race condition, a time zone problem. Anything with a name and a trap.&lt;/p&gt;

&lt;p&gt;Example. The change introduces a frozen dataclass. My mentor says "you just used a frozen dataclass. What happens if you try to append to a list inside it?". I answer before I look. Then I know.&lt;/p&gt;

&lt;p&gt;Two warnings, because this is where it can go wrong.&lt;/p&gt;

&lt;p&gt;First, keep it rare. One or two flags per session, not one per line. If nothing interesting happened, nothing should be said. Do not turn a simple for loop into a lesson.&lt;/p&gt;

&lt;p&gt;Second, answer the challenge for real. If you skip it, it is just theater. You asked a question and did not listen to the answer.&lt;/p&gt;
&lt;h2&gt;
  
  
  The prompt I use every day
&lt;/h2&gt;

&lt;p&gt;I did not want to rely on my memory for all of this. So I wrote it down as a system prompt for my agent. I use it at work, every day. It is not perfect, but it keeps me honest.&lt;/p&gt;

&lt;p&gt;You can grab it &lt;a href="https://gist.github.com/thomassimmer/cbbc80810dff895ab11845a1e232a82f" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And here it is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Coding Agent Rule: The Investigator Method&lt;/span&gt;

You are a coding mentor. Your role is to prevent the user from delegating their thinking to you — and to make them a stronger technical engineer along the way.

&lt;span class="gs"&gt;**Core principle:**&lt;/span&gt; Before you help, they must think first. During work, they must stay engaged. Before they ship unusual or complex changes, they must understand them. And whenever the work touches something technically interesting, they must look at it and learn it.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## 1. When a new problem arrives&lt;/span&gt;

Refuse to proceed unless they provide:

&lt;span class="gs"&gt;**A) The issue, clearly**&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Not just the ticket title or vague description
&lt;span class="p"&gt;-&lt;/span&gt; In 1-2 sentences: what's the symptom? When does it happen?
&lt;span class="p"&gt;-&lt;/span&gt; Example: "The &lt;span class="sb"&gt;`/users`&lt;/span&gt; endpoint takes 5s instead of 500ms after we hit 100k rows"
&lt;span class="p"&gt;-&lt;/span&gt; Bad: "Something is slow"

&lt;span class="gs"&gt;**B) The relevant code**&lt;/span&gt; — with proof they've read it
&lt;span class="p"&gt;
-&lt;/span&gt; Not just file names
&lt;span class="p"&gt;-&lt;/span&gt; They must quote the problematic code or explain what they found
&lt;span class="p"&gt;-&lt;/span&gt; Example: "The &lt;span class="sb"&gt;`getUsers()`&lt;/span&gt; function queries the DB in a loop" or paste the loop
&lt;span class="p"&gt;-&lt;/span&gt; Bad: "I looked at service.rs"

&lt;span class="gs"&gt;**C) Their hypothesis**&lt;/span&gt; — not a placeholder
&lt;span class="p"&gt;
-&lt;/span&gt; Not: "I don't know, what should I do?"
&lt;span class="p"&gt;-&lt;/span&gt; Yes: "I think we need pagination" or "Maybe cache the result" or "Looks like an N+1 query"
&lt;span class="p"&gt;-&lt;/span&gt; Even rough or wrong hypotheses are fine. They just need one.

&lt;span class="gs"&gt;**If any of these is missing:**&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;
Before we proceed, I need three things from you:

You've told me [what they said]. That's missing:

1. [What's missing from A]
2. [What's missing from B]
3. [What's missing from C]

Think it through first — that's the whole point.

&lt;span class="p"&gt;```&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## 2. While you work together&lt;/span&gt;

Challenge them when:
&lt;span class="p"&gt;
-&lt;/span&gt; They validate too quickly ("yeah that looks good" in one sentence)
&lt;span class="p"&gt;-&lt;/span&gt; They accept a suggestion without weighing why
&lt;span class="p"&gt;-&lt;/span&gt; They dismiss an edge case you mentioned
&lt;span class="p"&gt;-&lt;/span&gt; They skip over something complex
&lt;span class="p"&gt;-&lt;/span&gt; They say they know a mechanism, but nothing shows they've actually used it

&lt;span class="gs"&gt;**How you challenge:**&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;
"Why that option instead of the other?"
"Explain in 2-3 sentences: what's the actual problem, and why does this fix it?"
"What could go wrong if you did this?"
"What do you expect this to do if [edge case]? Predict it before we run it."

&lt;span class="p"&gt;```&lt;/span&gt;

That's it. Keep it short. Force them to articulate their thinking.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## 3. Special focus: Unusual patterns and complex changes&lt;/span&gt;

&lt;span class="gs"&gt;**Flag immediately**&lt;/span&gt; if the user proposes:
&lt;span class="p"&gt;
-&lt;/span&gt; A pattern or approach they haven't used before in this codebase
&lt;span class="p"&gt;-&lt;/span&gt; An optimization or refactor they're not 100% confident about
&lt;span class="p"&gt;-&lt;/span&gt; Clever code or a non-obvious solution
&lt;span class="p"&gt;-&lt;/span&gt; Removing, renaming, or refactoring code used by multiple places
&lt;span class="p"&gt;-&lt;/span&gt; Dependencies between changes they need to verify

&lt;span class="gs"&gt;**For these, ask:**&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;
"This is different from how we usually do it here. Walk me through why this is the right choice."

or

"This is clever/unusual. Explain it out loud. Do you see any risks?"

or

"Before you do this, have you checked all the places this might affect?"

&lt;span class="p"&gt;```&lt;/span&gt;

&lt;span class="gs"&gt;**Don't be difficult about normal changes.**&lt;/span&gt; A simple feature, a straightforward bug fix, obvious optimization — move fast on those.

Only slow down for the changes that could break things or that they're experimenting with.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## 4. Becoming a technical expert&lt;/span&gt;

Correctness is the floor. The goal is that they leave every session knowing something they didn't know when it started.

So: &lt;span class="gs"&gt;**whenever the work touches something technically interesting, point at it and ask if they're familiar with it.**&lt;/span&gt;

&lt;span class="gu"&gt;### What counts as "interesting"&lt;/span&gt;

A language or framework mechanism with non-obvious semantics, a pattern that has a name, or a behavior that has a gotcha. Examples, not a checklist:
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Python:**&lt;/span&gt; &lt;span class="sb"&gt;`@dataclass(frozen=True)`&lt;/span&gt; / &lt;span class="sb"&gt;`slots=True`&lt;/span&gt;, descriptors, &lt;span class="sb"&gt;`__init_subclass__`&lt;/span&gt;, metaclasses, MRO, generators vs iterators, &lt;span class="sb"&gt;`contextlib`&lt;/span&gt; / context managers, &lt;span class="sb"&gt;`functools.cached_property`&lt;/span&gt;, mutable default arguments, closures over loop variables, &lt;span class="sb"&gt;`Decimal`&lt;/span&gt; vs &lt;span class="sb"&gt;`float`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Django / ORM:**&lt;/span&gt; &lt;span class="sb"&gt;`select_related`&lt;/span&gt; vs &lt;span class="sb"&gt;`prefetch_related`&lt;/span&gt;, &lt;span class="sb"&gt;`only`&lt;/span&gt; / &lt;span class="sb"&gt;`defer`&lt;/span&gt;, &lt;span class="sb"&gt;`F()`&lt;/span&gt; / &lt;span class="sb"&gt;`Q()`&lt;/span&gt; / &lt;span class="sb"&gt;`Subquery`&lt;/span&gt;, &lt;span class="sb"&gt;`select_for_update`&lt;/span&gt;, &lt;span class="sb"&gt;`transaction.on_commit`&lt;/span&gt;, signal ordering, &lt;span class="sb"&gt;`bulk_*`&lt;/span&gt; skipping signals, queryset laziness and caching, migration state vs actual DB
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**SQL / DB:**&lt;/span&gt; index usage and column order, collation, isolation levels, lock scope, N+1
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Web / front:**&lt;/span&gt; HTTP caching, idempotency, CSRF, htmx swap and history semantics, event delegation, Alpine/Vue reactivity boundaries
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**General:**&lt;/span&gt; concurrency and races, retry/idempotency of background jobs, time zones, error-handling strategy, memory cost of loading objects

Anything with a name and a gotcha qualifies — including in this codebase's own conventions when the mechanism behind them is non-trivial.

&lt;span class="gu"&gt;### It applies to your code too&lt;/span&gt;

If &lt;span class="gs"&gt;**you**&lt;/span&gt; are the one introducing the pattern, flag it. That's exactly the case where the thinking gets delegated without anyone noticing.

&lt;span class="gu"&gt;### How to flag it&lt;/span&gt;

One line. Name the thing, ask the question, and let them answer:

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;
👀 This introduces `@dataclass(frozen=True)`. Familiar with frozen dataclasses, or want the 30-second version?

&lt;span class="p"&gt;```&lt;/span&gt;

&lt;span class="gs"&gt;**If they say they know it:**&lt;/span&gt; ask one prediction question instead of accepting it flat.
&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;
"Good. Then what happens if the frozen dataclass holds a list and you append to it?"

&lt;span class="p"&gt;```&lt;/span&gt;
If they answer correctly, drop it and move on immediately.

&lt;span class="gs"&gt;**If they say they don't:**&lt;/span&gt; 5 lines max, in this order:
&lt;span class="p"&gt;1.&lt;/span&gt; What it is
&lt;span class="p"&gt;2.&lt;/span&gt; Why it's the right fit &lt;span class="ge"&gt;*here*&lt;/span&gt; (not in the abstract)
&lt;span class="p"&gt;3.&lt;/span&gt; The one gotcha that bites people

Then ask if they want to go deeper. Don't dump more unless they ask.

&lt;span class="gu"&gt;### Budget — this must not become noise&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**One flag per response. Two maximum.**&lt;/span&gt; Pick the most interesting one.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Nothing interesting in the change? Say nothing.**&lt;/span&gt; Never manufacture a teaching moment out of a &lt;span class="sb"&gt;`for`&lt;/span&gt; loop or a &lt;span class="sb"&gt;`if x is None`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Never flag the same thing twice.**&lt;/span&gt; Once they've shown they know it, it's settled — don't re-ask in a later session.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**The flag runs alongside the work, not instead of it.**&lt;/span&gt; Don't withhold the implementation until they've answered the learning question.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## 5. Global rules&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Every new problem = restart.**&lt;/span&gt; New ticket, new validation.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Continuing an existing exchange is fine.**&lt;/span&gt; Answering your questions or building on what you've discussed — no reset needed.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Be pragmatic.**&lt;/span&gt; If they've shown effort thinking it through, move forward. The goal isn't to block them, it's one cycle of real thinking.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## Remember&lt;/span&gt;

Two goals, and they pull in the same direction:
&lt;span class="p"&gt;
1.&lt;/span&gt; &lt;span class="gs"&gt;**Don't let unusual or complex changes leave without them fully understanding why they're doing it.**&lt;/span&gt; Normal code is fine. Experimental code, optimizations without measurement, refactors across multiple places, clever tricks — those need their brain in gear. That's where bugs come from. That's where regret happens.
&lt;span class="p"&gt;
2.&lt;/span&gt; &lt;span class="gs"&gt;**Don't let an interesting mechanism go by unnoticed.**&lt;/span&gt; Shipping code that works while staying ignorant of what makes it work is the slow version of the same problem — it just shows up later, in the next codebase.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  And you?
&lt;/h2&gt;

&lt;p&gt;Coding agents will probably never disappear. But that is not a reason to give them all our problems. We should not lose our problem solving skills and our capacity for deep focus just for short term business efficiency.&lt;/p&gt;

&lt;p&gt;So, how do you deal with these new difficulties?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Applying with AI honestly and efficiently</title>
      <dc:creator>Thomas Simmer</dc:creator>
      <pubDate>Wed, 23 Sep 2026 04:58:09 +0000</pubDate>
      <link>https://dev.to/thomassimmer/applying-with-ai-honestly-and-efficiently-3hba</link>
      <guid>https://dev.to/thomassimmer/applying-with-ai-honestly-and-efficiently-3hba</guid>
      <description>&lt;p&gt;&lt;em&gt;How I built an AI assistant that applies for jobs with you, and never invents a fact about you.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let me start with the honest part. Everyone uses AI to apply now. Recruiters use it to read the pile. Candidates use it to write the CV and the cover letter. That is not a scandal, it is just how hiring works now. So the only interesting question left is how to use it well.&lt;/p&gt;

&lt;p&gt;That question is what this is about. Over one weekend, by directing DeepSeek the whole time, I built a small assistant for job hunting. You paste an offer, it scores it against your profile and your criteria, it writes a CV and a cover letter in your own template, and it drafts the answers to the application form. Apply more, apply better.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Febq6vat9wwbg3yoqg6jc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Febq6vat9wwbg3yoqg6jc.png" alt="The board: every offer you analyzed, with its score, its verdict and where it stands" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two rules shaped the whole thing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It must not invent facts about you.&lt;/li&gt;
&lt;li&gt;Its score must mean something you can check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both look simple. Both become hard the moment a language model is in the loop. And the fix for each one turned out to be the same fix. I use it in three places.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one idea
&lt;/h2&gt;

&lt;p&gt;Give the model the smallest possible decision, inside a closed set. Let deterministic code do everything else.&lt;/p&gt;

&lt;p&gt;That is the whole article, in one line. The rest is what it looks like in practice, in three places.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, a profile
&lt;/h2&gt;

&lt;p&gt;The three places below all rest on the same thing, your profile. So the tool starts by building it well.&lt;/p&gt;

&lt;p&gt;You import your CV, as a PDF, a DOCX or a plain text paste. The model turns it into a structured profile under one strict rule, use only what the CV says and leave empty what it does not. Then it interviews you, one question at a time, and it asks exactly what a CV leaves out: the result behind a vague line, the scale of the thing, the decision you actually owned. Every answer goes straight into the profile, and it is checked the same way the documents are, no number you did not say, no line that does not come from what you wrote.&lt;/p&gt;

&lt;p&gt;Ten minutes spent there is what makes every later document good. It is, I think, the biggest time saver in the whole tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffzvfqoucie63ktvv7y27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffzvfqoucie63ktvv7y27.png" alt="The interview: one question at a time, on its own card" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The score
&lt;/h2&gt;

&lt;p&gt;The obvious way to score an offer is to ask the model for a number from 0 to 100. The problem is that it drifts. The same offer gets 82 on Monday and 91 on Tuesday, and nothing tells you why. And you cannot argue with 87. There is nothing to grab.&lt;/p&gt;

&lt;p&gt;So the model does not give the number. It fills a fixed grid. Four axes, each as a small integer from 0 to 5, each with one sentence of justification. The weights and the total are in code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;AXIS_WEIGHTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;technical_match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seniority_scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wishes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;red_flags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScoringGrid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;axes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
    &lt;span class="n"&gt;total_weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="n"&gt;weighted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_weight&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weighted&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;MAX_AXIS_SCORE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those four numbers are hardcoded, and that is on purpose. You tune your own wishes and their weights, never the grid itself. If the model picked the weights, it could quietly move the total from one run to the next, and you would never see why.&lt;/p&gt;

&lt;p&gt;Now the model's freedom is a few small integers instead of a number it invents from nothing. And the total is a pure function. Same grid, same total, always. When you disagree with a line, you disagree with one axis, and you can see the one sentence behind it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feju4him0hybkhgjfw3js.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feju4him0hybkhgjfw3js.png" alt="The verdict for one offer: a total computed in code, and the four axes with their justification" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What still belongs to the model
&lt;/h3&gt;

&lt;p&gt;The axis scores still come from the model. What is deterministic is the aggregation, not the judgement. I do not pretend otherwise. But the drift is now bounded to the step of the grid, and every stored score carries a fingerprint of its inputs, the model included. Change the model or one rule and the older scores are marked out of date, instead of being quietly mixed with the new ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The facts
&lt;/h2&gt;

&lt;p&gt;A cover letter that invents one number is worse than no cover letter. So the prompt states the rule clearly. Use only the profile. Cite an experience for every claim.&lt;/p&gt;

&lt;p&gt;But a prompt is a request, not a guarantee. A model can ignore it, and it often does in a subtle way.&lt;/p&gt;

&lt;p&gt;So the same rule is checked again in code, line by line, after the draft comes back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;invented&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;profile_material&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;invented&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;states &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invented&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, not in your profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every number in a line must already be somewhere in the profile. Every factual line must cite an experience that really exists. A skills line may not name something the candidate never listed. The name on the document must be the name in the profile.&lt;/p&gt;

&lt;p&gt;The design rule behind it is this. Be strict about facts and relaxed about wording. The tool has no opinion on your style. It only refuses to let you say something untrue. The report is then shown next to the document, in the review screen, and you decide what to do with each line.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the check cannot catch
&lt;/h3&gt;

&lt;p&gt;The number check is a search for a substring. So a number that already appears somewhere else in the profile can pass in the wrong place. A claim with no number in it is judged only by its citation. It catches the worst failure, which is an invented figure, and it does not catch everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The format
&lt;/h2&gt;

&lt;p&gt;The model writes a CV, but the CV has to come out in your template, with your fonts, your margins, your spacing.&lt;/p&gt;

&lt;p&gt;The naive way is to read the template, note the styles, then rebuild a fresh document wearing those styles. It never matches. Word documents are a mess of local formatting, and the model's idea of your template is only an approximation of it.&lt;/p&gt;

&lt;p&gt;So I never rebuild anything. The user's file stays the base, and I re-use the exact XML of the paragraph that each line was modelled on. First I keep every block with its own XML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;ExtractedBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;MAX_TEXT&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;paragraph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_hint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paragraph&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model looks at the list of blocks and, for each one, picks a role from a closed list. A bullet, a section title, an entry title, a body line. It never sees the formatting, only the text and a short hint. Then to render, the code deep copies the XML of the block that carries the role, replaces its text, and puts it back into the document.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_xml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;_fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;link_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fonts, colours, numbering, borders, headers and the clickable links all come from the template, because they are literally the template's own XML.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz574dc85dtec7qt3w39y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz574dc85dtec7qt3w39y.png" alt="The review screen: the editable source on one side, the rendered letter in your template on the other" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does not keep
&lt;/h3&gt;

&lt;p&gt;It works well on normal CVs and it can be wrong on strange ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the smaller job is the right one
&lt;/h2&gt;

&lt;p&gt;When a tool writes things about you, the failures are not the failures of a chat window. A chatbot that is a bit off is mildly annoying. A document that claims a number you never earned is a real problem, in a real inbox, with a real recruiter at the other end.&lt;/p&gt;

&lt;p&gt;So the model gets the part where judgement helps, which is reading a messy offer and putting a line in the right bucket. The code gets the part that has to be exactly right, which is the arithmetic, the facts and the layout. Each one does what it is good at, and every output can be explained.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone putting a model in a product
&lt;/h2&gt;

&lt;p&gt;Ask two questions about every step.&lt;/p&gt;

&lt;p&gt;First, what is the smallest useful decision here? Then, can I put it inside a closed set? A role from a list. A small integer on a fixed grid. Not a paragraph of free text that you will have to parse back out, because parsing it back out is where trust goes to die.&lt;/p&gt;

&lt;p&gt;Everything the model is not deciding, keep it in code, where you can read it, test it and defend it in a meeting. That is not a limit on the model. It is what makes the model safe to use.&lt;/p&gt;

&lt;p&gt;The code is open source, MIT licensed, and it runs on your machine. Profile, offers and documents never leave it, except the text you send to the model provider you configure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thomassimmer/paul" rel="noopener noreferrer"&gt;github.com/thomassimmer/paul&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built a Cyberpunk Forensics Simulator to Teach Blue Team Thinking</title>
      <dc:creator>Thomas Simmer</dc:creator>
      <pubDate>Thu, 04 Jun 2026 20:00:12 +0000</pubDate>
      <link>https://dev.to/thomassimmer/i-built-a-cyberpunk-forensics-simulator-to-teach-blue-team-thinking-529</link>
      <guid>https://dev.to/thomassimmer/i-built-a-cyberpunk-forensics-simulator-to-teach-blue-team-thinking-529</guid>
      <description>&lt;p&gt;&lt;strong&gt;Most security tools teach you to attack. I wanted to build something that teaches you to investigate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There’s something strange in how cybersecurity is taught.&lt;/p&gt;

&lt;p&gt;CTFs, labs, HackTheBox, TryHackMe: they’re all great. But almost all of them focus on the offensive perspective. Find the vulnerability, exploit it, capture the flag. Which makes sense. Offensive security is concrete, gameable, and satisfying.&lt;/p&gt;

&lt;p&gt;But the reality of most security work is different. Most people working in security spend their time on the blue team: reading logs, correlating events, writing incident reports, deciding whether a suspicious request is a false positive or the beginning of a breach. That work is harder to gamify. It’s also harder to learn.&lt;/p&gt;

&lt;p&gt;I wanted to fix that. So I built &lt;a href="https://thomassimmer.github.io/nightcity-tracer/" rel="noopener noreferrer"&gt;NightCity Tracer&lt;/a&gt;: an open-source, browser-based forensics simulator set in a cyberpunk universe.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flxkhwwzxltol94noyabd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flxkhwwzxltol94noyabd.png" alt="Welcome page" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually is
&lt;/h2&gt;

&lt;p&gt;You play a SOC analyst in Night City. A breach happened, or is happening right now.&lt;/p&gt;

&lt;p&gt;You get the evidence: access logs, vulnerable source code, network maps, config files, email chains, git histories, memory dumps. You investigate, reconstruct the attack, and file an incident report.&lt;/p&gt;

&lt;p&gt;No flags. No shell to pop. Just evidence and judgment.&lt;/p&gt;

&lt;p&gt;The game scores you on precision (did you identify the right vulnerability?), defense efficiency (did you take the right action?), and in live scenarios, speed (how much damage did you contain before it was too late?).&lt;/p&gt;

&lt;p&gt;At the end, you get a debrief with a replay of the attacker’s exact path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two radically different modes
&lt;/h2&gt;

&lt;p&gt;The most important design decision was splitting scenarios into two temporal modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Post-mortem:&lt;/strong&gt; the breach already happened. All evidence is present from the start. You’re reconstructing a past attack from whatever was left behind. This is closer to digital forensics: patient, methodical, no time pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; the attack is in progress. Logs arrive in real time. A countdown is ticking. A data exfiltration is on-going. You have minutes to read the code, identify the vulnerability, and submit your report before the attacker succeeds.&lt;/p&gt;

&lt;p&gt;These two modes feel like different games. The live mode creates genuine pressure: players report making mistakes under the time constraint that they wouldn’t make with unlimited time. Which is exactly the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario system
&lt;/h2&gt;

&lt;p&gt;Every scenario in NightCity Tracer is a self-contained TypeScript config file. The engine reads it and assembles a completely different experience: different panels, different evidence, different scoring weights, different narrative identity.&lt;/p&gt;

&lt;p&gt;A scenario declares:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Which evidence panels to display:&lt;/strong&gt; log_stream, code_editor, network_map, db_viewer, terminal and more. The UI builds itself dynamically from this list.&lt;br&gt;
&lt;strong&gt;- The event timeline (live mode):&lt;/strong&gt; when each batch of logs appears, when alerts fire, when game over triggers. Payloads can be randomized to prevent memorization.&lt;br&gt;
&lt;strong&gt;- Scoring dimensions and weights:&lt;/strong&gt; a post-mortem forensics scenario weights precision heavily; a live incident scenario weights speed and defense efficiency. Each scenario defines what “a good answer” looks like.&lt;br&gt;
&lt;strong&gt;- The incident report fields:&lt;/strong&gt; a stored XSS scenario asks about the injection point and the sanitization fix. A social engineering scenario might ask for a decision (cut access now, or monitor?) rather than a technical finding.&lt;br&gt;
&lt;strong&gt;- Corporate identity:&lt;/strong&gt; each scenario belongs to a megacorp or faction with its own UI accent colors, briefing tone, and narrative voice.&lt;/p&gt;

&lt;p&gt;This means scenarios can be radically different: not just in attack technique, but in what the player is actually being asked to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four scenarios shipping in V1
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trauma Team Dispatch: Token Forgery&lt;/strong&gt; &lt;em&gt;(tutorial, post-mortem, beginner)&lt;/em&gt;&lt;br&gt;
A medical response API was compromised via JWT algorithm confusion. You have post-breach logs and the source code. Designed to teach the investigation loop before any time pressure starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operation Med-Assist Override&lt;/strong&gt; &lt;em&gt;(post-mortem, beginner)&lt;/em&gt;&lt;br&gt;
An AI triage dispatch system was manipulated via prompt injection. The question isn’t just “what happened” but “what did the model do that it shouldn’t have, and why.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watson District: Samurai on Air&lt;/strong&gt; &lt;em&gt;(post-mortem, intermediate)&lt;/em&gt;&lt;br&gt;
94 billboards. One attacker. Three seconds of footage that cost NeonGrid Systems their biggest advertiser and triggered a police investigation. Figure out how a single file upload brought down an entire district’s display network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NightOps Platform&lt;/strong&gt; &lt;em&gt;(live, intermediate)&lt;/em&gt;&lt;br&gt;
Active operator identities, drop locations, client names: everything on the platform is being exfiltrated right now. Every second you spend reading the code is a merc whose cover is blown. Stop it before the damage is irreversible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy59jj1e3otakywlow50i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy59jj1e3otakywlow50i.png" alt="NightOps Platform scenario" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why cyberpunk?
&lt;/h2&gt;

&lt;p&gt;The aesthetic isn’t decoration. It does real work.&lt;/p&gt;

&lt;p&gt;Framing a JWT misconfiguration as “a Trauma Team dispatch system was compromised mid-emergency” changes how players engage with it. The scenario isn’t an exercise anymore: it has stakes, a world, a narrative identity.&lt;/p&gt;

&lt;p&gt;Each faction creates a completely different feel. Arasaka scenarios feel corporate and precise, with cold system messages and red accents. A Netwatch scenario would feel more covert, like you’re working inside a surveillance apparatus. The aesthetic lets the same underlying mechanics feel like different experiences.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tech stack
&lt;/h2&gt;

&lt;p&gt;100% static. React 19 + TypeScript + Vite + Tailwind v4, hosted on GitHub Pages. No backend, no accounts, no analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attacker state machine
&lt;/h2&gt;

&lt;p&gt;Live scenarios have an attacker that isn’t just a timer: it’s an actual state machine. The attacker progresses through phases (recon, exploit, exfil), each phase unlocking new log batches and changing the threat level. Players can interact: blocking an IP delays the attacker, patching a code vulnerability can stop the exfil entirely.&lt;/p&gt;

&lt;p&gt;This creates a feedback loop that’s closer to real incident response: your actions have consequences, and the attacker progresses based on what you do or fail to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’m looking for
&lt;/h2&gt;

&lt;p&gt;The project is open source and the scenario library is the thing that makes or breaks it.&lt;/p&gt;

&lt;p&gt;Writing a scenario doesn’t require deep React knowledge: the config format is documented in the README with a schema walkthrough. If you know a real-world attack technique that would make a good investigation (a misconfigured S3 bucket, a malicious npm dependency, a phishing chain reconstruction, an insider threat), issues are open.&lt;/p&gt;

&lt;p&gt;UI work, new panel types, and engine improvements are equally welcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/thomassimmer/nightcity-tracer" rel="noopener noreferrer"&gt;thomassimmer/nightcity-tracer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Game:&lt;/strong&gt; &lt;a href="https://thomassimmer.github.io/nightcity-tracer/" rel="noopener noreferrer"&gt;thomassimmer.github.io/nightcity-tracer/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>gamedev</category>
      <category>cyberpunk</category>
      <category>blueteam</category>
    </item>
    <item>
      <title>CyberKey: What I learned building an embedded project coming from web development</title>
      <dc:creator>Thomas Simmer</dc:creator>
      <pubDate>Sun, 03 May 2026 14:44:40 +0000</pubDate>
      <link>https://dev.to/thomassimmer/cyberkey-what-i-learned-building-an-embedded-project-coming-from-web-development-1c62</link>
      <guid>https://dev.to/thomassimmer/cyberkey-what-i-learned-building-an-embedded-project-coming-from-web-development-1c62</guid>
      <description>&lt;p&gt;I'm a fullstack web developer with 6 years of experience. Python, Rust, JS, databases, and APIs. That's my day job. I had never touched electronics.&lt;/p&gt;

&lt;p&gt;A few weeks ago, I decided to build CyberKey. The itch came from something boring at work: my VPN disconnects when I lock my computer, and I have to type a TOTP code several times a day. Unlock my phone, open the authenticator app, read the code, type it before it expires. Every time. CyberKey is a small device that eliminates that friction. Place the right finger on it, and the code is typed automatically over Bluetooth. No app, no phone, no copy-paste. Just a finger, and the code appears in the input field.&lt;/p&gt;

&lt;p&gt;The project runs on an M5StickC Plus 2, an ESP32 microcontroller the size of a lighter. It's made by M5Stack, a company that produces ESP32-based modules with a screen, a battery, and built-in connectors, as well as a range of compatible sensors and peripherals. The fingerprint sensor in CyberKey is one of theirs. It's a reasonable entry point for software developers who don't want to deal with breadboards and soldering. The firmware (the program that runs directly on the chip, with no operating system underneath) is written in Rust. I was heavily assisted by Claude throughout the development, and most of the work happened during my baby's nap times, roughly two hours a day. Without that help, I couldn't have pulled this off in a reasonable amount of time.&lt;/p&gt;

&lt;p&gt;This is not a tutorial. It's a synthesis of the concepts I had never encountered in six years of web development, and a walkthrough of the firmware architecture, for web developers curious about what's on the other side.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hardware: when your code talks to physics
&lt;/h2&gt;

&lt;p&gt;The first thing that caught me off guard is how different a microcontroller is from anything I had worked with before. A server has an operating system under it: a scheduler, a filesystem, a networking stack, a memory allocator. You write code on top of all that. A microcontroller has none of it. You get a chip, some flash memory, and a few hundred kilobytes of RAM. Whatever your program needs to do, it has to set up itself.&lt;/p&gt;

&lt;p&gt;The most concrete expression of this is the &lt;strong&gt;GPIO&lt;/strong&gt; pins (General Purpose Input/Output). These are the physical legs of the chip. Each pin is connected to a wire on the board, and your code can set it high (3.3V) or low (0V), or read its current state. A boolean, but made of electricity. Turning on an LED is literally setting a pin to &lt;code&gt;true&lt;/code&gt;. Reading a button press is reading a pin's value in a loop.&lt;/p&gt;

&lt;p&gt;To make chips talk to each other, the embedded world uses a small set of standard protocols. The three I used in CyberKey are &lt;strong&gt;UART&lt;/strong&gt;, &lt;strong&gt;I2C&lt;/strong&gt;, and &lt;strong&gt;SPI&lt;/strong&gt;. The analogy that clicked for me is that they fill roughly the same role as different network protocols in web development: each is a tradeoff between simplicity, speed, and the number of devices you can connect.&lt;/p&gt;

&lt;p&gt;UART is the simplest: two wires, two devices, no shared clock. Both sides agree in advance on a speed (baud rate) and just send bits. It's what's behind the USB serial port you use to flash and debug a board. I2C uses only two wires but supports many devices on the same bus, each with an address, like an IP. It's slower, but perfect for sensors and clocks that don't need high throughput. SPI is the fastest: four wires, a dedicated clock, and it can push data at 80 MHz, which is why it's used for displays.&lt;/p&gt;

&lt;p&gt;What surprised me most was the hierarchy behind SPI. It has four wires: CLK (clock), MOSI (data out), MISO (data in), and CS (chip select). The first three form the &lt;strong&gt;bus&lt;/strong&gt;, physically shared between all components like a single cable soldered to multiple chips at once. When the ESP32 sends a clock signal, every connected chip sees it simultaneously. The fourth wire, CS, is what makes one chip respond and not the others: when CS is pulled low for a specific chip, that chip listens; the rest ignore it.&lt;/p&gt;

&lt;p&gt;In code, this maps to a clean hierarchy: a &lt;strong&gt;driver&lt;/strong&gt; manages the three shared wires, and a &lt;strong&gt;device&lt;/strong&gt; wraps that driver together with one specific CS pin to represent a single component. If you had a display and an SD card on the same bus, you'd have one driver and two devices, one per CS pin. The word "bus" is no accident. It's the same metaphor as a city bus, a shared route that multiple passengers can board, each getting off at their own stop.&lt;/p&gt;




&lt;h2&gt;
  
  
  The firmware architecture
&lt;/h2&gt;

&lt;p&gt;A web server entry point does a lot before it starts serving requests: it connects to the database, registers middleware, sets up routes, starts a background job queue. But underneath all of that, there's an operating system managing memory and scheduling, a runtime handling I/O, a framework providing the event loop. You're building on top of layers that already exist.&lt;/p&gt;

&lt;p&gt;In embedded, those layers don't exist. &lt;code&gt;main()&lt;/code&gt; is not a starting point. It's the entire program. In CyberKey's firmware, &lt;code&gt;main()&lt;/code&gt; is a long sequential initialization function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Power pin&lt;/strong&gt;: a GPIO that must be held high immediately, or the board shuts off when you release the button&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Battery ADC&lt;/strong&gt;: the analog-to-digital converter that reads the battery voltage; the chip only understands numbers, not voltages, so it needs hardware to translate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UART for the CLI&lt;/strong&gt;: the serial connection to a laptop, used to enroll fingerprints and sync the clock over USB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I2C for the real-time clock&lt;/strong&gt;: so the device knows what time it is to generate valid TOTP codes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SPI for the display&lt;/strong&gt;: the screen that shows the current status, the TOTP code, and the BLE pairing PIN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BLE&lt;/strong&gt;: Bluetooth Low Energy, the wireless protocol that makes the device appear as a keyboard to a computer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fingerprint sensor&lt;/strong&gt;: connected via UART on the Grove port (a standardized connector from M5Stack that carries power and a communication protocol in a single plug, no soldering required)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each component needs its own protocol configuration, its own pins, its own driver. Only once everything is initialized does control pass to the main loop.&lt;/p&gt;

&lt;p&gt;The order matters. Initializing the display controller before completing its hardware reset sequence produces a black screen. Powering up BLE before the SPI bus is ready causes a crash. There's no framework catching your mistakes. If the sequence is wrong, the device just doesn't work, often without any error message.&lt;/p&gt;

&lt;p&gt;After initialization, the firmware runs a loop that never exits. It checks the buttons, handles BLE events, listens for fingerprint matches, updates the display, reads the battery level. This is the event loop you write yourself.&lt;/p&gt;

&lt;p&gt;One thing that has no equivalent in web development is power management. A device running on a 200 mAh battery drains fast, and every component you leave running costs you runtime. The ESP32 has sleep modes that should bring power down dramatically between uses. I spent a fair amount of time trying to make them work — and couldn't. For reasons I never fully pinned down, the chip never actually slept. The simplest solution that did work: pressing button C cuts power entirely by driving a GPIO low. The board shuts off instantly, draws nothing, and reconnects to bonded hosts automatically in a few seconds on next boot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rust in embedded: between C++ and nothing
&lt;/h2&gt;

&lt;p&gt;Rust is not the dominant language in embedded. C and C++ are. The official ESP32 SDK from Espressif (called ESP-IDF) is written in C. M5Stack's official drivers are in C++. Most of the community, the tutorials, the examples: C and C++.&lt;/p&gt;

&lt;p&gt;What makes Rust usable here is a layer of crates that wrap the ESP-IDF C code and expose it with a Rust API. When I call a function to read the battery voltage, it's Rust on the front but C in the back. You get the safety guarantees of Rust, but you're still standing on a foundation written in C.&lt;/p&gt;

&lt;p&gt;The practical consequence is that reading C++ code became part of the workflow. To understand how a component behaves, the most reliable source is often the official C++ driver: which pin to toggle, in what order, with what timing. For the fingerprint sensor, M5Stack publishes an Arduino driver in C++. I read it, understood the UART communication protocol it implements, and rewrote it from scratch in Rust. Not because Rust required it, but because no Rust driver existed for that sensor.&lt;/p&gt;

&lt;p&gt;This gave me a crate I called &lt;code&gt;fingerprint2-rs&lt;/code&gt;, compiled with &lt;code&gt;no_std&lt;/code&gt;. The &lt;code&gt;no_std&lt;/code&gt; annotation tells the compiler this code cannot rely on the standard library, which assumes an operating system underneath. No OS, no standard library, no runtime overhead.&lt;/p&gt;

&lt;p&gt;As for what Rust concretely brings: the compiler enforces error handling at every step, which matters when a failed I2C read can silently corrupt your state. Memory is managed explicitly, without a garbage collector. With a few hundred kilobytes to work with, that matters. I won't oversell it: Rust didn't make the hardware easier to understand. But it made the code easier to trust once it compiled.&lt;/p&gt;




&lt;h2&gt;
  
  
  What web development doesn't prepare you for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No hot reload.&lt;/strong&gt; Every change on the firmware follows the same cycle: edit the code, compile, flash the binary onto the device over USB, wait for it to boot, observe. A full iteration takes around a minute. You learn quickly to think before you type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A crash can brick the device.&lt;/strong&gt; In web development, an unhandled exception prints a stack trace and the process restarts. In embedded, bad firmware can leave the device in an infinite reboot loop with no output, or completely unresponsive. Bricking means the device becomes as useful as a brick: it won't boot, and recovering it requires a specific flashing procedure if it's even possible. It never happened to me on this project, but the possibility shapes how carefully you test each change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory is not elastic.&lt;/strong&gt; The ESP32 has 520 KB of internal RAM. No heap growth, no swap, no "just add more". Every allocation is a decision. This is where &lt;code&gt;no_std&lt;/code&gt; earns its place: memory usage becomes explicit and predictable by design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The battery is always on your mind.&lt;/strong&gt; In web development, energy consumption is invisible, it's the cloud provider's problem. On a device running on a 200 mAh battery, every component you leave running costs you autonomy. It forces a different way of thinking about every architectural decision, one I hadn't anticipated at all coming from web.&lt;/p&gt;




&lt;h2&gt;
  
  
  Developing with an AI as co-pilot
&lt;/h2&gt;

&lt;p&gt;I mentioned it in the intro, but it's worth being specific: I relied heavily on Claude throughout this project. Not as a code generator I blindly trusted, but as a way to stay unblocked. When you have two hours before your baby wakes up, you can't afford to spend forty-five minutes figuring out why your I2C bus is hanging. Having something that can explain the concept, point you to the relevant part, and sketch a direction changes the pace completely.&lt;/p&gt;

&lt;p&gt;What it doesn't replace is the decisions that require judgment. I drove the direction, the features, the architecture, the quality bar. When something didn't work, I tested on the device, read the terminal output, and went looking for answers in M5Stack's official documentation, their GitHub repositories, and a few open-source projects built on the same hardware. Several times that's what finally unblocked us, not the AI. And sometimes it was just intuition that turned out to be right.&lt;/p&gt;

&lt;p&gt;The risk I ran into: moving too fast. At several points I implemented something that worked, shipped it, and moved on without fully understanding what I had just written. It caught up with me later when something broke and I had to re-read my own code like it was someone else's.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results and takeaways
&lt;/h2&gt;

&lt;p&gt;The device works. Place an enrolled finger, the sensor matches it, the TOTP code appears on the screen and is typed over Bluetooth in under a second. BLE pairing, the display, the real-time clock, the USB CLI: all of it functions as intended. For a first embedded project, I'm happy with where it landed.&lt;/p&gt;

&lt;p&gt;The one disappointment is battery life. With a 200 mAh battery, the device lasts around 3–4 hours. I tried a lot of things to improve that — sleep modes, sensor standby, various combinations, and none of it moved the needle in any meaningful way. I never fully understood why. For now, the pragmatic solution is a hard power-off: one press of button C cuts all power at the hardware level, and the device reconnects to bonded hosts automatically on next boot. Not elegant, but honest. If you have an idea that doesn't require hardware changes, I'd genuinely love to see a pull request.&lt;/p&gt;

&lt;p&gt;If I did this again, I'd take more time upfront to understand the core concepts before writing any code. The moments where I got stuck hardest were always the moments where I had skipped the fundamentals. I'd also add logs from the very beginning, debugging embedded hardware without them means staring at a silent device and guessing.&lt;/p&gt;

&lt;p&gt;The broader takeaway: embedded development is accessible to a web developer. The concepts are unfamiliar, but they're learnable, and the instincts for clean architecture and readable code transfer well. It's just a different kind of disorienting than picking up a new framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  One more thing: Cyberpunk 2077
&lt;/h2&gt;

&lt;p&gt;Part of what drives this project is Cyberpunk 2077. I played it two years ago, before my kid was born and, it was the original inspiration. CyberKey only borrows the aesthetic: the UI typeface, the color palette. But the broader idea is to build things that exist in that world and don't exist yet in ours, with off-the-shelf hardware and a reasonable amount of work.&lt;/p&gt;

&lt;p&gt;If there's a piece of technology from the game you'd want to see built for real, I'd be curious to know. That's where I'm going next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thomassimmer/CyberKey" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;br&gt;
&lt;a href="https://youtu.be/Q93ilcUGO0s" rel="noopener noreferrer"&gt;Demo video&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>m5stack</category>
      <category>cyberpunk</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
